231005 做题记录 // 构造

发布时间 2023-11-15 14:49:24作者: XSC062

刚刚看一言看到了神秘的歇后语,符合当下心理状态。

大本钟下送快递——上面摆,下面寄。


A - Errich-Tac-Toe (Hard Version)

https://vjudge.net/contest/585791#problem/A

如果我们将地图按国际象棋式斜向黑白染色分组,规定黑组要么黑色要么无色,白组要么白色要么无色,那么这样是一定不会三连击的。

容易发现无色格子永远不会被更改,而在方式 A 中被更改的格子在方式 B 中一定不会被更改;相应地,在方式 A 中不被更改的格子在方式 B 中一定会被更改,故两种染色方式更改的格子数总和就是一开始非无色的格子数。所以根据抽屉原理,一定能找到一种染色方式,代价 \(\le \dfrac k2\)

但是我们要找到的,是代价 \(\le \dfrac k3\) 的方案呀?我们观察到我们上面的分组方式,直接让相邻两个不一样了,连二连击都做不到;所以我们要使我们的染色方式更廉价。

我们仍然斜向染色,但是分为三组:

这样,因为刚才叙述过的原因,一定能找到一种染色方法,代价 \(\le \dfrac k3\)

枚举三种方式,取代价最小的一种即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 305;
char a[maxn][maxn];
int col[maxn][maxn];
int T, n, res, id, typ;
int min(int x, int y) {
	return x < y ? x : y;
}
int func(int x, int y) {
	int res = 0;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1; j <= n; ++j) {
			if (col[i][j] == y)
				res += (a[i][j] == 'X');
			else if (col[i][j] != x)
				res += (a[i][j] == 'O');
		}
	}
	return res;
}
int main() {
	scanf("%d", &T);
	while (T--) {
		scanf("%d", &n);
		int cnt = 0;
		for (int i = 1; i <= n; ++i)
			scanf("%s", a[i] + 1);
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j)
				cnt += (a[i][j] != '.');
			for (int j = 1; j <= i; ++j)
				col[j][i - j + 1]
						= (i - 1) % 3 + 1;
		}
		for (int i = 2; i <= n; ++i) {
			for (int j = 1; j <= n - i + 1; ++j)
				col[j + i - 1][n - j + 1]
						= (n + i - 2) % 3 + 1;
		}
		res = func(1, 2), id = 1, typ = 2;
		if (func(1, 3) < res)
			res = func(1, 3), id = 1, typ = 3;
		if (func(2, 1) < res)
			res = func(2, 1), id = 2, typ = 1;
		if (func(2, 3) < res)
			res = func(2, 3), id = 2, typ = 3;
		if (func(3, 1) < res)
			res = func(3, 1), id = 3, typ = 1;
		if (func(3, 2) < res)
			res = func(3, 2), id = 3, typ = 2;
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j) {
				if (col[i][j] == typ) {
					if (a[i][j] == 'X')
						a[i][j] = 'O';
				}
				else if (col[i][j] != id) {
					if (a[i][j] == 'O')
						a[i][j] = 'X';
				}
			}
		}
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j)
				putchar(a[i][j]);
			putchar('\n');
		}
	}
	return 0;
}
} // namespace XSC062

抽屉原理是构造中经常用到的手段,后面我们也会遇到运用了抽屉原理的更多题目。


B - Mine Sweeper II

https://vjudge.net/contest/585791#problem/B

观察到答案必须 \(\le \dfrac {n\times m}2\),根据在上一道题目得到的经验,考虑找到两种地位相等、并完全相反的方案。

我们知道,把 B 变成 A 一定可以满足条件;从「完全相反」出发,考虑把所有雷变成空地、所有空地变成雷。

将空地上的数字视为由空地向周围八格的雷连边,可以连到的边的数量。将地图完全翻转后,边除了起点和终点翻转之外,没有任何变化。所以,数字之和不变。

