[学习笔记]一类博弈问题与图的匹配的联系

引入

  • 经典问题:一个无向图,双方轮流选出一个点,一个点最多被选出一次,且每次选出的点必须和上一次对方选出的点相邻

  • 不能动者输

  • 双方都绝顶聪明,求先手是否有必胜策略,以及先手第一次选哪些点能必胜

结论

  • 先手第一次选 \(u\) 必胜当且仅当原图存在一个不包含 \(u\)最大匹配

  • 故先手必胜当且仅当这个图没有完美匹配

证明

  • 考虑归纳

  • 只有一个点显然先手必胜

  • 若原图存在一个最大匹配不包含 \(u\),则随便找一组不包含 \(u\) 的最大匹配

  • 由于是最大匹配,所以对于 \(u\) 相邻的所有点 \(v\) 都满足删掉点 \(u\) 之后,\(v\) 被剩下的图所有的最大匹配包含(否则可以连上 \((u,v)\) 得到更大的匹配)

  • 故后手不管下一次选哪个点都是被所有最大匹配包含的

  • 若原图所有的最大匹配都包含 \(u\),还是随便找一组最大匹配

  • 而删掉这个点 \(u\) 之后,这个图的最大匹配数会减 \(1\),即包含 \(u\) 的匹配边会被删掉,使得原先与 \(u\) 匹配的点被移出匹配点

  • 故后手移到 \(u\) 的匹配点即可

  • Q.E.D

校内模拟题 卡片游戏

Statement

  • \(n\) 种卡片,第 \(i\) 种卡片有 \(q_i\) 个,有一个属性值 \(p_i\)

  • Alice 和 Bob 轮流取卡片

  • 每个人取的卡片的属性值 \(a\) 必须满足:若对方上一次取得卡片属性值为 \(b\),则 \(\frac{\max(a,b)}{\min(a,b)}\) 为质数

  • 求 Alice 先取哪些卡片能必胜

  • 多组数据,数据组数不超过 \(100\)

  • \(1\le n\le 500\)\(1\le p_i\le 5\times10^{10}\)\(1\le q_i\le 10^9\),每组数据内 \(p\) 互不相同

Solution

  • 考虑暴力把游戏图建出来,令 \(cnt_i\) 表示 \(p_i\) 的质因子个数

  • 一条边连接的两端 \(cnt\) 之差绝对值为 \(1\),故这是一个二分图

  • 由于 \(q\) 很大,所以需要把所有 \(q_i\) 个点建成一个,具体地:

  • (1)源向 \(cnt_i\) 为奇数的点连边,容量为 \(q_i\)

  • (2)\(cnt_i\) 为偶数的点向汇连边,容量为 \(q_i\)

  • (3)如果 \(|cnt_i-cnt_j|=1\),并且有 \(p_i|p_j\)\(p_j|p_i\)

  • 注意到每个点度数的上界只有 \(p\) 的质因子个数,所以这张图的边数远不达 \(O(n^2)\),跑网络流的复杂度是可以接受的

  • 这样 Alice 能取第 \(i\) 张卡片当且仅当源连向 \(i\)\(i\) 连向汇的边不一定流满

  • 判断一条边 \(<u,v>\) 是否可以不满的方法:

  • (1)如果原图跑完最大流后这条边不满则直接判掉

  • (2)否则找一条汇到源的增广路,恰好经过 \(<v,u>\) 一次,沿这条路径从汇退给源 \(1\) 的流量,相当于同时把总流量和 \(<u,v>\) 的流量都减了 \(1\)

  • (3)然后若有源到汇的增广路则这条边可以不满,否则这条边一定流满

  • 此外,求 \(cnt_i\) 需要特殊的技巧:先用 \([2,4000]\) 内的质数去筛 \(p_i\)\(p_i\) 被筛完之后得到的数最多只有 \(2\) 个质因子,这时可以使用 Miller-Rabin 素数测试来判定其贡献了多少个质因子,判定 \(\frac{p_i}{p_j}\)\(p_i>p_j\))是否为质数只需判定 \(cnt_i=cnt_j+1\)\(p_j|p_i\) 这两个条件

Code

#include <bits/stdc++.h>

template <class T>
inline void read(T &res)
{
	res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	if (bo) res = ~res + 1;
}

template <class T>
inline T Min(const T &a, const T &b) {return a < b ? a : b;}

typedef long long ll;

const int N = 510, M = 4005, L = 1e6 + 5, INF = 0x3f3f3f3f;
const ll INFll = 1145141919810114514ll;

