POJ2914 Lutece 2710 Minimum Cut
13 April 2022 |
duanyll | Tags:
OI
题解
图论
题意
$n\leq 500$ 个点 $m$ 条边(最多完全图)的带权无向图,问至少删去的边权之和,使删边后图不连通。(任意两点的最小割)。
分析
显然枚举两点用最大流最小割来求跑不过。介绍此类问题的模板做法:Stoer-Wagner 算法,主要思想是枚举寻找任意两点的最小割,每处理两点之后进行“缩点”操作,可保证 $O(n^3)$ 复杂度。
考虑 $G$ 的最小割把 $G$ 分割为两侧,则枚举两点 $u,v$ 一定属于一下两种情况:
- $u, v$ 不在同一侧,那么枚举到 $u, v$ 时就能求出答案。
- $u, v$ 在同一侧,那么可以进行缩点操作,把 $u,v$ 合并成一个点,不会影响答案。
只需要给定起点时能够 $O(n^2)$ 求出最小割,然后不断删掉(合并)一个点再进行 $O(n^2)$ 的枚举,最终可达到 $O(n^3)$ 的复杂度。
由于具有缩点的特性,可以考虑类似 Prim 的做法,不断向一个点集中加点,这个点集对应最终最小割方案的一侧,而最小割另一侧已经通过缩点过程被缩成了一个点,于是只需要用 w[i]
数组来维护每个点集外的点到点集的直接连边边权之和。不断地选取当前 w[i]
最大的点加入点集中,最终选取的点的 w[v]
值就对应一种合法、并且最优的‘单点’最小割方案。
此时即可统计答案,然后缩点,再进行下一次类 Prim 枚举。缩点时应该考虑将最终剩下的 $v$ (一定要合并 $v$,下一次从头枚举才是有意义的,否则找到的最终点还是 $v$)和上一次添加进点集的点 $u$ 进行合并,容易贪心证明此种合并方式的最优性。
为了合并操作的方便,使用邻接矩阵存图,可以用下面的 for
循环完成缩点:
for (int i = 1; i <= remain; i++) {
if (i == u || i == v) continue;
g[i][u] += g[i][v];
g[u][i] += g[v][i];
}
然后只需删除 $v$,使得之后不再枚举它。下文代码中利用 fa
数组删除 $v$ 是一种巧妙的实现方式。
代码
int g[MAXN][MAXN];
int w[MAXN]; // w[i] 已选定集合中所有点到 i 的直接连边边权总和
int fa[MAXN]; // 处理合并操作后,删掉其中一个点
bool ins[MAXN];
int main() {
int n, m;
while (cin >> n >> m) {
memset(g, 0, sizeof g);
for (int i = 1; i <= m; i++) {
int u = read();
int v = read();
int c = read();
g[u][v] += c;
g[v][u] += c;
}
for (int i = 0; i < n; i++) {
fa[i] = i;
}
int ans = INF;
int remain = n;
while (remain > 1) {
memset(w, 0, sizeof w);
memset(ins, false, sizeof ins);
ins[0] = true;
int u = 0;
int v = 0;
for (int i = 1; i < remain; i++) {
u = v;
v = -1;
int cut_v = -1;
for (int j = 1; j < remain; j++) {
if (!ins[j]) {
w[j] += g[fa[j]][fa[u]];
if (w[j] > cut_v) {
cut_v = w[j];
v = j;
}
}
}
ins[v] = true;
}
ans = min(ans, w[v]);
// 类似并查集的合并操作
for (int i = 1; i < remain; i++) {
if (i == v) continue;
g[fa[i]][fa[0]] += g[fa[i]][fa[v]];
g[fa[0]][fa[i]] += g[fa[v]][fa[i]];
}
remain--;
fa[v] = fa[remain]; // 相当于是把 v 删掉了
}
write(ans);
putchar('\n');
}
}
Lutece 2710 是双倍经验。