AtCoder Grand Contest 017
题目传送门:AtCoder Grand Contest 017。
A - Biscuits
如果所有的袋子中的饼干数目都是偶数,则如果 \(P = 0\) 答案为 \(2^N\) 否则答案为 \(0\)。
否则答案为 \(2^{N - 1}\)。
#include <cstdio>
int N, P, C;
int main() {
scanf("%d%d", &N, &P);
for (int i = 1, x; i <= N; ++i) scanf("%d", &x), C += x & 1;
if (!C) printf("%lld\n", P ? 0ll : 1ll << N);
else printf("%lld\n", 1ll << (N - 1));
return 0;
}
B - Moderate Differences
存在 \(\mathcal O (1)\) 解法,但因为这里 \(N\) 不大,可以枚举有多少对相邻的数是递增的,然后判断 \(B\) 是否在满足的区间内。
#include <cstdio>
typedef long long LL;
int N, A, B;
LL C, D;
int main() {
scanf("%d%d%d%lld%lld", &N, &A, &B, &C, &D), --N;
for (int k = 0; k <= N; ++k)
if (A + k * C - (N - k) * D <= B && B <= A + k * D - (N - k) * C) return puts("YES"), 0;
puts("NO");
return 0;
}
C - Snuke and Spells
一个很精妙的性质:
- 考虑数轴上的 \(N\) 个坐标 \(1 \sim N\)。每个坐标上都有绳子,一开始长度均为 \(0\)。
- 对于每个颜色 \(A_i = k\) 的 \(i\) 号球,在坐标 \(k\) 上多挂 \(1\) 单位长度的绳子。
- 最后把每个坐标上的绳子向左(负方向)拉直。
- 如果绳子覆盖了 \([0, N]\),则一定可以通过施法让所有球消失。
- 而如果不能让所有球消失,在 \([0, N]\) 中未被覆盖的总长度,就是需要修改颜色的球数,也就是答案。
此结论可以感性理解。
由此我们先开桶统计,然后做后缀和,然后对于每个询问我们可以在桶里查询,修改每个 \([i - 1, i]\) 被覆盖的次数。
每个询问是 \(\mathcal O (1)\) 的,可以做到 \(\mathcal O (N + Q)\) 的复杂度。
#include <cstdio>
const int MN = 200005;
int N, Q, A[MN], C[MN], _S[MN * 2], *S = _S + MN, Ans;
int main() {
scanf("%d%d", &N, &Q);
for (int i = 1; i <= N; ++i) scanf("%d", &A[i]), ++C[A[i]];
for (int i = 1; i <= N; ++i) ++S[i], --S[i - C[i]];
for (int i = N; i >= -N; --i) S[i] += S[i + 1], Ans += i > 0 && !S[i];
while (Q--) {
int p, x;
scanf("%d%d", &p, &x);
--C[A[p]];
if (!--S[A[p] - C[A[p]]]) if (A[p] - C[A[p]] > 0) ++Ans;
if (!S[x - C[x]]++) if (x - C[x] > 0) --Ans;
++C[x];
A[p] = x;
printf("%d\n", Ans);
}
return 0;
}
D - Game on Tree
我们考虑根只有一个孩子的情况:显然 Alice 把这条边断掉即可赢得游戏。
如果有两个孩子呢:那就是谁先把其中一条边断掉谁就输掉游戏,然后就可以看成两个子树的子问题。
如果两棵子树的 SG 值相同,则 Alice 输掉游戏,Bob 赢得游戏。
如果有三个孩子呢?我的思路到这里就卡住了。
最后我是观察了如果三棵子树都是往下挂的链的情况:这完全等价于 Nim 游戏。
而且对于一般的三子树情况或者更多子树的情况我完全没有思路。
根据 Nim 游戏的结论,我只能猜测整棵树的 SG 值应该等于每棵子树的 SG 值加上 \(\boldsymbol{1}\) 后的异或和。
对着样例验证发现没错,交上去 AC 了。那么这个结论要如何证明呢?
我们注意到,如果有 \(k\) 棵子树,那么我们可以把根节点复制 \(k\) 份,每个根节点只下接一棵子树。
这样就分成了独立的 \(k\) 个游戏,而每个游戏中,根节点只有一棵子树。显然原树的 SG 值等于这些子游戏的 SG 值的异或和。
但是对于根节点只有一个孩子的游戏,它的 SG 值又如何求出呢——已经无法分解成更小的游戏了。
但我们可以证明这样一个结论:对于根节点只有一个孩子的游戏,其 SG 值为其子树的 SG 值加上 \(1\)。
我们可以这样证明:如果直接断开了根与其子树相连的边,则下一状态的 SG 值为 \(0\)。
否则,也就是断开了子树内的边,此时仍然满足根节点只有一个孩子,而且问题规模更小。
结合数学归纳法,我们可以证明原树可以转移到每个子树能转移到的状态的 SG 值加 \(1\) 的状态,从而证明此结论。
所以只要对这棵树做一次 DFS 即可,某棵树的 SG 值为其所有子树的 SG 值加 \(1\) 后的异或和。
#include <cstdio>
#include <vector>
const int MN = 100005;
int N;
std::vector<int> G[MN];
int DFS(int u, int p) {
int ret = 0;
for (int v : G[u]) if (v != p) ret ^= DFS(v, u) + 1;
return ret;
}
int main() {
scanf("%d", &N);
for (int i = 1, x, y; i < N; ++i)
scanf("%d%d", &x, &y),
G[x].push_back(y),
G[y].push_back(x);
puts(DFS(1, 0) ? "Alice" : "Bob");
return 0;
}
E - Jigsaw
注意到,如果 \(C_i \ne 0\),则 \(A_i\) 毫无意义,同理如果 \(D_i \ne 0\),则 \(B_i\) 也是毫无意义。
我们可以把每个拼图抽象成一个数对:\((l_i, r_i)\)。需要满足:
对于两块拼图 \((l_i, r_i)\) 和 \((l_j, r_j)\),想让 \(i\) 拼图能拼在 \(j\) 拼图的左边,当且仅当 \(r_i = l_j\) 的时候才成立。
把所有 \(l, r\) 看成图中的节点,对于每块拼图,我们可以从 \(l_i\) 向 \(r_i\) 连一条边。
则题目即是要求:把这张图分成若干条路径,每条边被恰好经过一次,且路径的起点和终点节点有特殊要求(要贴合桌沿)。
实际上就只有两类点,必须从左侧点出发到达右侧点(但是这张图不是二分图)。
要如何判断是否可行呢?我的想法是:反正已知需要花费的路径数目(通过度数确定),那么跑一下最大费用最大流就行。
这样好像确实是可行的,毕竟 \(H\) 才 \(200\),而且图中都是单位边权单位费用,也许有些玄学性质保证复杂度。
不过题解给出了更优秀的判定方法:
-
首先要保证每个左侧点的出度不比入度少,右侧点入度不比出度少。
-
对于每个弱连通分量,其中左侧点中至少要有一个能作为起点的点:也就是出度比入度多。
只要满足这两个条件就一定可以给出方案了,不难证明是正确的(欧拉回路那套理论)。
#include <cstdio>
#include <vector>
const int MH = 405;
int N, H, deg[MH];
std::vector<int> G[MH];
int ok, vis[MH];
void DFS(int u) {
vis[u] = 1;
if (deg[u]) ok = 1;
for (int v : G[u]) if (!vis[v]) DFS(v);
}
int main() {
scanf("%d%d", &N, &H);
for (int i = 1; i <= N; ++i) {
int a, b, c, d, x, y;
scanf("%d%d%d%d", &a, &b, &c, &d);
if (!c) x = a;
else x = c + H;
if (!d) y = b + H;
else y = d;
++deg[x], --deg[y];
G[x].push_back(y), G[y].push_back(x);
}
for (int i = 1; i <= H; ++i)
if (deg[i] < 0 || deg[i + H] > 0) return puts("NO"), 0;
for (int i = 1; i <= 2 * H; ++i) if (!G[i].empty() && !vis[i]) {
ok = 0, DFS(i);
if (!ok) return puts("NO"), 0;
}
puts("YES");
return 0;
}
F - Zigzag
我们考虑从左到右,依次确定每条折线的形态。
如果直接状压 DP,每次转移一条线,可以做到 \(\mathcal O (4^N \operatorname{poly}(N))\)。
哎,你有没有想到,当你做在 \(M\) 行 \(N\) 列的网格上摆棋子的那种题目时,也就是传统状压 DP 题:
一行一行转移,也是复杂度较劣的。那样虽然阶段数是 \(\mathcal O (M)\),但是状态数和每个状态的转移数都是 \(\mathcal O (2^N)\) 的。
但是我们有「轮廓线 DP」啊!也就是每次只在一行往前推一格,这样虽然阶段数是 \(\mathcal O (M N)\) 的,但是转移数是 \(\mathcal O (1)\) 的。
也就做到了 \(\mathcal O (2^N M N)\) 的复杂度。
其实这个思想也能用到这题上,如果你也先按照折线从左到右,再按照每条折线从上到下依次确定:
因为上一条折线中,比当前位置往起点的部分其实是不需要了,所以可以不记那些状态(参考轮廓线 DP 时上一层状态)。
此时阶段数为 \(\mathcal O (M N)\),但是还需记一个状态,也就是上一条折线在此时的高度时的水平坐标,这是 \(\mathcal O (N)\) 的。
而转移是 \(\mathcal O (1)\) 的,综合来看总复杂度是 \(\mathcal O (2^N M N^2)\) 的。还是过不去。
我们需要最后一个优化:把记录上一条折线在此时的高度时的水平坐标这一个信息去掉。
注意如果在此时的高度时,上一条折线比这条折线的水平位置严格靠左,那么这个点其实是毫无意义的。
实际上所有这条折线永远都走不到的地方,都是无意义的。
也就是说上一条折线,太靠左了的话,有一部分信息是没必要的。
那么我们把上一条折线右侧的区域,和这条折线未来可能走到的区域,取个交,也无妨。
这样也就是说上一条折线是直接从当前位置出发的了,而当前位置可以直接由这条折线之前的路径确定。
这样就成功去掉一个 \(\mathcal O (N)\) 的状态了,转移仍然是 \(\mathcal O (1)\) 的。
仅有上一条折线往左,而这条折线想往右时,需要把上一条折线多余的部分抹掉,变成从当前点出发的样子。
时间复杂度为 \(\mathcal O (2^N M N)\)。
#include <cstdio>
const int Mod = 1000000007;
const int MN = 20, MM = 20;
inline void Add(int &x, int y) { x -= Mod - y; x += x >> 31 & Mod; }
int N, M, K, t[MM][MN];
int f[2][1 << MN];
int main() {
scanf("%d%d%d", &N, &M, &K), --N;
for (int i = 0; i < M; ++i)
for (int j = 0; j < N; ++j)
t[i][j] = -1;
for (int i = 1; i <= K; ++i) {
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
t[--a][--b] = c;
}
int o = 0;
f[o][0] = 1;
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
o ^= 1;
for (int S = 0; S < 1 << N; ++S) f[o][S] = 0;
for (int S = 0; S < 1 << N; ++S) if (f[o ^ 1][S]) {
int v = f[o ^ 1][S];
if (t[i][j] != 1) {
if (~S >> j & 1) Add(f[o][S], v);
}
if (t[i][j] != 0) {
if (S >> j & 1) Add(f[o][S], v);
else {
int T = S >> j;
if (T) T &= T - 1;
T = (T + 1) << j | (S & ((1 << j) - 1));
Add(f[o][T], v);
}
}
}
}
}
int Ans = 0;
for (int S = 0; S < 1 << N; ++S) Add(Ans, f[o][S]);
printf("%d\n", Ans);
return 0;
}