CodeForces Round #939(Div. 2) 补题记录(A~F)

A

n 个人里面每一次第 a1 个人走,然后第 a1+1 个人变成了新的第 a1 个人走,第 a1+2 个人又变成了新的第 a1 个人走,以此类推。

因此这 n 个人所有编号 a1 的人全部都会被移走。所以每一次询问的答案就是 min(a11,x)

#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000100;
int a[N];
signed main() {
    int T;
    cin >> T;
    while (T--) {
        int k, q;
        cin >> k >> q;
        for (int i = 1; i <= k; i++)
            cin >> a[i];
        while (q--) {
            int x;
            cin >> x;
            cout << min(x, a[1] - 1) << ' ';
        }
        cout << '\n';
    }
    return 0;
}

B

因为先手和后手的石子的总数量相同,所以:

  • 两张牌都在先手的牌的数量和两张牌都在后手的牌的数量相同。若两张牌都在先手则定义这张牌是 A 的,否则定义是 B 的。
  • 剩下的牌一定一张在先手一张在后手。这样的牌定义为是 C 的。

因此若先手拿了 C 的牌,那么后手一定可以拿走这张牌所对应的那一张牌。因此此时先手并不能获取贡献,而后手可以得到一分。

而对于一张 A 的牌,先手若选取了一张 A 的牌拿走,那么后手因为没有 A 的牌所以无法拿走下一张 A 的牌,此时后手可以拿走任意一张 B 的牌。然后先手拿走另外一张 A 的牌,后手拿走另外一张 B 的牌。此时先手和后手的得分各增加一分。

最后一定只会剩下 C 的牌。这些牌对先手而言不会获得贡献。因此答案为所有不同的 A 的牌的的数量。

开一个桶来记录每一张牌,单组数据的时间复杂度为 O(n)

#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000100;
int a[N], box[N];
signed main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        for (int i = 1; i <= n; i++)
            box[i] = 0;
        for (int i = 1; i <= n; i++) {
            cin >> a[i];
            box[a[i]]++;
        }
        int cnt = 0;
        for (int i = 1; i <= n; i++)
            if (box[i] == 2) cnt++;
        cout << cnt << '\n';
    }
    return 0;
}

C

猜测最后构造的 n×n 矩阵的 (i,j) 位置的数一定满足 ai,j=max(i,j)。问题在于如何构造。

经过多次尝试发现可以每一次先后给第 i 行、列都按照 1,2,3,,n 的顺序染色,其中 i=n,n1,n2,,1

因此直接模拟即可。时间复杂度为 O(n2),染色次数刚好为 2×n,可以通过。

#pragma GCC optimize(3)
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000100;
int a[510][510];
signed main() {
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        int cnt = 0, s = 0;
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= n; j++)
                a[i][j] = max(i, j), s += a[i][j];
        cout << s << ' ' << 2 * n << '\n';
        int idx = n;
        for (int i = 1; i <= n; i++) {
            cout << "1 " << idx << ' ';
            for (int i = 1; i <= n; i++) cout << i << ' '; cout << '\n';
            cout << "2 " << idx << ' ';
            for (int i = 1; i <= n; i++) cout << i << ' '; cout << '\n';
            idx--;
        }
    }
    return 0;
}

D

首先考虑:对于 a 数组的任意一段区间 [l,r],都总有一种办法可以让这些数字全部变成 0

构造:

  • [l,r] 一段区间全部为 0,则已经达成条件。
  • 否则,将所有 x[l,r]N+ax0,都让 [x,x] 这一段区间取 mex

但是第二步最坏需要执行区间的长度次操作,太过于慢。所以考虑优化:

  • [l,r] 中没有 0,则对 [l,r] 整体执行一次 mex 操作,全部变为 0
  • [l,r] 中有 0,则对 [l,r] 整体先执行一遍 mex 操作,此时 [l,r] 区间中所有元素全部非 0,所以转化为第一种情况。

然后发现对于一个全 0 的区间 [l,r],都可以让这一段区间的所有元素全部变为 rl+1

构造方案:

容易发现,若对于一段序列 a,满足 a 中的 n 个元素分别为 0,1,2,,n1,则对整个 a 执行一次 mex 之后,a 中的所有元素变为 n,n,n,,n

所以说,若想要把 [l,r] 一段区间的值全部变为 rl+1,则若当前执行到了前缀 [l,i],则只需要将 [l,i1] 的值变为 1,2,3,,il 然后将 ai 变为 0,对 [l,i] 这个区间整体做 mex 操作即可让 [l,i] 前缀的值全部变为 il+1

