Codeforces Global Round 20 A-F,H

连续一个月掉小分,这把终于上大分了 好耶!

A. Log Chopping

题意:现在有 \(n\) 个木棍,第 \(i\) 根长度为 \(a_i\),Alice和Bob两人轮流操作,可以选择一根木棍将其切成两半,且两半的长度都需要是>1的整数。最后无法操作的人失败,问谁获胜。

虚假博弈,显然能切的次数是固定的,最终会全变成长度为1的木棍,判断能切的奇偶性即可。

int n, m;

void solve(){
    cin >> n;
    int sum = 0;
    for(int i=1;i<=n;i++){
        int x; cin >> x;
        sum += x - 1;
    }
    if(sum % 2) cout << "errorgorn\n";
    else cout << "maomao90\n";
}

B. I love AAAB

题意:你有一个字符串(初始为空),操作:可以在其中任意位置插入一段形如"AA...AB"的字符串,A的数量至少为1。给定目标串b,问操作任意次,能否构造出串b。

类似括号序,出现一个B时前面要至少找到一个A与其匹配,那么从左到右扫一遍,将A视为1,B视为-1,看有无前缀和小于0即可。注意特判:不存在B也是不行的。

int n, m;

void solve(){
    string s;
    cin >> s;
    int sum = 0;
    for(int i=0;i<s.size();i++){
        if(s[i] == 'A') sum++;
        else sum--;
        if(sum < 0){
            cout << "NO\n";
            return;
        }
    }
    if(s.back() != 'B'){
        cout << "NO\n";
        return;
    }
    cout << "YES\n";
}

C. Unequal Array

题意:给出一个序列 \(a\),其质量定义为其中 \(a_i=a_{i+1}\) 的元素个数,现在你可以进行操作:选取两个相邻元素,将它们修改为任意的 \(x\)。问:最少多少次操作可以使得序列的"质量"小于等于1

不难发现,执行这个操作只能改变本来满足的 \(a_i=a_{i+1}\) 的位置移动,而无法直接消除。那么我们找到原序列最早和最晚出现的相邻点,只有将这两对相邻点不断逼近后合为一对,才能满足条件。那么答案即为这两对相邻点之间的距离(注意边界和0)

int n, m;
int a[maxn];

void solve(){
    cin >> n;
    for(int i=1;i<=n;i++) cin >> a[i];
    int mi = 0, mx = 0;
    for(int i=1;i<n;i++){
        if(a[i] == a[i+1]){
            if(!mi) mi = i;
            mx = i;
        }
    }
    if(mx == mi) cout << "0\n";
    else cout << max(mx-mi-1, 1) << '\n';
}

D. Cyclic Rotation

题意:给出一个序列 \(a\) 和它的重新排列 \(b\),有一种操作:选取两个相同元素,将左边的移动到另一个右边。问能否对序列 \(a\) 进行任意次操作,使其变为 \(b\)

模拟,我们从最末端开始考虑,\(a\)\(b\) 的末尾元素必须相同,设他们末尾都为 \(x\)\(a\) 末尾连续的 \(x\) 数量必须少于 \(b\) 末尾连续 \(x\) 的数量。这样才有可能从 \(a\) 前面拿若干个 \(x\) 到末尾来使得他们末尾。记录每种数被拿的次数。考虑完后,将他们末尾的 \(x\) 都删掉,继续处理下一个末尾。

遇到 \(a\)\(b\) 末尾不相同时,如果 \(a\) 的末尾恰好”被拿过“,就可以忽略 \(a\) 中这个末尾,记得让计数-1。如果没有被拿过,那么NO

还有一点:当 \(a\) 的末尾连续 \(x\) 长度大于 \(b\) 末尾连续 \(x\) 长度时,如果 \(x\) 可以被拿走一些也是可行的,不用直接NO。

这样模拟能执行到最后就是YES

int n, m;
int a[maxn], b[maxn];
int rem[maxn], del[maxn];

