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\),而且图中都是单位边权单位费用,也许有些玄学性质保证复杂度。

不过题解给出了更优秀的判定方法:

  1. 首先要保证每个左侧点的出度不比入度少,右侧点入度不比出度少。

  2. 对于每个弱连通分量,其中左侧点中至少要有一个能作为起点的点:也就是出度比入度多。

只要满足这两个条件就一定可以给出方案了,不难证明是正确的(欧拉回路那套理论)。

#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;
}
posted @ 2020-08-07 22:05  粉兔  阅读(918)  评论(0编辑  收藏  举报