更具体的来说,考虑递归。设当前递归的区间是 [l,r]

  • l=r
    • al=0,则一次操作区间 [l,l]al=1
    • al0,则两次操作区间 [l,l]。其中第一次 al=0,第二次即转化为第一种 al=0 的情况。
  • lr
    • 先让 [l,r] 区间全部归 0
    • [l+1,r] 区间递归处理,此时 [l,r] 区间的值为 0,1,2,,rl
    • [l,r] 区间整体做 mex 操作。此时 [l,r] 区间的值是 rl+1,rl+1,rl+1,,rl+1
    • [l,r1] 整体做 mex 操作,此时 [l,r] 区间的值是 0,0,0,,0,rl+1
    • [l,r1] 区间递归处理,此时 [l,r] 区间的值是 0,1,2,,rl+1

因此在最坏操作 2rl+1 级别次的操作数量下让 [l,r] 区间的值变成了 0,1,2,,rl+1

最后整体对 [l,r] 再执行一遍 mex 操作,[l,r] 区间的值全部变为 rl+1,操作执行成功。


现在考虑一种比较简单的方法:对 [0,n) 中的每一个元素 ai,都分别钦定其执行操作 / 不执行操作。设当前所有被钦定执行操作的连续下标区间分别为 [l1,r1][l2,r2][lk,rk],那么对这 k 个区间分别执行上面的操作。考虑证明这样做操作的次数一定不会超过 5×105 次。

首先计算一段区间 [l,r] 执行上面的操作需要花费的最多 mex 覆盖次数。

  • l=r,则最多执行 2 次操作。
  • lr,则:
    • 第一步,让区间 [l,r] 全部归零。操作最多执行 2 次。
    • 第三、四步,让区间 [l,r] 和区间 [l,r1] 分别整体做 mex 操作,操作最多执行 2 次。
  • 现在考虑对区间 [l,r] 的长度 rl+1 从小到大讨论。
    • rl+1=1l=r 最多执行 2 次操作。
    • rl+1=2 则归 0 最多 2 步,此时两次递归处理的时候归 0 操作最多执行 4 次,因此最多执行 6 次操作。
    • rl+1=3 则同理,最多执行 2+4+8=14 次操作。
  • 以此类推。因而当 rl+1=k 即区间 [l,r] 的长度为 k 的时候,最多执行的操作数量是 2k+12 次。
  • 因为 n=18,所以单次区间执行 mex 覆盖的次数最多为 2192=524286>500000 次。但是容易发现这个方法很难使得 mex 覆盖操作执行的次数卡的这么满,所以是可以在 500000 次操作内得到答案的。

现在考虑证明多个区间的最多操作次数一定不会多于单个最大区间覆盖的区间的最多操作次数。

考虑将区间 [l,r] 恰好拆分成 [l,k][k+1,r]。那么 [l,r] 最多需要执行 2rl+1 次操作,[l,k][k+1,r] 分别最多需要执行 2kl+12rk 次操作,合并就是 2kl+1+2rk 次操作。容易证明 2rl+12kl+1+2rk

又因为若将 [l,k] 区间缩水为 [l,k1] 区间,那么 [l,k1] 区间最多执行 2kl 次操作,有 2kl+1>2kl。缩水为 [l+1,k] 区间也同理。

因此就证明了上述命题。因此这样的构造方案不会超出构造次数的限制。


考虑转化原问题。因此原问题的求操作后数列的最大和问题就变为了:

给定长度为 n 的序列 a。现在可以执行若干次操作,每一次操作可以选定一段区间 [l,r] 并将这段区间内所有的元素全部赋值为 rl+1。问最后 a 序列的最大和为多少。

这是一个很简单的 dp 问题。但是发现 n18 所以直接按照上面的简单方法钦定每一个位置是否选择即可。记得要将两个部分分离计算,否则时间复杂度为优秀的 O(4n×n2)

若分离计算,则总的时间复杂度为 O(2n×n)

代码实现:

#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 18;
int a[N], b[N];
void mex(int l, int r) {
    bool box[20] = {false};
    for (int i = l; i <= r; i++)
        if (a[i] <= 18)
            box[a[i]] = true;
    int val = 0;
    while (box[val])
        val++;
    for (int i = l; i <= r; i++)
        a[i] = val;
}
void clear(vector<pair<int, int>> &oper, int l, int r) {
    if (accumulate(a + l, a + r + 1, 0ll) == 0)
        return;
    if (count(a + l, a + r + 1, 0ll) == 0) {
        mex(l, r);
        oper.push_back({l, r});
    } else {
        mex(l, r);
        oper.push_back({l, r});
        mex(l, r);
        oper.push_back({l, r});
    }
}
void fun(vector<pair<int, int>> &oper, int l, int r) {
    if (l == r) {
        clear(oper, l, r);
        a[l] = 1;
        return;
    }
    clear(oper, l, r);
    fun(oper, l + 1, r);
    mex(l, r);
    oper.push_back({l, r});
    mex(l, r - 1);
    oper.push_back({l, r - 1});
    fun(oper, l, r - 1);
}
signed main() {
    int n;
    cin >> n;
    for (int i = 0; i < n; i++)
        cin >> a[i];
    int mx = accumulate(a, a + n, 0ll), id = 0;
    for (int i = 1; i < (1ll << n); i++) {
        for (int j = 0; j < n; j++)
            b[j] = a[j];
        vector<pair<int, int>> seg;
        int l = -1, r = -1;
        for (int j = 0; j < n; j++)
            if (i >> j & 1) {
                if (r == -1)
                    l = r = j;
                else
                    r = j;
            } else {
                if (r != -1)
                    seg.push_back({l, r});
                l = r = -1;
            }
        if (r != -1)
            seg.push_back({l, r});
        for (auto &[l, r] : seg)
            for (int j = l; j <= r; j++)
                b[j] = r - l + 1;
        int now = accumulate(b, b + n, 0ll);
        if (now > mx) {
            mx = now;
            id = i;
        }
    }
    cout << mx << ' ';
    if (!id) {
        cout << "0 ";
    } else {
        vector<pair<int, int>> seg, oper;
        int l = -1, r = -1;
        for (int j = 0; j < n; j++)
            if (id >> j & 1) {
                if (r == -1)
                    l = r = j;
                else
                    r = j;
            } else {
                if (r != -1)
                    seg.push_back({l, r});
                l = r = -1;
            }
        if (r != -1)
            seg.push_back({l, r});
        for (auto &[l, r] : seg) {
            fun(oper, l, r);
            oper.push_back({l, r});
        }
        cout << oper.size() << '\n';
        for (auto &[l, r] : oper)
            cout << l + 1 << ' ' << r + 1 << '\n';
    }
    return 0;
}

E1

定义 NXT(x) 表示 x 号怪物下一个怪物的编号。

首先可以发现若 ax=0,且 aNXT(x)0,那么 NXT(x) 号怪物一定可以存活下来。

证明:ax=0,则对于一次攻击有且只有 x 号怪物会攻击 NXT(x) 号怪物。因为 ax=0,所以 NXT(x) 号怪物一定会在一次攻击后,体力值变为 max(0,aNXT(x)ax)=max(0,aNXT(x)0)=aNXT(x),始终不会变化且 >0

发现 maxai2×105,所以考虑对值域搞事情。令 W=maxai,则猜测所有怪物互相攻击很少的次数之后,答案的更新次数就会迅速的减少。

n>2 且恰好存在一个位置 i 满足 ai=0 且从 NXT(x) 开始恰好有连续的两个位置满足 aj0。则 NXT(x)NXT(NXT(x)) 中,NXT(x) 必然会存活下来,而 NXT(NXT(x)) 必然会被 NXT(x) 所杀死。

然后大胆猜测执行 2×W 次操作之后,一定不会存在一个 x 满足 ax=0aNXT(x)0aNXT(NXT(x))0aNXT(NXT(NXT(x)))0aNXT(NXT(NXT(NXT(x))))=0

证明:若存在一个 x 满足 ax=0aNXT(x)0aNXT(NXT(x))0aNXT(NXT(NXT(x)))0aNXT(NXT(NXT(NXT(x))))=0。那么在执行 2×W 次攻击操作之后,为了让 aNXT(NXT(NXT(x)))>0,所以尽量的让 aNXT(x) 的值小,于是令 aNXT(x)=1。在执行 2×W 次操作之后,aNXT(NXT(x)) 会至少受到 2×W 次攻击,血量减少 2×W。为了让此时的 a(NXT(NXT(x)))>0,且使得她对 aNXT(NXT(NXT(x))) 的伤害尽量的小,所以 aNXT(NXT(x)) 的值应位 2×W+1。此时 aNXT(NXT(NXT(x))) 受到的攻击值至少为 2+3+4++2×W+1,也就是 (2+W+1)×(2×W)2=W×(W+3)。拆开括号得到 W+3×W。因为 W>0 所以 W+3×W>W。也就是说,此时 aNXT(NXT(NXT(x))) 受到的伤害已经超过了 W,所以证伪。记得特判 n=2 的情况,这种情况直接暴力模拟即可。