void solve(){
    cin >> n;
    for(int i=1;i<=n;i++) rem[i] = del[i] = 0;
    for(int i=1;i<=n;i++) cin >> a[i], rem[a[i]]++;
    vector<int> tmp(n+1, 0);
    for(int i=1;i<=n;i++) cin >> b[i], tmp[b[i]]++;
    for(int i=1;i<=n;i++) if(tmp[i] != rem[i]){
        cout << "NO\n";
        return;
    }

    int r1 = n, j = n;
    while(j >= 1){
        while(a[r1] != b[j]){
            if(r1>0 && del[a[r1]]) del[a[r1]]--, r1--;
            else{
                cout << "NO\n";
                return;
            }
        }
        int lena = 1, lenb = 1;
        for(int i=r1-1;i>=0;i--){
            if(a[i] != a[r1]){
                lena = r1 - i;
                break;
            }
        }
        for(int i=j-1;i>=0;i--){
            if(b[i] != b[j]){
                lenb = j - i;
                break;
            }
        }
        if(lena > lenb){
            if(lena - del[a[r1]] > lenb){
                cout << "NO\n";
                return;
            }
            del[a[r1]] -= lena - lenb;
            r1 -= lena;
            j -= lenb;
        }else{
            del[a[r1]] += lenb - lena;
            r1 -= lena;
            j -= lenb;
        }
    }
    cout << "YES\n";
}

E. notepad.exe

