引入
-
经典问题:一个无向图,双方轮流选出一个点,一个点最多被选出一次,且每次选出的点必须和上一次对方选出的点相邻
-
不能动者输
-
双方都绝顶聪明,求先手是否有必胜策略,以及先手第一次选哪些点能必胜
结论
-
先手第一次选 (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)
-
(1le nle 500),(1le p_ile 5 imes10^{10}),(1le q_ile 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) 取模
-
(nle 2000)
Solution
-
还是一样,一个新图,树上有祖先后代关系的点之间连边,先手必胜当且仅当可选的点集没有完美匹配
-
由于这是一棵树,考虑贪心匹配,即对于一个子树,贪心地让子树内配成的对数最多(剩下的点数最少)
-
(f[u][i]) 表示 (u) 的子树内的点集有多少个合法的子集,使得剩下的点数为 (i)
-
转移时先把所有子节点的 DP 数组进行背包合并
-
然后讨论 (u) 是否能被选出:(u) 能被选出则 (ileftarrow|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;
}