时间复杂度为 O(n×W),可以通过 E1。

E2

发现 W109,此时使用 E1 的做法会超时。考虑同样的猜测当执行 3×W3 次操作之后,不会存在一个 x 满足 ax=0aNXT(x)0aNXT(NXT(x))0aNXT(NXT(NXT(x)))0aNXT(NXT(NXT(NXT(x))))0。证明同 E1 的证明(稍微复杂一点儿)。

问题在于一个链上若有连续的四只怪物 xyzw 满足 ax=0ay0az0aw0aNXT(w)=0。那么 y 怪物必然存活,因此 z 怪物必然死亡。w 怪物可以幸存当且仅当 z 怪物在存活时的攻击不能够杀死 w。设 z 怪物存活了 c 次攻击,那么 w 怪物所受到的 z 怪物的伤害就是 (azay)+(az2×ay)+(az3×ay)++(azmoday)。当且仅当 aw>(azay)+(az2×ay)+(az3×ay)++(azmoday) 时才可以保证 aw>0 在任何此攻击的时刻始终成立。同样需要特判 n=2n=3 的情况,这两种情况直接暴力模拟即可。

注意:若连续的三个位置是 n,1,2 则最先攻击的是 12,需要特殊处理一下。

时间复杂度为 O(n×W3),大力卡常之后可以通过 E2。

#include <bits/stdc++.h>
// #define int long long
using namespace std;
const int N = 500100;
int a[N], n;
int dist(int l, int r) {
    if (l <= r)
        return r - l + 1;
    else {
        r += n;
        return r - l + 1;
    }
}
int NXT(int x) {
    if (x == n)
        return 1;
    return x + 1;
}
bool check() {
    for (int i = 1; i <= n; i++)
        if (a[i] && a[NXT(i)] && a[NXT(NXT(i))] && a[NXT(NXT(NXT(i)))])
            return true;
    return false;
}
int PRE(int x) {
    if (x == 1)
        return n;
    return x - 1;
}
signed main() {
    ios_base::sync_with_stdio(false), cin.tie(0);
    int T;
    cin >> T;
    while (T--) {
        cin >> n;
        for (int i = 1; i <= n; i++)
            cin >> a[i];
        if (n > 3) {
            int W = *max_element(a + 1, a + n + 1);
            while (check()) {
                for (int j = 1; j <= n; j++)
                    a[j % n + 1] = max(a[j % n + 1] - a[j], 0);
            }
            vector<pair<int, int>> seg;
            for (int i = 1; i <= n; i++)
                if (a[i]) {
                    int p = i;
                    while (a[i]) i = i % n + 1;
                    seg.emplace_back(p, PRE(i));
                    if (p > i - 1)
                        break;
                }
            vector<int> arr;
            for (auto &[l, r] : seg) {
                if (a[PRE(l)])
                    continue;
                if (dist(l, r) <= 2)
                    arr.emplace_back(l);
                else {
                    arr.emplace_back(l);
                    if (l != n) {
                        int dl = a[NXT(l)] % a[l], dr = a[NXT(l)] - a[l];
                        if (!dl)
                            dl += a[l];
                        if (dl > dr)
                            arr.emplace_back(r);
                        else {
                            int dlen = (dr - dl) / a[l] + 1;
                            long long ds = 1ll * (dl + dr) * dlen / 2;
                            if (ds < a[r])
                                arr.emplace_back(r);
                        }
                    } else {
                        int dl = a[NXT(l)] % a[l], dr = a[NXT(l)];
                        if (!dl)
                            dl += a[l];
                        if (dl > dr)
                            arr.emplace_back(r);
                        else {
                            int dlen = (dr - dl) / a[l] + 1;
                            long long ds = 1ll * (dl + dr) * dlen / 2;
                            if (ds < a[r])
                                arr.emplace_back(r);
                        }
                    }
                }
            }
            sort(arr.begin(), arr.end());
            cout << arr.size() << '\n';
            for (auto &x : arr)
                cout << x << ' ';
            cout << '\n';
        } else if (n == 3) {
            while (a[1] && a[2] && a[3]) {
                a[2] = max(0, a[2] - a[1]);
                a[3] = max(0, a[3] - a[2]);
                a[1] = max(0, a[1] - a[3]);
            }
            vector<int> arr;
            if (a[1] && !a[3])
                arr.emplace_back(1);
            if (a[2] && !a[1])
                arr.emplace_back(2);
            if (a[3] && !a[2])
                arr.emplace_back(3);
            cout << arr.size() << '\n';
            for (auto &x : arr)
                cout << x << ' ';
            cout << '\n' ;
        } else {
            // n == 2
            while (a[1] && a[2]) {
                a[2] = max(0, a[2] - a[1]);
                a[1] = max(0, a[1] - a[2]);
            }
            vector<int> arr;
            if (a[1])
                arr.emplace_back(1);
            if (a[2])
                arr.emplace_back(2);
            cout << arr.size() << '\n';
            for (auto &x : arr)
                cout << x << ' ';
            cout << '\n';
        }
    }
}

