简介
本文包括 IOI 2020 集训队作业第 7 到 12 题的题解。
- 「AGC 022E」 Median Replace
- 「AGC 022F」 Checkers
- 「AGC 023D」 Go Home
- 「AGC 023F」 01 on Tree
- 「AGC 024D」 Isomorphism Freak
- 「AGC 024E」 Sequence Growing Hard
Median Replace
题目描述
对于一个 01 串,每次选三个连续的数字删除后插入它们的中位数,如果存在一种方案操作多次后变成只有 1 的串,那么称这个串是好的。给定长度为 $n$ 的串($n$ 是奇数),其中的一些字符是问号,问有多少种将问号替换为 0, 1 的方案,使得得到的 01 串是好的。
数据范围:$n \le 3 \times 10^5$。
思路分析
考虑建出自动机:
其中 1 和 11 为终止态。我们直接在上面 dp 算出走 $n$ 步到终止态的方案数即可,时间复杂度 $O(n)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | const int maxn = 3e5, mod = 1e9 + 7; |
5 | const int ch[10][2] = {{5, 1}, {3, 2}, {2, 2}, {4, 1}, {3, 3}, {7, 6}, {5, 1}, {5, 5}}; |
6 | int n, dp[maxn + 3][10]; |
7 | char s[maxn + 3]; |
8 | |
9 | void upd(int &x, int y) { |
10 | x += y, x < mod ? 0 : x -= mod; |
11 | } |
12 | |
13 | int main() { |
14 | scanf("%s", s + 1); |
15 | n = strlen(s + 1); |
16 | dp[0][0] = 1; |
17 | for (int i = 1; i <= n; i++) { |
18 | if (s[i] != '1') { |
19 | for (int j = 0; j < 8; j++) { |
20 | upd(dp[i][ch[j][0]], dp[i - 1][j]); |
21 | } |
22 | } |
23 | if (s[i] != '0') { |
24 | for (int j = 0; j < 8; j++) { |
25 | upd(dp[i][ch[j][1]], dp[i - 1][j]); |
26 | } |
27 | } |
28 | } |
29 | printf("%d\n", (dp[n][1] + dp[n][2]) % mod); |
30 | return 0; |
31 | } |
Checkers
题目描述
有 $n$ 个棋子在数轴上,第 $i$ 个的 x 坐标为 $X^i$($X$ 为一个足够大的数)。进行 $n - 1$ 次操作,每次选择两个棋子 $A, B$,让 $A$ 的 x 坐标变为 $A$ 关于 $B$ 的对称点,然后删除 $B$。最后会留下一个棋子,问它有多少种可能的坐标。
数据范围:$n \le 50$。
思路分析
我们将 $A$ 跳过 $B$ 的操作看作树上 $A$ 成为了 $B$ 的父亲,这样操作序列就可以看成一棵树。不难发现深度为 $d$ 的点对答案的贡献为 $\pm 2^d$,我们只要考虑它的正负号。考虑最终操作树上的某个点,在它的某个儿子合并到它的时候,以及它的所有祖先在它之后加入儿子的时候,它的符号都会被取反。实际上,我们不用考虑所有祖先,只用考虑自己的父亲,因为祖先可以通过父亲做前缀和来得到。
对于某一个父亲,如果它有 $k$ 个儿子,那么就有 $\lfloor\frac{k}{2}\rfloor$ 个儿子之后加入的兄弟数量是奇数。我们考虑一层一层地 dp:$dp(i, j)$ 表示用了 $i$ 个点,在最后一层中有 $j$ 个点的儿子数应该是奇数(预支了 $j$ 个点有奇数个儿子)的方案数。考虑枚举下一层有 $k$ 个点,那么就有 $\frac{k - j}{2}$ 个点的父亲在它以后加入了奇数个点。注意这时我们还没有考虑下一层的儿子结点。
接下来我们枚举实际上有 $x$ 个结点的贡献是负的(考虑儿子)。那么,如果 $x \ge \frac{k - j}{2}$,我们就让要 $x - \frac{k - j}{2}$ 个原本贡献是正的点取反;否则 $x < \frac{k - j}{2}$,我们就要让 $\frac{k - j}{2}$ 个原本贡献是负的点取反。而取反的方法就是给该点预支奇数个儿子,所以我们会从 $dp(i, j)$ 转移到 $dp(i + k, \vert x - \frac{k - j}{2} \vert)$,不同的正负贡献的值的数量是 $C_{n - i}^k \times C_k^x$。直接 dp 即可,时间复杂度 $O(n^4)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | const int maxn = 50, mod = 1e9 + 7; |
5 | int n, C[maxn + 3][maxn + 3], dp[maxn + 3][maxn + 3]; |
6 | |
7 | int _abs(int x) { |
8 | return x < 0 ? -x : x; |
9 | } |
10 | |
11 | int main() { |
12 | scanf("%d", &n); |
13 | for (int i = 0; i <= n; i++) { |
14 | C[i][0] = 1; |
15 | for (int j = 1; j <= i; j++) { |
16 | C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % mod; |
17 | } |
18 | } |
19 | dp[1][0] = dp[1][1] = n; |
20 | for (int i = 1; i < n; i++) { |
21 | for (int j = 0; j <= i; j++) { |
22 | for (int k = j == 0 ? 2 : j; k <= n - i; k += 2) { |
23 | int x = (k - j) >> 1; |
24 | for (int l = 0; l <= k; l++) { |
25 | dp[i + k][_abs(l - x)] = (dp[i + k][_abs(l - x)] + 1ll * dp[i][j] * C[n - i][k] % mod * C[k][l]) % mod; |
26 | } |
27 | } |
28 | } |
29 | } |
30 | printf("%d\n", dp[n][0]); |
31 | return 0; |
32 | } |
Go Home
题目描述
有 $n$ 个公寓,第 $i$ 个坐标为 $X_i$,人数为 $P_i$。公司在 $S$,下班后所有人都乘坐大巴回家。大巴一到家门口人就会下车,每个时刻大巴上的人会投票(向左或向右),然后大巴往票数多的一方开一格。每个人都要最小化回自己家的时间,问最后一个人回家的时间。
数据范围:$n \le 10^5$。
思路分析
首先,如果 $S$ 在第一栋楼左边或者第 $n$ 栋楼右边,它肯定是顺序经过所有楼。否则,我们看它是先经过 $1$ 号楼还是 $n$ 号楼。如果 $P_1 \ge P_n$,在车到达第 $n - 1$ 栋楼时,想向右的人就只有 $n$ 号楼里的人了,其他人都想向左,而 $P_1 \ge P_n$,所以车肯定会向左开,一直开到 $1$。也就是说,$P_1 \ge P_n$ 时,车会先到达 $1$ 号楼后到达 $n$ 号楼。这时,$n$ 号楼的人就会先让车向左,因为这样可以缩短他们回家的时间。这样,我们就可以将 $P_1 \gets P_1 + P_n$,并删掉 $n$ 号楼。$P_1 < P_n$ 时同理,这样我们就可以把问题转化成规模更小的问题,所以直接递归即可。时间复杂度 $O(n)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | typedef long long ll; |
5 | const int maxn = 1e5; |
6 | int n, s, x[maxn + 3]; |
7 | ll p[maxn + 3]; |
8 | |
9 | int _abs(int x) { |
10 | return x < 0 ? -x : x; |
11 | } |
12 | |
13 | ll solve(int l, int r, int t) { |
14 | if (l > r) { |
15 | return _abs(s - t); |
16 | } |
17 | ll ret = 0; |
18 | if (x[l] > s || (x[r] > s && p[l] >= p[r])) { |
19 | p[l] += p[r]; |
20 | ret = solve(l, r - 1, x[r]); |
21 | if (t != -1) ret += _abs(x[r] - t); |
22 | } else { |
23 | p[r] += p[l]; |
24 | ret = solve(l + 1, r, x[l]); |
25 | if (t != -1) ret += _abs(x[l] - t); |
26 | } |
27 | return ret; |
28 | } |
29 | |
30 | int main() { |
31 | scanf("%d %d", &n, &s); |
32 | for (int i = 1; i <= n; i++) { |
33 | scanf("%d %lld", &x[i], &p[i]); |
34 | } |
35 | printf("%lld\n", solve(1, n, -1)); |
36 | return 0; |
37 | } |
01 on Tree
题目描述
给定 $n$ 个点的树,第 $i$ 个点上的数是 $v_i \in {0, 1}$。每个父亲向儿子连一条边,求点的一个拓扑序,使得构成的 01 序列逆序对数最小。
数据范围:$n \le 2 \times 10^5$。
思路分析
典型的一种树上贪心套路。假设一开始每个点都是一个独立的集合,每次我们选择一个最优的集合将它与它的父亲合并,并同时计算对答案的贡献。在这题中,最优的集合就是 $\frac{C_0}{C_1}$ 最大的集合,其中 $C_k$ 表示 $k$ 的个数。证明过程是考虑某个点的两个儿子交换合并顺序后答案会不会变得更优,这里省略。用堆加并查集维护即可,时间复杂度 $O(n \log n)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | typedef long long ll; |
5 | const int maxn = 2e5; |
6 | int n, p[maxn + 3], v[maxn + 3], fa[maxn + 3]; |
7 | bool vis[maxn + 3]; |
8 | ll ans; |
9 | |
10 | struct thing { |
11 | int x, y, u, f; |
12 | friend bool operator > (const thing &a, const thing &b) { |
13 | return 1ll * a.x * b.y < 1ll * b.x * a.y; |
14 | } |
15 | } cur[maxn + 3]; |
16 | |
17 | priority_queue<thing, vector<thing>, greater<thing> > H; |
18 | |
19 | int find(int x) { |
20 | return fa[x] == x ? x : fa[x] = find(fa[x]); |
21 | } |
22 | |
23 | int main() { |
24 | scanf("%d", &n); |
25 | for (int i = 2; i <= n; i++) { |
26 | scanf("%d", &p[i]); |
27 | } |
28 | for (int i = 1; i <= n; i++) { |
29 | scanf("%d", &v[i]); |
30 | } |
31 | for (int i = 1; i <= n; i++) { |
32 | fa[i] = i; |
33 | cur[i].u = i, cur[i].f = p[i]; |
34 | cur[i].x = !v[i], cur[i].y = v[i]; |
35 | H.push(cur[i]); |
36 | } |
37 | while (!H.empty()) { |
38 | thing x = H.top(); |
39 | H.pop(); |
40 | if (!x.f || vis[x.u]) { |
41 | continue; |
42 | } |
43 | vis[x.u] = true; |
44 | int u = x.u, v = find(x.f); |
45 | ans += 1ll * cur[v].y * cur[u].x; |
46 | cur[v].x += cur[u].x; |
47 | cur[v].y += cur[u].y; |
48 | fa[u] = v; |
49 | H.push(cur[v]); |
50 | } |
51 | printf("%lld\n", ans); |
52 | return 0; |
53 | } |
Isomorphism Freak
题目描述
定义一棵点有颜色的树是好的当且仅当对于任意两个颜色相同的结点,这棵树以它们为根形成的两棵有根树同构。给定 $n$ 个点的树,要加上若干个点,并给点染上颜色,使得树是好的。问最小的颜色数量,以及在颜色数量最小的情况下树上叶子结点个数的最小值。
数据范围:$n \le 100$。
思路分析
发现第一问答案为 $\lceil \frac{d}{2} \rceil$,其中 $d$ 表示直径长度(点数)。第二问分类讨论:如果 $d$ 是偶数,那么直径中心所在的边分出的两个子树中,每个深度相同的点儿子数量都要相同。如果 $d$ 是奇数,那么直径中心是点,会分出多棵子树,其中的每个深度相同的点儿子数量也要相同;或者我们也有可能在直径下面再加一个点,转化为 $d$ 是偶数的情况。由于 $n$ 很小,直接暴力即可,时间复杂度 $O(n^3)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | typedef long long ll; |
5 | const int maxn = 100; |
6 | const ll inf = 5e16; |
7 | int n, deg[maxn + 3], son[maxn + 3], dep[maxn + 3], mx[maxn + 3]; |
8 | vector<int> G[maxn + 3]; |
9 | pair<int, ll> ans, res; |
10 | |
11 | void dfs(int u, int pa = 0) { |
12 | son[u] = 0; |
13 | for (int i = 0, v; i < G[u].size(); i++) { |
14 | v = G[u][i]; |
15 | if (v == pa) continue; |
16 | dep[v] = dep[u] + 1; |
17 | son[u]++; |
18 | dfs(v, u); |
19 | } |
20 | } |
21 | |
22 | pair<int, ll> calc() { |
23 | int x = 0; |
24 | for (int i = 1; i <= n; i++) { |
25 | x = max(x, dep[i]); |
26 | } |
27 | for (int i = 1; i <= x; i++) { |
28 | mx[i] = 0; |
29 | } |
30 | int c = 0; |
31 | for (int i = 1; i <= n; i++) { |
32 | mx[dep[i]] = max(mx[dep[i]], son[i]); |
33 | if (dep[i] == 1) c++; |
34 | } |
35 | ll y = c; |
36 | for (int i = 1; i < x; i++) { |
37 | y = min(inf, y * mx[i]); |
38 | } |
39 | return make_pair(x, y); |
40 | } |
41 | |
42 | pair<int, ll> solve_1(int u) { |
43 | dep[u] = 1; |
44 | dfs(u); |
45 | return calc(); |
46 | } |
47 | |
48 | pair<int, ll> solve_2(int u, int v) { |
49 | dep[u] = dep[v] = 1; |
50 | dfs(u, v), dfs(v, u); |
51 | return calc(); |
52 | } |
53 | |
54 | void search(int u, int pa = 0) { |
55 | for (int i = 0, v; i < G[u].size(); i++) { |
56 | v = G[u][i]; |
57 | if (v == pa) continue; |
58 | res = min(res, solve_2(u, v)); |
59 | search(v, u); |
60 | } |
61 | } |
62 | |
63 | pair<int, ll> solve() { |
64 | res.first = maxn + 3, res.second = 0; |
65 | for (int i = 1; i <= n; i++) { |
66 | res = min(res, solve_1(i)); |
67 | } |
68 | search(1); |
69 | return res; |
70 | } |
71 | |
72 | int main() { |
73 | scanf("%d", &n); |
74 | for (int i = 1, u, v; i < n; i++) { |
75 | scanf("%d %d", &u, &v); |
76 | G[u].push_back(v), G[v].push_back(u); |
77 | deg[u]++, deg[v]++; |
78 | } |
79 | ans = solve(); |
80 | for (int i = 1; i <= n; i++) if (deg[i] == 1) { |
81 | G[i].push_back(++n); |
82 | ans = min(ans, solve()); |
83 | G[i].pop_back(), n--; |
84 | } |
85 | printf("%d %lld\n", ans.first, ans.second); |
86 | return 0; |
87 | } |
Sequence Growing Hard
「AGC 024E」Sequence Growing Hard
题目描述
求这样的 $(A_0, A_1, \cdots, A_n)$ 组数:
- $A_i$ 是长度为 $i$ 的序列,每个元素都是 $[1, k]$ 中的整数。
- $A_{i + 1}$ 是 $A_i$ 插入一个元素得到的。
- $A_{i + 1}$ 的字典序大于 $A_i$。
数据范围:$n, k \le 300$。
思路分析
考虑插入什么数会使字典序变大。假设我们要在数 $y$ 之前插入 $x$,那么要么 $x > y$,要么 $x = y$ 并且 $x$ 比 $y$ 后面第一个不等于 $y$ 的数大。考虑第二种情况得到的序列,其实和在第一个不等于 $y$ 的数之前插入 $x$ 没有区别,所以问题转化成了只有第一种情况。对于序列中的每个数,可以看作一个二元组 $(p_i, v_i)$ 分别表示它是第几次插入的,以及它的权值。在 $A_0$ 中我们建立一个虚拟的二元组 $(0, 0)$,这样之后每一次的插入我们都可以看作在某个二元组 $(x, y)$ 之前插入 $(i, v_i)$,满足 $y > v_i$。我们将在某个二元组之前插入二元组看作将新的二元组作为之前二元组的一个孩子,这样问题就转化成了一个有根树计数问题,每个结点是一个二元组,并满足:
- 根结点的二元组是 $(0, 0)$。
- 二元组的第一维形成排列,第二维的数在 $[0, k]$ 中。
- 二元组的父亲的两维小于儿子的两维。
设 $f(i, j)$ 表示 $i$ 个点的树,根结点的第二维是 $j$ 的方案数。我们考虑将 $i$ 个节点的第一维根据相对顺序用 $0, 1, \cdots, i - 1$ 来编号,观察到 $1$ 的父亲肯定是 $0$,所以我们枚举 $1$ 号结点子树的大小 $s$ 以及第二维的值 $l$,就可以列出转移方程:
答案就是 $f(n + 1, 0)$。前缀和优化即可,时间复杂度 $O(n^3)$。
代码实现
1 |
|
2 | using namespace std; |
3 | |
4 | const int maxn = 300; |
5 | int n, m, mod, C[maxn + 3][maxn + 3], dp[maxn + 3][maxn + 3], f[maxn + 3][maxn + 3]; |
6 | |
7 | int func(int x) { |
8 | return x < mod ? x : x - mod; |
9 | } |
10 | |
11 | void prework(int n) { |
12 | for (int i = 0; i <= n; i++) { |
13 | C[i][0] = 1; |
14 | } |
15 | for (int i = 1; i <= n; i++) { |
16 | for (int j = 1; j <= i; j++) { |
17 | C[i][j] = func(C[i - 1][j - 1] + C[i - 1][j]); |
18 | } |
19 | } |
20 | } |
21 | |
22 | int main() { |
23 | scanf("%d %d %d", &n, &m, &mod); |
24 | prework(n); |
25 | for (int i = 0; i <= m; i++) { |
26 | dp[1][i] = 1; |
27 | } |
28 | for (int i = m; ~i; i--) { |
29 | f[1][i] = func(f[1][i + 1] + 1); |
30 | } |
31 | for (int i = 2; i <= n + 1; i++) { |
32 | for (int j = 0; j <= m; j++) { |
33 | for (int k = 1; k < i; k++) { |
34 | dp[i][j] = (dp[i][j] + 1ll * f[k][j + 1] * dp[i - k][j] % mod * C[i - 2][k - 1]) % mod; |
35 | } |
36 | } |
37 | for (int j = m; ~j; j--) { |
38 | f[i][j] = func(f[i][j + 1] + dp[i][j]); |
39 | } |
40 | } |
41 | printf("%d\n", dp[n + 1][0]); |
42 | return 0; |
43 | } |