题意:交互题,有一台打字机和若干个字符串,这些字符串是未知的,你可以指定最大打印宽度 \(w\),打印机将按顺序打印这些字符串,两串之间需要一个空格,当行宽度不够即会换行,最终会返回你打印的行数 \(h\)。你需要用至多 \(n+30\) 次询问,输出可能的最小打印面积(\(w\times h\)

首先容易想到的是,可以二分找到一行能打下所有字符串的最小宽度 \(w_1\),此时最小打印面积为 字符串总长+n-1个空格

然后去想,如果要打印两行使得答案更优,那么这个字符串必须差不多能正好分成等长的两半,这样就能贪一个空格。只需要尝试 \(\lfloor w_1/2\rfloor\) 的宽度即可,宽度更大则不可能更优,更小则不可能两行打印完。

同理,尝试三行,四行...尝试第 \(i\) 行即询问宽度 \(\lfloor w_1/i\rfloor\),更新答案。

int n, m;

int query(int x){
    cout << "? " << x << endl;
    cout.flush();
    int r; cin >> r;
    return r;
}

void solve(){
    cin >> n;
    int l = 1, r = 4000000;
    while(l < r){
        int mid = (l + r) / 2;
        if(query(mid) == 1){
            r = mid;
        }else{
            l = mid+1;
        }
    }
    ll ans = l;
    for(int i=2;i<=n;i++){
        int h = query(l / i);
        if(h == 0) break;
        ans = min(ans, 1ll * h * (l / i));
    }
    cout << "! " << ans << endl;
    cout.flush();
}

F. Array Shuffling & Checker for Array Shuffling

题意:给出一个序列 \(a\),你需要将其打乱,使得其复原需要的最少交换次数尽可能多。

F1:构造一个打乱后的序列。

F2:给定打乱后的序列,判断其是否满足条件(写个spj)

首先要将问题简化,注意到这题其实 \(a\) 的顺序是不影响打乱方案的(写代码的时候要管,但逻辑上不影响结果),我们可以将 \(a\) 中相同的元素都放在一起

再进一步,对于每一种元素,记录其出现的次数,将所有元素按照出现次数排序。每个元素都依次放到下一个元素的出现位置上(有空缺,先不管),最后一个元素去填补所有空缺。这样似乎能贪心地让序列尽可能乱。

严谨地考虑:假设我们有了打乱后的序列 \(b\),现在将每个 \(a_i\)\(b_i\) 连边,那么我们发现复原的过程其实是一个走环的过程(任何排列其实都构成若干个环,不过因为我们这里有重复元素,不是几个独立环的组合,但复原过程还是可以拆成走若干次环)

对于一个长度为 \(l\) 的环,将其复原到位需要 \(l-1\) 次,那么可以想到,环越少,复原需要的次数就越多。

假设出现次数最多的元素出现了 \(x\) 次,那么显然环不可能少于 \(x\) 个,上述构造方案恰恰可以令环数仅为 \(x\)(即除了出现最多的那个元素涉及的环之外,不存在其他环)

F2就是这个结论,将最多的那个元素删去后,拓扑判断是否存在环即可。

F1:

int n, m;
int a[maxn];

struct sss{
    vector<int> pos;
    int val;
}s[maxn];

void solve(){
    cin >> n;
    for(int i=1;i<=n;i++) s[i].pos.clear(), s[i].val = i;
    int mxa = 0;
    for(int i=1;i<=n;i++){
        cin >> a[i];
        s[a[i]].pos.push_back(i);
    }
    vector<int> ans(n+1, 0);
    sort(s+1, s+n+1, [](sss x, sss y){
        return x.pos.size() < y.pos.size();
    });
    for(int i=1;i<n;i++){
        if(s[i].pos.empty()) continue;
        for(int j=0;j<s[i].pos.size();j++){
            ans[s[i+1].pos[j]] = s[i].val;
        }
    }
    for(int i=1;i<=n;i++) if(!ans[i]) ans[i] = s[n].val;
    for(int i=1;i<=n;i++) cout << ans[i] << " \n"[i == n];
}

F2:

int n, m;
int a[maxn], b[maxn];
int cnt[maxn];
int mxp;

vector<int> vp[maxn];

void solve(){

    cin >> n;
    for(int i=1;i<=n;i++) cnt[i] = 0;
    for(int i=1;i<=n;i++){
        vp[i].clear();
        cin >> a[i];
        cnt[a[i]]++;
    }
    for(int i=1;i<=n;i++) cin >> b[i];
    mxp = 1;
    for(int i=1;i<=n;i++) if(cnt[i] > cnt[mxp]) mxp = i;
    vector<int> du(n+1, 0);
    for(int i=1;i<=n;i++){
        if(a[i] == mxp || b[i] == mxp) continue;
        vp[a[i]].push_back(b[i]);
        du[b[i]]++;
    }
    queue<int> q;
    for(int i=1;i<=n;i++) if(du[i] == 0) q.push(i);

    int ans = 0;
    while(!q.empty()){
        int p = q.front();
        q.pop();
        ans++;
        for(int v: vp[p]){
            du[v]--;
            if(du[v] == 0) q.push(v);
        }
    }

    if(ans == n) cout << "AC\n";
    else cout << "WA\n";
}

H. Zigu Zagu

题意:有一个01字符串,你可以操作:选择一段01交互出现的区间(不存在连续0或连续1),删去这个区间。有Q个询问,每次询问 \(s[l..r]\) 这个子串需要最少几次操作使得其被删为空。

贪心地考虑不难发现,一定删尽可能长的区间,我们可以首先把整串拆成几个可删的区间。

决策点在于:如果先删去中间的一个形如 0...11...0的区间,就能使两端区间合并,从而减少删的次数。

我们用一个区间的结尾来标记(指代)这个区间,区间的开头即为上一个区间的结尾(不然上一个区间应该扩展)

比如我用 01101 表示这样的一个整串:..0 0..1 1..1 1..0 0..1

即当我删去一个1时,如果前一位为0,这个0会自动和下一位合并,相当于一同被删去。也就是说,我们有删1,删0,删01,删10这四种操作

那么删除需要的次数即为0和1中较多的那个数!

最后考虑一下边界即可

int n, m;
int sum[maxn][2];
string s;

void solve(){
    cin >> n >> m >> s;
    s = ' ' + s;
    s += s.back();
    for(int i=1;i<=n;i++){
        sum[i][0] = sum[i-1][0];
        sum[i][1] = sum[i-1][1];
        if(s[i] == s[i+1]) sum[i][s[i]-'0']++;
    }
    while(m--){
        int l, r;
        cin >> l >> r;
        cout << max(sum[r-1][1]-sum[l-1][1], sum[r-1][0]-sum[l-1][0]) + 1 << '\n';
    }
}

Codeforces Global Round 20 A-F,H
http://www.lxtyin.ac.cn/2022/04/23/题解/Codeforces Global Round 20 A-F1,H/
作者
lx_tyin
发布于
2022年4月23日
许可协议