2023 CCPC 深圳

2023 CCPC 深圳

D. Bot Brothers

有一棵 \(n\) 个点的树,\(m\) 个叶子,编号为 \(1∼m\)。两人在树上博弈,均从根出发,轮流行动,每次走向一个当前所在节点的子节点,如果在叶子就不移动。最终如果两人所在叶子编号一个是另一个 \(+1\)(\(\bmod m\) 意义下),则 \(+1\) 的一方获胜。\(m < n \leq 10^5\)。

Sol:博弈,哈希

首先后手是一定不败的,因为后手可以始终跟着先手,最终达到相同的叶子节点。

那么现在考虑什么样的情况下后手必胜。记 \(S_x\) 代表 \(x\) 子树内叶子节点的集合,\(T_x\) 代表 \(x\) 子树内叶子节点 \(+1\) 的集合。假设先手在 \(x\),后手在 \(y\),且 \(dep[x] = dep[y]\),其中 \(dep[x]\) 代表 \(x\) 的深度。

如果 \(T_x \nsubseteq S_y\),则先手一定可以一直向不存在于 \(S_y\) 中的叶子节点移动,因为 \(y\) 也只能往下移动到 \(y'\),并且 \(S_y' \subseteq S_y\),所以后手永远无法移动到获胜的叶子节点处。

所以后手的移动要始终保证 \(T_x \subseteq S_y\),也就是说,当先手从 \(x\) 移动到其儿子时,对于 \(x\) 的每个儿子 \(x'\) 一定存在唯一的 \(y\) 的儿子 \(y'\),使得 \(T_{x'} \subseteq S_{y'}\)。根据该性质,定义 \(dfs(x, y, fx, fy)\) 代表先手在 \(x\),后手在 \(y\),且 \(x\) 的父亲为 \(fx\),\(y\) 的父亲为 \(fy\) 时,后手是否必胜。枚举所有合法的 \(x'\) 和 \(y'\) 递归解决即可。因为如果后手必胜,每个 \(x'\) 一定存在唯一的 \(y\) 的儿子 \(y'\),所以只有 \(O(m)\) 种结果,所以复杂度为 \(O(m \log n)\),其中的 \(\log n\) 是通过 map 来确定与 \(x'\) 对应的 \(y'\)。注意当某个人已经到叶子节点的时候,直接退出即可。至于为什么 \(x'\) 一定存在唯一的 \(y\) 的儿子 \(y'\),下面的方法可以回答。

但是根据该性质,存在更优秀的做法,观察发现后手必胜时树的结构十分特殊,即相同深度的点的叶子节点集合 \(S\) 形如 \(\{a, a + x, a + 2x\dots\}\),且 \(|S| = \frac{m}{sz}\),其中 \(sz\) 为该深度节点个数。即,将相同深度的点按照 \(a\) 排序后,一定有 \(\forall i< sz,T_i = S_{i + 1}\),通过哈希 check 即可。需要注意的是,如果某个叶子节点的父亲只有 \(1\) 个儿子,我们需要删掉该父亲,直到父亲存在 \(2\) 个儿子以上为止。时间复杂度 \(O(n)\)。

cpp 复制代码
// O(nlogn)
#include <bits/stdc++.h>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define SZ(x) ((int)(x).size())
#define pb push_back
#define endl '\n'
using ll = long long;
using ull = unsigned long long;
const int N = 100010;
mt19937_64 rnd(time(0));

int n, m, dep[N];
ull hs[N], S[N], T[N];
vector<int> adj[N];
bool valid;

inline void dfs(int u, int par) {
    dep[u] = dep[par] + 1;
    int c = 0;
    S[u] = T[u] = 0;
    for (auto v : adj[u]) if (v != par) {
        c += 1;
        dfs(v, u);
        S[u] += S[v];
        T[u] += T[v];
    }
    if (c == 0) {
        S[u] = hs[u];
        T[u] = hs[u % m + 1];
    }
}

