AtCoder Grand Contest 004

题目传送门:AtCoder Grand Contest 004

A - Divide a Cuboid

就是平行地切一刀,有偶数就是 \(0\),否则是较小两数乘积。

#include <cstdio>

int main() {
	long long A, B, C;
	scanf("%lld%lld%lld", &A, &B, &C);
	if (~A & 1 || ~B & 1 || ~C & 1) puts("0");
	else printf("%lld\n", A > B ? A > C ? B * C : A * B : B > C ? A * C : A * B);
	return 0;
}

B - Colorful Slimes

最终考虑某个颜色 \(i\) 的史莱姆,一定是先抓到某个颜色 \(j\) 的史莱姆,然后通过 \((i - j) \bmod N\) 次换颜色操作变成颜色 \(i\) 的。

可以发现所有的变换颜色操作是可以一起做的,也就是说,假设这个 \((i - j) \bmod N\) 的最大值为 \(x\),那就是只要做 \(x\) 次。

枚举 \(x\),然后查询每个颜色以及它往前 \(x\) 个颜色中,抓史莱姆的最小代价即可,时间复杂度为 \(\mathcal O (N^2)\)

#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 2005;

int N, X, A[MN * 2], B[MN * 2];
LL Ans;

int main() {
	scanf("%d%d", &N, &X);
	for (int i = 1; i <= N; ++i) {
		scanf("%d", &A[i]);
		A[N + i] = B[i] = A[i];
		Ans += B[i];
	}
	for (int k = 1; k < N; ++k) {
		LL Sum = (LL)k * X;
		for (int i = 1; i <= N; ++i) {
			B[i] = std::min(B[i], A[N + i - k]);
			Sum += B[i];
		}
		Ans = std::min(Ans, Sum);
	}
	printf("%lld\n", Ans);
	return 0;
}

C - AND Grid

经典构造题,由于边界上不会有紫色的,我们钦定最左边一列全是红色,最右边一列全是蓝色。

然后中间的,有可能有紫色的部分,奇数行全红色,偶数行全蓝色。这样首先保证连通且不重叠

然后如果哪个地方是紫色的,就在那个地方染上缺少的一种颜色即可。

#include <cstdio>

const int MN = 505;

int N, M;
char A[MN][MN], B[MN][MN], C[MN][MN];

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; ++i) {
		scanf("%s", A[i] + 1);
		for (int j = 1; j <= M; ++j) {
			(i & 1 ? B : C)[i][j] = '#';
			(i & 1 ? C : B)[i][j] = '.';
		}
		B[i][1] = C[i][M] = '#';
		B[i][M] = C[i][1] = '.';
		for (int j = 1; j <= M; ++j)
			if (A[i][j] == '#')
				B[i][j] = C[i][j] = '#';
	}
	for (int i = 1; i <= N; ++i) printf("%s\n", B[i] + 1); puts("");
	for (int i = 1; i <= N; ++i) printf("%s\n", C[i] + 1);
	return 0;
}

D - Teleporter

一开始的形状就是,一个连通的基环内向树,\(1\) 在环里。

最终条件就是,\(a_1 = 1\),然后其它点就形成到 \(1\) 的一棵树,深度不超过 \(K\)\(1\) 深度为 \(0\))。

那么反正先把 \(a_1\) 给整成 \(1\),然后就是一棵树的结构,然后考虑最深的点如果 \(K\) 步到不了 \(1\) 那就把他的 \(K - 1\) 阶祖先给接到 \(1\) 上。

容易证明这样的贪心是正确的,时间复杂度为 \(\mathcal O (N)\)(如果对深度用基数排序,代码中直接快排)。

#include <cstdio>
#include <algorithm>
#include <vector>

const int MN = 100005;

int N, K, A[MN], Ans;
std::vector<int> G[MN];

int dep[MN], kpar[MN], per[MN], stk[MN], tp;
void DFS(int u) {
	stk[++tp] = u;
	if (dep[u] > K) kpar[u] = stk[tp - K + 1];
	for (int v : G[u]) dep[v] = dep[u] + 1, DFS(v);
	--tp;
}

int del[MN];
void Del(int u) {
	del[u] = 1;
	for (int v : G[u]) if (!del[v]) Del(v);
}

int main() {
	scanf("%d%d", &N, &K);
	for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
	if (A[1] != 1) A[1] = 1, Ans = 1;
	for (int i = 2; i <= N; ++i) G[A[i]].push_back(i);
	DFS(1);
	for (int i = 1; i <= N; ++i) per[i] = i;
	std::sort(per + 1, per + N + 1, [](int i, int j) { return dep[i] > dep[j]; });
	for (int i = 1; i <= N; ++i) if (kpar[per[i]] && !del[per[i]]) ++Ans, Del(kpar[per[i]]);
	printf("%d\n", Ans);
	return 0;
}