由此我们就得到了分两组的方案,由抽屉原理,必有一组方案的代价 \(\le \dfrac {n\times m}2\),取较小者即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 1e3 + 5;
int n, m, res1, res2;
char a[maxn][maxn], b[maxn][maxn];
int main() {
	scanf("%d %d", &n, &m);
	for (int i = 1; i <= n; ++i)
		scanf("%s", a[i] + 1);
	for (int i = 1; i <= n; ++i) {
		scanf("%s", b[i] + 1);
		for (int j = 1; j <= m; ++j) {
			res1 += (a[i][j] != b[i][j]);
			res2 += (a[i][j] == b[i][j]);
		}
	}
	if (res1 < res2) {
		for (int i = 1; i <= n; ++i)
			puts(a[i] + 1);
	}
	else {
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= m; ++j) {
				if (a[i][j] == '.')
					putchar('X');
				else putchar('.');
			}
			putchar('\n');
		}
	}
	return 0;
}
} // namespace XSC062

C - Ehab's Last Corollary

https://vjudge.net/contest/585791#problem/C

我们尝试证明出题人的猜想……

  • 如果整个图上没有环,就整个一棵树

    树呢,是一个二分图对不对,然后抽屉原理,我们看两部节点中比较大的那一坨,它的大小一定是 \(\ge \dfrac n2\)\(\ge \dfrac k2\) 的,直接输出就好了。

  • 否则,对于一个环,如果它的点数小于等于 \(k\),那我们直接将它作为第二个问题的答案输出。

  • 否则,我们在图的最小环上隔一个点选一个点,一定能选出 \(\left\lfloor\dfrac k2\right\rfloor\) 个相互独立的点。你说为什么它们之间没有直接连边呢?因为我们是在一个比 \(k\) 大的最小环上选的,要是它们之间有直接连边,那就会又构成一个更小的环了。

    所以我们只需要找到一个最小的环然后按上述操作得到答案…… 然而找最小环这一点本身不太现实……

    这个时候我们怎么办呢?

    我们用一点神奇科技。考虑图的 DFS 树。

    • 如果有返祖边 \((v, u)\),且深度 \(d_v-d_u< k\),那么 \(u\to v\) 在树上的简单路径和返祖边 \((v, u)\) 共同构成一个长度不超过 \(k\) 的环,直接输出。

    • 否则,因为 \(d_v-d_u\ge k\),有 \(d_v\ge k\),且对于任意 \(d_y-d_x<k\),返祖边 \((y,x)\) 不存在。

      我们仍然考虑上面提到的隔一个取一个的方法。从任意 \(d_v\ge k\) 开始取点,分别取 \(v\)\(0\) 代父辈(即自身),\(2\) 代父辈(即爷爷),\(4\) 代父辈……

      为什么这么取就不会出 bug 呢?因为我们上面提到的「对于任意 \(d_y-d_x<k\),返祖边 \((y,x)\) 不存在」,所以不会有杂边干扰。

时间复杂度 \(\mathcal O(n + m)\)。只能说真是妙啊。jly 赛高!

namespace XSC062 {
using namespace fastIO;
const int maxn = 1e5 + 5;
bool vis[maxn];
int n, m, k, x, y;
int f[maxn], dep[maxn];
std::vector<int> g[maxn];
int col[maxn], cnt[5];
void color(int x, int fa, int now) {
	col[x] = now, ++cnt[now];
	for (auto i : g[x]) {
		if (i == fa)
			continue;
		color(i, x, 3 - now);
	}
	return;
}
void DFS(int x, int fa) {
	vis[x] = 1;
	for (auto i : g[x]) {
		if (i == fa)
			continue;
		if (vis[i]) {
			if (dep[i] < dep[x] &&
				dep[x] - dep[i] < k) {
				print(2, '\n');
				int p = x, cnt = 1;
				while (p != i)
					++cnt, p = f[p];
				print(cnt, '\n'), p = x;
				while (p != i)
					print(p, ' '), p = f[p];
				print(i, '\n');
				exit(0);
			}
			continue;
		}
		f[i] = x;
		dep[i] = dep[x] + 1;
		DFS(i, x);
	}
	return;
}
void add(int x, int y) {
	g[x].push_back(y);
	return;
}
int main() {
	read(n), read(m), read(k);
	for (int i = 1; i <= m; ++i) {
		read(x), read(y);
		add(x, y), add(y, x);
	}
	if (m == n - 1) {
		print(1, '\n');
		color(1, -1, 1);
		k = (k + 1) / 2;
		x = cnt[1] > cnt[2] ? 1 : 2;
		for (int i = 1; i <= n && k; ++i) {
			if (col[i] == x)
				print(i, ' '), --k;
		}
		putchar('\n');
		return 0;
	}
	dep[1] = 1, DFS(1, -1);
	for (int i = 1; i <= n; ++i) {
		if (dep[i] >= k) {
			print(1, '\n');
			x = i, k = (k + 1) / 2;
			while (k--) {
				print(x, ' ');
				x = f[f[x]];
			}
			putchar('\n');
			break;
		}
	}
	return 0;
}
} // namespace XSC062