F

容易发现两个人 ij 之间(i>j)可以互相传球当且仅当 li+ljijri+rj。考虑将这个式子拆分得两个式子。

  • ilij+lj
  • irij+rj

发现判断的式子均为 i±li/ri 的形式,所以将这些值存储来之后排序。

具体的说,对于每一个 i,记录两个二元组 (i+li,0)(ili,1),然后按照第一关键字从小到大,第二关键字从小到大的双关键字排序对这些二元组排序。

然后考虑处理,分类讨论:

  • 当前二元组的第二维是 0,那么就将 i+ri push 进去。
  • 当前二元组的第二维是 1,那么就将 push 中所有第一维的值 iri 的点向 i 连一条双向边。

答案就是新建立的图 G 的连通块数量。

但是建立的边的数量是 O(n2) 级别的,直接做时间复杂度为 O(αn2),不可以通过。

考虑优化。发现 push 集中若存在两个点 ij 满足 ij 两个点在图 G 中在同一个连通块内,那么 i 这个点在 j 这个点进入 push 集的同时就可以删除掉了。这是因为对于 j 之后的每一个连边操作,若这个连边操作可以和 i 连边,那么一定可以和 j 连边。

因此每一次连边的时候,对于图 G 中的每一个连通块都只需要保留 i+ri 的值最大的那一个 i 即可。使用一个 ds 如 STL set 来维护一下她即可。

时间复杂度为 O(αnlogn),可以通过。

#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
const int N = 4000100;
int fa[N], l[N], r[N];
int find(int x) {
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}
struct _ {
    int x, y, id;
} z[N << 1];
bool operator<(const _ &l, const _ &r) {
    return l.x < r.x || l.x == r.x && l.y < r.y;
}
signed main() {
    ios_base::sync_with_stdio(0);
    cin.tie(0);
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        for (int i = 1; i <= n; i++)
            fa[i] = i;
        for (int i = 1; i <= n; i++) {
            cin >> l[i] >> r[i];
            z[i * 2 - 1] = {i + l[i], 0, i};
            z[i * 2] = {i - l[i], 1, i};
        }
        sort(z + 1, z + n + n + 1);
        set<pair<int, int>> se;
        for (int i = 1; i <= 2 * n; i++) {
            if (z[i].y == 0)
                se.insert({z[i].id + r[z[i].id], z[i].id});
            else {
                vector<int> v;
                while (se.size() && (*se.rbegin()).first >= z[i].id - r[z[i].id]) {
                    v.push_back((*se.rbegin()).second);
                    se.erase(prev(se.end()));
                }
                if (v.size()) {
                    for (int j = 1; j < v.size(); j++) {
                        int a = v[j], b = v[j - 1];
                        int ta = find(a), tb = find(b);
                        if (ta != tb) fa[ta] = tb;
                    }
                    int a = z[i].id, b = v[0];
                    int ta = find(a), tb = find(b);
                    if (ta != tb) fa[ta] = tb;
                    se.insert({v[0] + r[v[0]], v[0]});
                }
            }
        }
        int cnt = 0;
        for (int i = 1; i <= n; i++)
            if (find(i) == i)
                cnt++;
        cout << cnt << '\n';
    }
    return 0;
}
posted @   yhbqwq  阅读(163)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全网最简单!3分钟用满血DeepSeek R1开发一款AI智能客服,零代码轻松接入微信、公众号、小程
· .NET 10 首个预览版发布,跨平台开发与性能全面提升
· 《HelloGitHub》第 107 期
· 全程使用 AI 从 0 到 1 写了个小工具
· 从文本到图像:SSE 如何助力 AI 内容实时呈现?(Typescript篇)
点击右上角即可分享
微信分享提示