inline void dfs(int x, int y, int fx, int fy) {
    if (x <= m || y <= m) return;
    map<ull, int> mp;
    for (auto v : adj[x]) if (v != fx) {
        mp[T[v]] = v;
    }
    for (auto v : adj[y]) if (v != fy) {
        if (!mp.count(S[v])) {
            valid = false;
            return;
        }
        dfs(mp[S[v]], v, x, y);
        if (!valid) return;
    }
}

void solve() {
    cin >> n >> m;
    valid = true;
    for (int i = 1; i <= n; ++i) hs[i] = rnd();
    for (int i = 1; i <= n; ++i) {
        adj[i].clear();
    }
    for (int i = 1; i < n; ++i) {
        int u, v; cin >> u >> v;
        adj[u].pb(v);
        adj[v].pb(u);
    }
    dfs(n, 0);
    dfs(n, n, 0, 0);
    if (valid) cout << "Doddle\n";
    else cout << "Tie\n";
}
signed main(void) {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int Test = 1;
    cin >> Test;
    while (Test--) solve();
    return 0;
}
cpp 复制代码
// O(n)
#include <bits/stdc++.h>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define SZ(x) ((int)(x).size())
#define pb push_back
#define endl '\n'
using ll = long long;
using ull = unsigned long long;
const int N = 100010;
mt19937_64 rnd(time(0));

int n, m, dep[N];
bool valid[N];
ull hs[N];
vector<int> adj[N];
vector<int> node[N]; // node[i] 代表深度为 i 的所有节点
struct Info {
    int id; // 子树内最小的叶子节点编号
    int sz; // 子树内叶子节点个数
    ull hs1; // 子树内叶子节点编号的哈希和
    ull hs2; // 子树内叶子 (节点编号 + 1) 的哈希和
} val[N];

inline void dfs(int u, int par) {
    dep[u] = dep[par] + 1;
    int c = 0;
    val[u].id = n + 1;
    val[u].hs1 = val[u].hs2 = 0;
    val[u].sz = 0;
    for (auto v : adj[u]) if (v != par) {
        c += 1;
        dfs(v, u);
        val[u].id = min(val[u].id, val[v].id);
        val[u].hs1 += val[v].hs1;
        val[u].hs2 += val[v].hs2;
        val[u].sz += val[v].sz;
    }
    if (c == 0) {
        val[u].id = u;
        val[u].hs1 = hs[u];
        int v = u + 1;
        if (v > m) v = 1;
        val[u].hs2 = hs[v];
        val[u].sz = 1;
    } else if (c == 1 && val[u].sz == 1) {
        valid[u] = false;
    }
}

inline void dfs1(int u, int par) {
    if (!valid[u]) {
        node[dep[u]].pb(val[u].id);
        return;
    }
    node[dep[u]].pb(u);
    for (auto v : adj[u]) if (v != par) {
        dfs1(v, u);
    }
}

void solve() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++i) hs[i] = rnd();
    for (int i = 1; i <= n; ++i) {
        adj[i].clear();
        node[i].clear();
        valid[i] = true;
    }
    for (int i = 1; i < n; ++i) {
        int u, v; cin >> u >> v;
        adj[u].pb(v);
        adj[v].pb(u);
    }
    dfs(n, 0);
    dfs1(n, 0);
    for (int i = 2; i <= n; ++i) {
        if (node[i].size() == 0) break;
        sort(all(node[i]), [&](auto a, auto b) {
            return val[a].id < val[b].id;
        });
        int x = node[i].size();
        for (int j = 0; j < x - 1; ++j) {
            int u = node[i][j], v = node[i][j + 1];
            if (val[u].hs2 != val[v].hs1) {
                cout << "Tie\n";
                return;
            }
        }
    }
    cout << "Doddle\n";
}
signed main(void) {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int Test = 1;
    cin >> Test;
    while (Test--) solve();
    return 0;
}

