洛谷 P1971 兔兔与蛋蛋游戏 题解

前置知识

  • 二分图博弈

二分图博弈模板题链接

思路

首先这题有个并不显然的性质:我们发现,同一个格子不能经过两次。

简单证明:

如果我们走回这个格子,那么路径显然成一个环。我们把这个环拿出来,因为是网格图上的环,我们容易发现其周长必然是偶数。

如果现在格子里填的是黑棋子,那么经过偶数步我们当前必然只能走白格子,反之亦然(自己画图理解一下)。

有了这个性质,这题就成了二分图博弈模板题:有两类点,每次只能从一类点向另一类点走,无法移动的人失败。

二分图博弈经典结论:起点是二分图最大匹配必经点则先手必胜,否则先手必败。

题目告诉我们:兔兔犯错误当且仅当这次操作过后,必胜方由兔兔变成了蛋蛋。

于是我们只要判断所有操作过后的局面是先手必胜还是先手必败即可。如果兔兔操作前是先手必胜局面,操作后还是先手必胜局面(此时先手是蛋蛋),则兔兔犯错误。

判断一个局面是否先手必胜,也即判断当前点是否是二分图最大匹配必经点,首先要把之前操作过的点删掉(不能走第二遍),记录当前最大匹配,然后删掉当前点,看最大匹配是否减小即可。

听起来太麻烦了?我们有一个小 trick!

对于这种删掉点的,我们通常考虑把时间轴倒过来,让时间倒流。这样就把删点的操作变成了加点。

于是我们把操作的点按时间倒序加入。每次加入一个点后如果最大匹配变大,那么这个点是必经点,当前局面是先手必胜局面。

为什么加入点后最大匹配变大就是必经点?考虑反证法,如果不是,说明有一个最大匹配不经过该点,那么没有该点的最大匹配应该等于有该点的最大匹配,与加入点后最大匹配变大矛盾。

求最大匹配用 dinic 和匈牙利算法都可以,我这里用的是 dinic,参考代码较长,懒得写的可以自行换成匈牙利。

参考代码

#include <queue>
#include <vector>
#include <cstring>
#include <climits>
#include <iostream>
#include <algorithm>

using i64 = long long;

const int N = 1e4 + 10, M = 1e5 + 10;
int gox[] = {1, -1, 0, 0};
int goy[] = {0, 0, 1, -1};

bool vis[50][50], win[N];
char G[50][50];
int n, m, K, S = N - 1, T = N - 2;
int idx = 1, head[N], nex[M], to[M], fl[M];
std::vector< std::pair<int, int> > del	;

void addEdge(int u, int v, int f)
{
	nex[++idx] = head[u];
	head[u] = idx;
	to[idx] = v, fl[idx] = f;
}
void addFlow(int u, int v, int f)
{
	addEdge(u, v, f);
	addEdge(v, u, 0);
}

namespace dinic
{
	int now[N], d[N];

	bool BFS(void)
	{
		std::queue<int> q;
		std::memset(d, 0, sizeof d);
		std::memcpy(now, head, sizeof head);
		d[S] = 1; q.emplace(S);
		while (q.size()) {
			int u = q.front(); q.pop();
			for (int e = head[u]; e; e = nex[e])
				if (fl[e] && !d[to[e]]) {
					d[to[e]] = d[u] + 1;
					if (to[e] == T) return 1;
					q.emplace(to[e]);
				}
		}
		return 0;
	}
	int dfs(int u, int flow)
	{
		if (u == T)
			return flow;
		int rest = flow;
		for (int e = now[u]; rest && e; e = nex[e])
			if (fl[e] && d[to[e]] == d[u] + 1) {
				now[u] = e;
				int tmp = dfs(to[e], std::min(rest, fl[e]));
				if (!tmp) d[to[e]] = 0;
				rest -= tmp;
				fl[e] -= tmp;
				fl[e ^ 1] += tmp;
			}
		return flow - rest;
	}
	int main(void)
	{
		int res = 0;
		while (BFS())
			res += dfs(S, INT_MAX);
		return res;
	}
}

inline int getId(int x, int y)
{
	return (x - 1) * m + y;
}
int main(void)
{
	std::cin >> n >> m;
	// 在最开始把二分图连向源汇点的边连上
	for (int i = 1; i <= n; ++i) {
		std::cin >> G[i] + 1;
		for (int j = 1; j <= m; ++j) {
			if (G[i][j] == '.') {
				addFlow(S, getId(i, j), 1);
				vis[i][j] = 1;
				del.emplace_back(i, j);
			}
			if (G[i][j] == 'X')
				addFlow(S, getId(i, j), 1);
			if (G[i][j] == 'O')
				addFlow(getId(i, j), T, 1);
		}
	}
	std::cin >> K; //读入操作,把这些点删掉
	for (int i = 1, x, y; i <= K << 1; ++i) {
		std::cin >> x >> y;
		vis[x][y] = 1;
		del.emplace_back(x, y);
	}
	// 把没删的点之间的边连上
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= m; ++j)
			if (!vis[i][j] && G[i][j] == 'X')
				for (int k = 0; k < 4; ++k) {
					int xx = i + gox[k];
					int yy = j + goy[k];
					if (xx < 1 || xx > n || yy < 1 || yy > m) continue;
					if (!vis[xx][yy] && G[i][j] != G[xx][yy])
						addFlow(getId(i, j), getId(xx, yy), 1);
				}
	dinic::main(); // 先跑一遍 dinic
	// 按时间倒序把每个点和与它相连的边加入
	for (int i = K << 1; i >= 0; --i) {
		auto t = del[i];
		vis[t.first][t.second] = 0;
		for (int k = 0; k < 4; ++k) {
			int xx = t.first + gox[k];
			int yy = t.second + goy[k];
			if (xx < 1 || xx > n || yy < 1 || yy > m) continue;
			if (!vis[xx][yy] && G[t.first][t.second] != G[xx][yy]) {
				if (G[xx][yy] == 'X' || G[xx][yy] == '.')
					addFlow(getId(xx, yy), getId(t.first, t.second), 1);
				else
					addFlow(getId(t.first, t.second), getId(xx, yy), 1);
			}
		}
		win[i] = dinic::main(); //在残量网络上跑 dinic,如果有流量说明该点为必经点。
	}
	int res = 0;
	for (int i = 0; i <= K << 1; i += 2)
		if (win[i] && win[i + 1]) //当前先手必胜并且操作完依然先手必胜,就是犯了错误
			++res; //先统计答案数量
	std::cout << res << '\n';
	for (int i = 0; i <= K << 1; i += 2)
		if (win[i] && win[i + 1]) //输出答案
			std::cout << (i >> 1) + 1 << '\n';
	return 0;
}
posted @ 2022-05-03 10:44  LroseC  阅读(119)  评论(0)    收藏  举报