E - Salvage Robots

第一步转化:机器人不动,而是出口和边界在动(想象你的手指捏着这个出口 E 在机器人中游走)。

考虑某个时刻,出口在四个方向上的最大移动范围分别为 \(u, d, l, r\)(上下左右),如图:

好的,此时黄框内的机器人,只要还没掉下去,那就一定可以被出口吸入(这个词怎么怪怪的)。

但是啥时候才会掉下去呢?我们观察下面这张图的红色部分:

此时红色部分内必然是一个机器人都不留下的,有可能是直接掉下去了,有可能是在掉下去之前被出口给吸走了的,总之已经没了。

那么在这个出口移动的 \(u, d, l, r\) 限制下的最大解救机器人的数量,假设我们已经算出来了,可以发现我们可以做 DP 转移:

记这个数量为 \(\mathrm{f}[u][d][l][r]\),不失一般性,假设下一步出口往下移动,应该转移到 \(\mathrm{f}[u][d + 1][l][r]\)

在上图中,此时能多被解救的机器人,就是倒数第三行的第五列到第九列内的机器人,也就是在黄色矩形下面的一排白色区域。

如果是出口往右移动,那就是倒数第三列第四行到第七行。如果往上或者往左,那就是一个机器人也救不了——它们全掉下去了。

DP 转移使用前缀和优化一下,最终答案就是黄色矩形变成整个大矩形时的 DP 值,时间复杂度为 \(\mathcal O (N^4)\),数组可以滚动掉一维。

#include <cstdio>
#include <algorithm>

const int MN = 105;

int N, M, px, py, A[MN][MN], B[MN][MN], C[MN][MN];
char s[MN][MN];

int f[MN][MN][MN];

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; ++i) {
		scanf("%s", s[i] + 1);
		for (int j = 1; j <= M; ++j) {
			A[i][j] = s[i][j] == 'o';
			B[i][j] = B[i][j - 1] + A[i][j];
			C[i][j] = C[i - 1][j] + A[i][j];
			if (s[i][j] == 'E') px = i, py = j;
		}
	}
	for (int u = 0; u <= px - 1; ++u) {
		for (int d = 0; d <= N - px; ++d) {
			for (int l = 0; l <= py - 1; ++l) {
				for (int r = 0; r <= M - py; ++r) {
					int tu = std::max(px - u, 1 + d);
					int td = std::min(px + d, N - u);
					int tl = std::max(py - l, 1 + r);
					int tr = std::min(py + r, M - l);
					if (u && px - u == tu && tl <= tr) f[d][l][r] += B[px - u][tr] - B[px - u][tl - 1];
					if (d) f[d][l][r] = std::max(f[d][l][r], f[d - 1][l][r] + (px + d == td && tl <= tr ? B[px + d][tr] - B[px + d][tl - 1] : 0));
					if (l) f[d][l][r] = std::max(f[d][l][r], f[d][l - 1][r] + (py - l == tl && tu <= td ? C[td][py - l] - C[tu - 1][py - l] : 0));
					if (r) f[d][l][r] = std::max(f[d][l][r], f[d][l][r - 1] + (py + r == tr && tu <= td ? C[td][py + r] - C[tu - 1][py + r] : 0));
				}
			}
		}
	}
	printf("%d\n", f[N - px][py - 1][M - py]);
	return 0;
}

F - Namori

\(N\) 是奇数时直接输出 \(-1\)。下文默认 \(N\) 为偶数。

原图有三种情况,树,基环树(奇环),基环树(偶环)。需要分类讨论:

当原图为一棵树时:树必然是二分图,我们进行黑白染色。

注意到原本奇怪的条件,在相邻节点颜色不同的情况下,就变成了:把「黑色」移动到相邻节点,更容易理解了。

也就是说:在黑色节点上都放上一枚棋子,我们每次可以把一枚棋子移动到相邻的空节点,最终所有棋子要落到白色节点上。

那么黑白节点数量应该是要相等的,如果相等则一定可行,但是需要的步数是多少呢?

给出结论:考虑每条边,如果这条边的某个方向上原棋子个数为 \(x\),但是目标棋子个数为 \(y\),则显然棋子至少经过该边 \(|x - y|\) 次。

步数即是每条边的这个值的总和,这显然是一个下限。至于为何能取到,限于篇幅不证,感兴趣的直接去看官方题解最后一段就行。