E. Two in One

给定长度为 \(n\) 的序列 \(a\),找到一个区间以及在该区间中选择两种颜色,使得两种颜色的出现次数的二进制或最大。\(n \leq 10^5, a_i \leq n\)。

Sol:贪心,结论

不妨考虑在 \([1, n]\) 中选择两种颜色,若选择的两种颜色的出现次数分别为 \(x, y\),存在以下 \(2\) 种情况:

  • 若 \(\text{highbit}(x) = \text{highbit}(y)\),其中 \(\text{highbit}(x)\) 代表 \(x\) 在二进制下最高的 \(1\) 所在位,记 \(t = \text{highbit}(x)\)。

    不难证明,在区间 \([1, n]\) 不断缩减的过程中(要么 \(x\) 减少 \(1\),要么 \(y\) 减少 \(1\)),一定存在某个时刻,使得在当前区间中某一个颜色的出现次数的 \(\text{highbit}\) 仍为 \(t\),而另一个颜色的出现次数恰好为 \(2^t - 1\)。此时两种颜色的出现次数的二进制或最大,等于 \(2^{t + 1} - 1\)。

    贪心的,只需要选择 \([1, n]\) 中出现次数最多、次多的两种颜色即可。

  • 若 \(\text{highbit}(x) \neq \text{highbit}(y)\),记 \(t = \text{highbit}(x\ \&\ y)\)。

    不难证明,在区间 \([1, n]\) 不断缩减的过程中(要么 \(x\) 减少 \(1\),要么 \(y\) 减少 \(1\)),一定存在某个时刻,使得在当前区间中某一个颜色的出现次数仍 \(\geq 2^t\),而另一个颜色的出现次数恰好为 \(2^t - 1\),并且 \(x, y\) 高于 \(t\) 位的二进制不会改变。此时两种颜色的出现次数的二进制或最大,等于 \(x \mid y \mid 2^{\text{highbit}(x\ \&\ y)} - 1\)。

    贪心的,只需要选择 \([1, n]\) 中出现次数最多、次多的两种颜色即可。

综上,假设 \([1, n]\) 中出现次数最多、次多的颜色出现次数分别为 \(x, y\),则答案一定为 \(x \mid y \mid 2^{\text{highbit}(x\ \&\ y)} - 1\),注意特判 \(\text{highbit}(x\ \&\ y) = 0\) 的情况。

时间复杂度:\(O(n)\)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define all(x) (x).begin(), (x).end()
#define SZ(x) ((int)(x).size())
#define int long long
#define pb push_back
#define endl '\n'
using ll = long long;
const ll mod = 1000000007;
const ll INF = 1ll << 60;
const int inf = 1 << 30;
const int N = 200010;

void solve() {
    int n; cin >> n;
    vector<int> c(n + 1);
    int mx = 0, sc = 0;
    for (int i = 1; i <= n; ++i) {
        int x; cin >> x;
        c[x] += 1;
    }
    sort(c.begin() + 1, c.begin() + n + 1, greater<>());
    int x = c[1], y = c[2];
    if (__lg(x) == __lg(y)) {
        cout << (1 << __lg(x) + 1) - 1 << endl;
    } else {
        cout << (x | y | ((x & y) ? (1 << __lg(x & y)) - 1 : 0)) << endl;
    }
}
signed main(void) {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int Test = 1;
    cin >> Test;
    while (Test--) solve();
    return 0;
}

K. Quad Kingdoms Chess

给定两个序列 \(a, b\),根据以下规则,判断最后结果是否平局:

双方只从序列开头拿数字,假设序列 \(a, b\) 首元素分别为 \(x, y\),则:

  • 若 \(x > y\),则 \(x\) 被淘汰;
  • 若 \(x < y\),则 \(y\) 被淘汰;
  • 若 \(x = y\),\(x, y\) 同时被淘汰。

重复该过程,直到存在一方序列为空。