int n, q[N], tot, pri[M], cnt[N], ecnt, nxt[L], adj[N], go[L], cap[L], S, T,
cur[N], lev[N], len, que[N], anst, wh[N];
ll p[N], ans[N];
bool vis[M], siv[N];

void add_edge(int u, int v, int w)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v; cap[ecnt] = w;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u; cap[ecnt] = 0;
}

ll mul(ll a, ll b, ll zyy) {return ((__int128) a) * b % zyy;}

ll qpow(ll a, ll b, ll zyy)
{
	ll res = 1;
	while (b)
	{
		if (b & 1) res = mul(res, a, zyy);
		a = mul(a, a, zyy);
		b >>= 1;
	}
	return res;
}

bool prime(ll num)
{
	if (num <= 4000) return !vis[num];
	if (!(num & 1)) return 0;
	ll tmp = num - 1; int cnt = 0;
	while (!(tmp & 1)) tmp >>= 1, cnt++;
	for (int i = 1; i <= 10; i++)
	{
		ll x = qpow(pri[i], tmp, num);
		for (int i = 1; i <= cnt; i++)
		{
			ll y = mul(x, x, num);
			if (y == 1 && x != 1 && x != num - 1) return 0;
			x = y;
		}
		if (x != 1) return 0;
	}
	return 1;
}

int calc(ll num)
{
	int res = 0;
	for (int i = 1; i <= tot; i++)
		while (num % pri[i] == 0) num /= pri[i], res++;
	return num > 1 ? res + 1 + (!prime(num)) : res;
}

bool bfs()
{
	for (int i = 1; i <= n + 2; i++) lev[i] = -1, cur[i] = adj[i];
	lev[que[len = 1] = S] = 0;
	for (int i = 1; i <= len; i++)
	{
		int u = que[i];
		for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
			if (cap[e] && lev[v] == -1)
			{
				lev[que[++len] = v] = lev[u] + 1;
				if (v == T) return 1;
			}
	}
	return 0;
}

ll dinic(int u, ll flow)
{
	if (u == T) return flow;
	ll res = 0, delta;
	for (int &e = cur[u], v = go[e]; e; e = nxt[e], v = go[e])
		if (cap[e] && lev[u] < lev[v])
		{
			delta = dinic(v, Min(1ll * cap[e], flow - res));
			if (delta)
			{
				cap[e] -= delta; cap[e ^ 1] += delta;
				res += delta; if (res == flow) break;
			}
		}
	if (res < flow) lev[u] = -1;
	return res;
}

bool sfd(int u, int tar)
{
	if (u == tar) return 1;
	siv[u] = 1;
	for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
		if (cap[e] && !siv[v] && sfd(v, tar)) return 1;
	return 0;
}

void work()
{
	read(n);
	for (int i = 1; i <= n; i++) read(p[i]), read(q[i]),
		cnt[i] = calc(p[i]);
	ecnt = 1; S = n + 1; T = n + 2;
	memset(adj, 0, sizeof(adj));
	for (int i = 1; i <= n; i++)
		for (int j = 1; j <= n; j++)
			if (cnt[i] + 1 == cnt[j])
			{
				if (p[j] % p[i]) continue;
				if (cnt[i] & 1) add_edge(i, j, INF);
				else add_edge(j, i, INF);
			}
	memset(siv, 0, sizeof(siv));
	for (int i = 1; i <= n; i++)
		if (cnt[i] & 1) add_edge(S, i, q[i]), wh[i] = ecnt - 1;
		else add_edge(i, T, q[i]), wh[i] = ecnt - 1;
	while (bfs()) dinic(S, INFll); anst = 0;
	for (int u = 1; u <= n; u++)
	{
		if (cap[wh[u]]) {ans[++anst] = p[u]; continue;}
		memset(siv, 0, sizeof(siv));
		int e1 = -1, e2, e3;
		for (int e = adj[u], v = go[e]; e; e = nxt[e], v = go[e])
		{
			if (v > n || (cnt[u] & 1) == (cnt[v] & 1)) continue;
			if ((cnt[u] & 1) && cap[e ^ 1] && cap[wh[v] ^ 1])
				{e1 = wh[u]; e2 = e; e3 = wh[v]; break;}
			if (!(cnt[u] & 1) && cap[e] && cap[wh[v] ^ 1])
				{e1 = wh[v]; e2 = e ^ 1; e3 = wh[u]; break;}
		}
		cap[e1]++; cap[e1 ^ 1]--; cap[e2]++; cap[e2 ^ 1]--; cap[e3]++; cap[e3 ^ 1]--;
		cap[cnt[u] & 1 ? e1 : e3]--; if (sfd(S, T)) ans[++anst] = p[u];
		cap[cnt[u] & 1 ? e1 : e3]++;
		cap[e1]--; cap[e1 ^ 1]++; cap[e2]--; cap[e2 ^ 1]++; cap[e3]--; cap[e3 ^ 1]++;
	}
	std::sort(ans + 1, ans + anst + 1);
	for (int i = 1; i <= anst; i++) printf("%lld ", ans[i]);
	puts("");
}

