2025.05.15 CW 模拟赛 C. 匹配
C. 匹配
读懂题意花了 \(1\) 个小时.
题目描述
小 w 有一个二分图,两边都有 \(n\) 个点。
小 w 想找一组完美匹配,但他只想用最简单的方法:每次找一条两个端点都在匹配中的边把它加上。
小 w 发现如果每次在可行的边里任选一条,最后可能并没办法匹配上所有点,这使得他非常愤怒(?)
于是他准备改一下这张图使得无论选哪条最后都可以是一个完美匹配。
由于小 w 很喜欢原来的图,所以你不能删边,并且你需要使得加的边最少。
由于小 w 在卷卷,所以想让你帮他算一下最少加几条边。
可以证明一定存在一种加边方案。
注:匹配的定义是一个端点不交的边的集合。一个点在匹配中的意思是集合里存在一条边以这个点为一端。完美匹配是所有点都在匹配中。
思路
对于「无论选哪条最后都可以是一个完美匹配」, 我们转化一下条件. 对于最终的每一个连通块, 两边的点数一定要是一样的, 同时连通块一定是一个完全二分图. 如果不是一个完全二分图, 那么我们一定能构造出一种方案使得至少一个点无法被匹配.

定义 \((a, b)\) 表示连通块左侧有 \(a\) 个点, 右侧有 \(b\) 个点. 根据上面的条件, 我们可以将问题转化为: 将连通块分成若干个集合, 使得每一个集合内 \(a = b\), 要求最小化每个集合内一边点数的平方和. 为什么不考虑原本的边? 因为原本有的边是一定的, 在最后输出时减去即可.
我们先考虑一个十分朴素的状压 \(\rm{DP}\). 令 \(f_{S}\) 表示当前选择了 \(S\) 集合内的连通块的最小代价. 转移的话枚举 \(S\) 的子集 \(T\) 即可, 同时注意合法的转移需要满足 \(S \oplus T\) 集合中的连通块 \(a = b\). 复杂度 \(\mathcal{O}(3^{2n})\).
进行一些小优化. 首先, 对于本身 \(a = b\) 的连通块, 我们可以直接将它们删除, 因为它一定不会和其他连通块合并. 另外, 对于形如 \((0, 1), (1, 0)\) 的连通块, 我们将它们单独拎出来, 分成两种情况
- 它们自身合并, 例如 \(1\) 个 \((0, 1), (1, 0)\) 可以合并成一个 \((1, 1)\).
- 加入到其余连通块中, 如果 \(a - b = k\), 我们可以再在该块中放入 \(k\) 个 \((0, 1)\) 以合法.
这样, 需要状压的连通块数成功变成 \(\frac{2n}{3}\). 更改一下状态, 设 \(f_{S, x, y}\) 表示当前选择了 \(S\) 内的连通块 \((\)不包含 \(a = b, (0, 1), (1, 0)\) 的连通块\()\), \((0, 1)\) 选择了 \(x\) 个, \((1, 0)\) 选择了 \(y\) 个的最小代价.
转移时我们枚举 \(S\) 的超集 \(T\), 表示新选择了哪些连通块, 如果 \(a, b\) 不等就用 \((1, 0)\) 或者 \((0, 1)\) 补齐. 同时因为 \((0, 1), (1, 0)\) 可以自己组, 所以也有转移 \(f_{S, x + 1, y + 1} \gets f_{S, x, y} + 1\).
参考代码
#include <bits/stdc++.h>
using namespace std;
int T, n, f[60], s1[60], s2[60], a[60], b[60], cn, p[2], sz1[1 << 16], sz2[1 << 16];
char c[30];
int find(int x)
{
if (x == f[x])
return x;
return f[x] = find(f[x]);
}
const int INF = 1e9 + 7;
int dp[1 << 16][26][26], c1[3000010], c2[3000010], c3[3000010], top;
int dfs(int s, int x, int y)
{
if (s == ((1 << cn) - 1) && x == p[0] && y == p[1])
return 0;
if (dp[s][x][y] != -1)
return dp[s][x][y];
int& ans = dp[s][x][y];
c1[++top] = s, c2[top] = x, c3[top] = y;
ans = INF;
if (x < p[0] && y < p[1])
ans = min(ans, dfs(s, x + 1, y + 1) + 1);
int S = s ^ ((1 << cn) - 1), now = S;
while (now)
{
int ga = x, gb = y, no = sz2[now];
if (sz1[now] < 0)
ga -= sz1[now], no -= sz1[now];
else
gb += sz1[now];
if (ga <= p[0] && gb <= p[1])
ans = min(ans, dfs(s | now, ga, gb) + no * no);
now = (now - 1) & S;
}
return ans;
}
void sol()
{
cin >> n;
p[0] = p[1] = 0;
int m = 0;
cn = 0;
for (int i = 1; i <= 2 * n; i++)
f[i] = i, s1[i] = s2[i] = 0;
for (int i = 1; i <= n; i++)
{
scanf("%s", c + 1);
for (int j = 1; j <= n; j++)
if (c[j] == '1')
f[find(i)] = find(j + n), m++;
}
for (int i = 1; i <= 2 * n; i++)
s1[find(i)] += (i <= n), s2[find(i)] += (i > n);
for (int i = 1; i <= 2 * n; i++)
if (find(i) == i)
{
if (s1[i] == 1 && s2[i] == 1)
{
m--;
continue;
}
if (s1[i] + s2[i] == 1)
p[s2[i]]++;
else
a[cn] = s1[i], b[cn] = s2[i], cn++;
}
for (int i = 1; i < (1 << cn); i++)
{
int lb = (i & -i), x = log2(lb);
sz1[i] = sz1[i ^ lb] + a[x] - b[x];
sz2[i] = sz2[i ^ lb] + a[x];
}
printf("%d\n", dfs(0, 0, 0) - m);
while (top)
dp[c1[top]][c2[top]][c3[top]] = -1, top--;
}
int main()
{
memset(dp, -1, sizeof(dp));
cin >> T;
while (T--)
sol();
return 0;
}
总结
要学会转化, 多找找性质.

浙公网安备 33010602011771号