如果双方序列最终都为空,则平局,否则仍有剩余元素的一方获胜。

存在 \(q\) 次询问,每次单点修改后,询问最后结果是否平局。

\(|a|, |b|,q \leq 10^5, a_i, b_i \leq 10^5\)。

Sol:线段树,哈希

显然我们只关心最大值,并且如果平局,则两个序列最大值必须相同,记 \(a\) 中最大值第一次出现位置为 \(x\),\(b\) 中为 \(y\),则 \(a[1, x]\) 和 \(b[1, y]\) 最后一定会平局,我们不必关心淘汰的过程,因为 \(a_x\) 和 \(b_y\) 都是最大值且相同,所以结果是固定的。然后我们继续找 \(a[x + 1, |a|]\) 和 \(b[y + 1, |b|]\) 中的最大值,判断是否相同,然后重复该过程即可判断 \(a[1, |a|]\) 和 \(b[1, |b|]\) 是否平局。

不难发现,我们寻找的位置一定是后缀最大值的位置,即令 \(sufMax[i]\) 代表序列 \(a[i, n]\) 中的最大值,则我们关心的位置一定是 \(a_i = sufMax_i\) 的位置。

并且最终平局的话,\(a_{|a|},b_{|b|}\) 一定是必选的,观察后发现选择的位置一定是以序列末尾为起点的单调不减的子序列,记作 \(T\) 。例如 \(\{2, 3, 5, 4, 1,2, 1\}\),则选择的位置构成的子序列为 \(\{5, 4, 2, 1\}\),从右往左看单调不减。那么若最终平局,当且仅当 \(T_a = T_b\) 成立。

因为存在单点修改,我们只需要通过线段树分别维护子序列 \(T_a,T_b\) 的异或哈希即可,具体的:

以序列 \(a\) 为例,假设线段树上节点 \(i\) 所代表区间为 \(a[l, r]\),则该节点维护的信息为以 \(a[r]\) 为起点的 \(a[l, r]\) 中单调不减的子序列的哈希值,即 \(T_{a[l, r]}\) 所代表的哈希值,记作 \(shs[i]\);同时维护区间最大值 \(mx\)。

考虑左右儿子如何合并,即如何合并 \(T_{a[l, mid]}\) 和 \(T_{a[mid + 1,r]}\) 成 \(T_{a[l, r]}\),定义 \(ls\) 代表节点 \(i\) 的左儿子,\(rs\) 代表节点 \(i\) 的右儿子,则有:

  • 如果 \(mx[ls] < mx[rs]\),则 \(T_{a[l, r]} = T_{a[mid + 1, r]}\),即 \(shs[i] = shs[rs], mx[i] = mx[rs]\)。

  • 如果 \(mx[ls] \geq mx[rs]\),则 \(T_{a[l, r]}\) 中仍有部分元素在 \(T_{a[l, r]}\) 中,需要找到 \(a[l, mid]\) 中从右数起第一个 \(\geq mx[rs]\) 的位置 \(x\),则 \(T_{a[l, r]} = T_{a[l, x]} + T_{a[mid + 1, r]}\),即 \(shs[i] = \text{qry}(l, mid, mx[rs]) + shs[rs],mx[i] = mx[ls]\)。

    其中 \(\text{qry}(l, r, k)\) 代表 \(T_{a[l, x]}\) 的哈希值,其中 \(x\) 为 \(a[l, r]\) 中从右数起第一个 \(\geq k\) 的位置,具体的实现类似 P4198 楼房重建 通过线段树上二分实现:

    • 若 \(mx[rs] \geq k\),则返回 \(shs[i] - shs[rs] + \text{qry}(mid + 1, r, k)\)。注意并不是返回 \(shs[ls] + \text{qry}(mid + 1, r, k)\);
    • 若 \(mx[rs] < k\),则返回 \(\text{qry}(l, mid, k)\)。