int main()
{
	#ifdef nealchentxdy
	#else
		freopen("game.in", "r", stdin);
		freopen("game.out", "w", stdout);
	#endif
	
	int T;
	for (int i = 2; i <= 4000; i++) if (!vis[i])
		for (int j = i * i; j <= 4000; j += i)
			vis[j] = 1;
	for (int i = 2; i <= 4000; i++) if (!vis[i]) pri[++tot] = i;
	read(T);
	while (T--) work();
	return 0;
}

ZROI 树上游戏

Statement

  • 有一棵 \(n\) 个点的有根树,\(1\) 为根,双方轮流操作

  • 每次选出一个点,这个点必须和上一次对方选出的点有祖先后代关系,每个点都不能被选超过一次

  • 求这棵树有多少个点的子集,满足这个子集内的点不能被选出的限制下,先手必胜,对 \(998244353\) 取模

  • \(n\le 2000\)

Solution

  • 还是一样,一个新图,树上有祖先后代关系的点之间连边,先手必胜当且仅当可选的点集没有完美匹配

  • 由于这是一棵树,考虑贪心匹配,即对于一个子树,贪心地让子树内配成的对数最多(剩下的点数最少)

  • \(f[u][i]\) 表示 \(u\) 的子树内的点集有多少个合法的子集,使得剩下的点数为 \(i\)

  • 转移时先把所有子节点的 DP 数组进行背包合并

  • 然后讨论 \(u\) 是否能被选出:\(u\) 能被选出则 \(i\leftarrow|i-1|\),否则 \(i\) 不变

  • 答案为 \(\sum_{i=1}^nf[1][i]\)

  • 由树上背包合并的经典复杂度分析得到复杂度 \(O(n^2)\)

Code

#include <bits/stdc++.h>

template <class T>
inline void read(T &res)
{
	res = 0; bool bo = 0; char c;
	while (((c = getchar()) < '0' || c > '9') && c != '-');
	if (c == '-') bo = 1; else res = c - 48;
	while ((c = getchar()) >= '0' && c <= '9')
		res = (res << 3) + (res << 1) + (c - 48);
	if (bo) res = ~res + 1;
}

const int N = 2005, M = N << 1, rqy = 998244353;

int n, ecnt, nxt[M], adj[N], go[M], f[N][N], sze[N], tmp[N], ans;

void add_edge(int u, int v)
{
	nxt[++ecnt] = adj[u]; adj[u] = ecnt; go[ecnt] = v;
	nxt[++ecnt] = adj[v]; adj[v] = ecnt; go[ecnt] = u;
}

void dfs(int u, int fu)
{
	f[u][0] = 1;
	for (int e = adj[u], v; e; e = nxt[e])
		if ((v = go[e]) != fu)
		{
			dfs(v, u);
			for (int i = 0; i <= sze[u] + sze[v]; i++) tmp[i] = 0;
			for (int i = 0; i <= sze[u]; i++)
				for (int j = 0; j <= sze[v]; j++)
					tmp[i + j] = (1ll * f[u][i] * f[v][j] + tmp[i + j]) % rqy;
			for (int i = 0; i <= sze[u] + sze[v]; i++) f[u][i] = tmp[i];
			sze[u] += sze[v];
		}
	sze[u]++;
	int tm = f[u][0];
	for (int i = 0; i < sze[u]; i++) f[u][i] = (f[u][i] + f[u][i + 1]) % rqy;
	f[u][1] = (f[u][1] + tm) % rqy;
}

int main()
{
	#ifdef zhouzhouzka
	#else
		freopen("game.in", "r", stdin);
		freopen("game.out", "w", stdout);
	#endif
	
	int x, y;
	read(n);
	for (int i = 1; i < n; i++) read(x), read(y), add_edge(x, y);
	dfs(1, 0);
	for (int i = 1; i <= n; i++) ans = (ans + f[1][i]) % rqy;
	return std::cout << ans << std::endl, 0;
}
posted @ 2020-02-26 19:44  epic01  阅读(388)  评论(0编辑  收藏  举报