zoukankan      html  css  js  c++  java
  • 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
    ", A > B ? A > C ? B * C : A * B : B > C ? A * C : A * B);
    	return 0;
    }
    

    B - Colorful Slimes

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

    可以发现所有的变换颜色操作是可以一起做的,也就是说,假设这个 ((i - j) mod 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
    ", 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
    ", B[i] + 1); puts("");
    	for (int i = 1; i <= N; ++i) printf("%s
    ", 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
    ", 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
    ", f[N - px][py - 1][M - py]);
    	return 0;
    }
    

    F - Namori

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    也没法直接解出被扣掉的边上应该被操作几次了。不过我们可以假设操作了 (k) 次(正表示 (a o 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
    ", 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
    ", 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
    ", Ans + (k < 0 ? -k : k));
    		}
    	}
    	return 0;
    }
    
  • 相关阅读:
    STL 源代码剖析 算法 stl_numeric.h -- copy
    SQLServer 复制和数据库镜像 具体配置部署
    2016第23周三
    2016第23周二
    2016第23周一
    2016第22周日
    2016第22周六
    2016第22周五
    五个JS经典面试题
    2016第22周三
  • 原文地址:https://www.cnblogs.com/PinkRabbit/p/AGC004.html
Copyright © 2011-2022 走看看