由于合并的复杂度为 \(O(\log n)\),则总复杂度为 \(O(n\log ^2 n)\)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
using ll = long long;
using ull = unsigned long long;
const int N = 100010;
mt19937_64 rnd(time(0));

int n1, n2, q, a[N], b[N];
ull hs[N];

struct SegmentTree {
    int mx[N << 2];
    ull shs[N << 2]; // sum of hs
    #define lson (id << 1)
    #define rson (id << 1 | 1)
    #define mid ((l + r) >> 1)
    ull qry(int id, int l, int r, int k) {
        if (mx[id] < k) return 0ULL;
        if (l == r) return (mx[id] >= k ? shs[id] : 0ULL);
        if (mx[rson] >= k) return qry(rson, mid + 1, r, k) + shs[id] - shs[rson];
        else return qry(lson, l, mid, k); 
    }
    void up(int id, int l, int r) {
        mx[id] = max(mx[lson], mx[rson]);
        shs[id] = qry(lson, l, mid, mx[rson]) + shs[rson];
    }
    void build(int id, int l, int r, int *a) {
        if (l == r) {
            mx[id] = a[l];
            shs[id] = hs[a[l]];
            return;
        }
        build(lson, l, mid, a), build(rson, mid + 1, r, a);
        up(id, l, r);
    }
    void modify(int id, int l, int r, int x, int v) {
        if (l == r) {
            mx[id] = v;
            shs[id] = hs[v];
            return;
        }
        if (x <= mid) modify(lson, l, mid, x, v);
        else modify(rson, mid + 1, r, x, v);
        up(id, l, r);
    }
    #undef lson
    #undef rson
    #undef mid
} T[2];

void solve() {
    for (int i = 1; i <= 100000; ++i) hs[i] = rnd();
    cin >> n1;
    for (int i = 1; i <= n1; ++i) {
        cin >> a[i];
    }
    cin >> n2;
    for (int i = 1; i <= n2; ++i) {
        cin >> b[i];
    }
    T[0].build(1, 1, n1, a);
    T[1].build(1, 1, n2, b);
    cin >> q;
    while (q--) {
        int op, x, y; cin >> op >> x >> y;
        if (op == 1) {
            T[0].modify(1, 1, n1, x, y);
        } else {
            T[1].modify(1, 1, n2, x, y);
        }
        if (T[0].shs[1] == T[1].shs[1]) {
            cout << "YES\n";
        } else {
            cout << "NO\n";
        }
    }
}
signed main(void) {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int Test = 1;
    // cin >> Test;
    while (Test--) solve();
    return 0;
}

L. Rooted Tree

一棵树,一开始仅有根节点 \(1\),每次操作会均匀随机选择一个叶子节点 \(u\) 并连接 \(M\) 个新节点作为 \(u\) 的儿子。询问操作 \(K\) 次后树上所有节点的深度和的期望值,答案对 \(10^9 + 9\) 取模。钦定 \(dep(1) = 0\)。\(M \leq 100, K \leq 10^7\)。

Sol:期望的线性性,概率,DP

根据期望的线性性,定义 \(T_i\) 代表第 \(i\) 次操作新增的叶子节点的集合,\(T\) 代表操作完 \(K\) 次后的节点集合,即 \(T = \{1\} + T_1 + T_2 + \dots + T_K\),则有:

\[E(\sum_{v\in T} dep(v)) = \sum_{i = 1}^K E(\sum_{v\in T_i}dep(v)) \]

定义 \(ans(i)\) 代表第 \(i\) 次操作新增的叶子节点的深度和的期望,即 \(ans(i) = E(\sum_{v\in T_i}dep(v))\)。

定义 \(cnt(i)\) 代表第 \(i\) 次操作后叶子节点个数,则有 \(cnt(i) = (M - 1) \times i + 1\),\(cnt(0) = 1\).