D - 景点划分

https://vjudge.net/contest/585791#problem/D

(暴论)总有一天我会死于鼻炎!!!

(打喷嚏)(擤鼻涕)(试图嗑药然后发现自己水杯里的是隔夜水)(再打喷嚏)

? 前言

你打开了题目。你想,不就是从图里抠两个连通块出来吗,这也能进 IOI?

你开始打代码。你突然发现不对劲。你抠掉了一个大小为 \(a\) 的连通块,然后发现剩下的部分裂成了很多个块,其中根本找不到一个大小 \(\ge b\) 的块。

你发现,事情没有这么简单。

这是你吗?不,这不是你,这是我,小丑 lym ?

不妨设 \(a\le b\le c\),则由抽屉原理,\(a\le \dfrac n3\)

我们从最特殊的情况开始思考。假如图是树,那么答案怎么求呢?

对于任意一条边,在其左右两边的连通块中,根据抽屉原理,较大者的大小必定 \(\ge \dfrac{n}{2}\),根据重心的定义,重心必然属于较大连通块。


E - Baggage

https://vjudge.net/contest/585791#problem/E


F - Strange Housing

https://vjudge.net/contest/585791#problem/F

是难得一见的小清新题目(经历过前两题的洗礼之后)。

我们从原图中抽一个生成树出来,比如 DFS 树。

然后我们又知道树是连通二分图,所以我们按照二分图来染色就可以了。

但这么做有个 bug,就是树里有返祖边,这就可能会导致二分图的一个部分里出现在原图中相连的点。

所以我们可以换一种思考方式,把二分图的染色方法带到原图里。

一个点当且仅当周围有染色点或自身为染色点时是可达的。

对于一个点,我们先检查其周围一圈有没有染色点;如果有就不能染色。在 DFS 遍历的时候直接染色即可。

namespace XSC062 {
using namespace fastIO;
const int maxn = 3e5 + 5;
int col[maxn];
bool vis[maxn];
int T, n, m, x, y, cnt;
std::vector<int> g[maxn];
void DFS(int x) {
	vis[x] = 1;
	for (auto i : g[x]) {
		if (col[i] == 1) {
			col[x] = 0;
			break;
		}
	}
	if (col[x] == -1)
		++cnt, col[x] = 1;
	for (auto i : g[x]) {
		if (vis[i])
			continue;
		if (col[x] == 1)
			col[i] = 0;
		DFS(i);
	}
	return;
}
void add(int x, int y) {
	g[x].push_back(y);
	return;
}
int main() {
	read(T);
	while (T--) {
		read(n), read(m);
		for (int i = 1; i <= n; ++i) {
			g[i].clear();
			g[i].shrink_to_fit();
			vis[i] = 0, col[i] = -1;
		}
		while (m--) {
			read(x), read(y);
			add(x, y), add(y, x);
		}
		cnt = 0, DFS(1);
		for (int i = 2; i <= n; ++i) {
			if (!vis[i]) {
				puts("NO");
				goto noSol;
			}
		}
		puts("YES");
		print(cnt, '\n');
		for (int i = 1; i <= n; ++i) {
			if (col[i] == 1)
				print(i, ' ');
		}
		putchar('\n');
		noSol: ;
	}
	return 0;
}
} // namespace XSC062