当原图为一棵基环树(奇环)时:我们随意扣掉一条边,此时变成树的情况。注意扣掉的边连接的两点的颜色必然相同

那么对这条边进行的操作,就相当于在这两个节点上凭空增加或减少一枚棋子。

注意到变成树后,棋子的数量必须要等于白节点的数量,而原棋子数量等于黑节点数量。

而且在扣掉的边上进行的操作,相当于让棋子数 \(+2\)\(-2\)。所以可以直接解出操作的次数(正表示增加,负表示减少)。

然后神奇的一幕出现了,此时那两个节点上可能有不止一枚棋子,甚至可能有负数枚棋子。但是对于树的结论依然成立。

(注意到那个结论甚至没有提到棋子不能重叠之类的问题)

当原图为一棵基环树(偶环)时:我们随意扣掉一条边,此时变成树的情况。注意扣掉的边连接的两点的颜色必然不同

这个时候仍然是二分图,此时棋子的移动只不过是多了一条边而已。把扣掉的那条边的两端点记作 \(a, b\)

注意到此时就没法改变棋子的数量了,所以黑白节点数量必须相等。

也没法直接解出被扣掉的边上应该被操作几次了。不过我们可以假设操作了 \(k\) 次(正表示 \(a \to b\) 移动,负则反之)。

操作了 \(k\) 次后,还是变成可能有多枚棋子的情况,照样计算。但是此时我们必须把计算的公式拿出来研究研究了。

可以发现每条边的公式都是 \(|c - d \cdot k|\),其中 \(d\) 的值可能为 \(0\)\(1\)\(-1\)

可以发现这只不过就是求一堆 V 字形套了绝对值的一次函数的叠加的最小值。取中位数就好了。

综上所述,三种情况均可以在 \(\mathcal O (N)\) 的时间内完成区分和计算答案(代码中取中位数使用了排序,无伤大雅)。

#include <cstdio>
#include <algorithm>
#include <vector>

typedef long long LL;
const int MN = 100005;

int N, M;
std::vector<int> G[MN];

int a, b, vis[MN], par[MN], dep[MN], num1[MN], num2[MN], kval[MN];
void DFS(int u, int fr) {
	vis[u] = 1;
	dep[u] = dep[par[u] = fr] + 1;
	num1[u] = dep[u] & 1;
	num2[u] = ~dep[u] & 1;
	for (int v : G[u]) if (v != fr) {
		if (!vis[v]) DFS(v, u), num1[u] += num1[v], num2[u] += num2[v];
		else a = u, b = v;
	}
}

int main() {
	scanf("%d%d", &N, &M);
	if (N & 1) return puts("-1"), 0;
	for (int i = 1; i <= M; ++i) {
		int x, y;
		scanf("%d%d", &x, &y);
		G[x].push_back(y);
		G[y].push_back(x);
	}
	DFS(1, 0);
	if (M == N - 1) {
		if (num1[1] != num2[1]) return puts("-1"), 0;
		LL Ans = 0;
		for (int i = 2; i <= N; ++i) {
			int x = num2[i] - num1[i];
			Ans += x < 0 ? -x : x;
		}
		printf("%lld\n", Ans);
	} else {
		if ((dep[a] ^ dep[b]) & 1) {
			if (num1[1] != num2[1]) return puts("-1"), 0;
			for (int x = a; x; x = par[x]) ++kval[x];
			for (int x = b; x; x = par[x]) --kval[x];
			LL Ans = 0;
			static int seq[MN], cnt;
			seq[cnt = 1] = 0;
			for (int i = 2; i <= N; ++i) {
				if (!kval[i]) {
					int x = num2[i] - num1[i];
					Ans += x < 0 ? -x : x;
				} else
					seq[++cnt] = kval[i] * (num2[i] - num1[i]);
			}
			std::sort(seq + 1, seq + cnt + 1);
			int mid = seq[cnt / 2 + 1];
			for (int i = 1; i <= cnt; ++i)
				Ans += seq[i] < mid ? mid - seq[i] : seq[i] - mid;
			printf("%lld\n", Ans);
		} else {
			int k = N / 2 - num1[1];
			for (int x = a; x; x = par[x]) num1[x] += k;
			for (int x = b; x; x = par[x]) num1[x] += k;
			LL Ans = 0;
			for (int i = 2; i <= N; ++i) {
				int x = num2[i] - num1[i];
				Ans += x < 0 ? -x : x;
			}
			printf("%lld\n", Ans + (k < 0 ? -k : k));
		}
	}
	return 0;
}
posted @ 2020-06-13 03:51  粉兔  阅读(57)  评论(0编辑  收藏