定义 \(f(i)\) 代表第 \(i\) 次操作后叶子节点的深度和的期望,即 \(f(i) = \sum_{j = 1}^{cnt_i} E(dep(v_j))\),其中 \(v_j\) 是第 \(i\) 次操作后的叶子节点,显然 \(f(0) = 0\)。

\(ans(i)\) 可以根据第 \(i - 1\) 次操作之后的叶子节点的深度得到,即:

\[\begin{align} ans(i) &= \sum_{j = 1}^{cnt(i - 1)}\frac{1}{cnt(i - 1)}(E(dep_{v_j}) + 1) \times M \\ &= M (\frac{f(i - 1)}{cnt(i - 1)} + 1) \end{align} \]

\(f(i)\) 等于 \(f(i - 1)\) 加上新增的 \(ans(i)\),然后再减去减少的叶子节点的深度的期望和,即:

\[\begin{align} f(i) &= f(i - 1) + ans(i) - \sum_{j = 1}^{cnt(i - 1)}\frac{1}{cnt(i - 1)}E(dep(v_j)) \\ &= f(i - 1) + ans(i) - \frac{f(i - 1)}{cnt(i - 1)} \end{align} \]

\(O(K + \log V)\) 预处理所有 \(cnt(i)\) 的逆元即可。

简单的来说令 \(prod[i] = \prod_{j = 1}^i cnt(j), iprod[i] = (\prod_{j = 1}^i cnt(j))^{-1}\)。

先 \(O(K)\) 得到 \(prod\) 后,通过快速幂得到 \(iprod[K]\) 后,根据 \(iprod[i] = iprod[i + 1] \times cnt(i + 1)\),可以 \(O(K)\) 递推出 \(iprod\)。所以 \((cnt(i))^{-1} = prod[i - 1] \times iprod[i]\)。

不难发现 \(M\) 与复杂度没有关系,本质上 \(M \leq 100\) 用来保证 \(cnt(i)\) 在模 \(10^9 + 9\) 意义下存在逆元。

时间复杂度:\(O(K)\)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
#define endl "\n"
using ll = long long;
const int mod = 1e9 + 9;

int add(int a, int b) {
    int c = a + b;
    if (c >= mod) c -= mod;
    return c;
}
int sub(int a, int b) {
    int c = a - b;
    if (c < 0) c += mod;
    return c;
}
int mul(int a, int b) {
    ll c = 1ll * a * b;
    if (c >= mod) c %= mod;
    return c;
}
inline int ksm(int a, int b) {
    int res = 1;
    for (; b; b >>= 1) {
        if (b & 1) {
            res = 1ll * res * a % mod;
        }
        a = 1ll * a * a % mod;
    }
    return res;
}

const int N = 10000010;
int f[N], cnt[N], inv[N], prod[N], iprod[N];

void solve() {
    int M, K; cin >> M >> K;
    cnt[0] = 1;
    for (int i = 1; i <= K; ++i) cnt[i] = cnt[i - 1] + M - 1;
    prod[0] = 1;
    for (int i = 1; i <= K; ++i) prod[i] = mul(prod[i - 1], cnt[i]);
    iprod[K] = ksm(prod[K], mod - 2);
    for (int i = K - 1; i >= 1; --i) iprod[i] = mul(iprod[i + 1], cnt[i + 1]);
    for (int i = 1; i <= K; ++i) inv[i] = mul(prod[i - 1], iprod[i]);
    int ans = 0;
    f[0] = 0;
    for (int i = 1; i <= K; ++i) {
        int res = mul(M, add(mul(f[i - 1], inv[i - 1]), 1));
        ans = add(ans, res);
        f[i] = 0;
        f[i] = add(f[i], res);
        f[i] = add(f[i], f[i - 1]);
        f[i] = sub(f[i], mul(f[i - 1], inv[i - 1]));
    }
    cout << ans << endl;
}
signed main() {
    ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
    int T = 1;
    // cin >> T;
    while (T--) {
        solve();
    }
    return 0;
}