XTU 1261 - Roads - [最小割][2017湘潭邀请赛B题(江苏省赛)]
之前在网上搜了一个下午没搜到这道题的题解,然后同时又对着叉姐写的两行字题解看了一个下午;
虽然基本上已经知道了这题的思路,但愣是因为自己代码实现起来太繁复,外加不确定正确性,没敢码……
但是一道题肝了一下午没肝出来,就要放弃的话,怕是太扎心了,忍不住就跑去ICPCCamp.Post问叉姐了(https://post.icpc-camp.org/d/715-2017-b-roads)
看了叉姐的对于我的几个问题的回复,我总算肯定了我的思路,而且叉姐还在下面给了标程,当时可以说心情非常愉悦;
听起来是非常的exciting,但是当我看到了整篇code没用一个数组,放codeblocks里愣是编译不通过,看了半天愣是没看懂的时候,我的心情瞬间原地爆炸,整个人都是崩溃的。
当然啦,主要原因还是那天整个人状态都不是很好,心态已经属于炸了的状态,看不懂也是正常;
睡了两觉,摆好心态,再回来看,就不是很难了。
嗯,口胡结束,开始正文。
分割线
题目链接:http://202.197.224.59/OnlineJudge2/index.php/Problem/read/id/1261
Time Limit : 1000 MS Memory Limit : 65536 KB
In ICPCCamp, there are n towns conveniently labeled with 1,2,…,n and m bidirectional roads planned to be built. The i-th road will be built between cities a[i] and b[i] with cost c[i] . The builders in ICPCCamp will build the (n−1) roads with the least total cost to connect any of two cities directly or indirectly.
Bobo, the mayor of ICPCCamp is going to remove some of the roads from the construction plan. He would like to know the minimum number roads to be removed to strictly increases the total cost.
Note that the total cost is considered as +∞ if no valid (n−1) roads exist after removing. It is also counted as "total cost strictly increases".
Input
The input contains zero or more test cases and is terminated by end-of-file. For each test case:
The first line contains two integers n and m . The i-th of the following m lines contains a[i],b[i],c[i] .
- 2≤n≤50
- n−1≤m≤n^2
- 1≤a[i],b[i]≤n
- 1≤c[i]≤10^9
- Any two cities will be connected if all m roads are built.
- The sum of n does not exceed 10^3 .
Output
For each case, output an integer which denotes the result.
Sample Input
3 3 1 2 1 1 3 2 2 3 3 3 4 1 2 1 1 2 1 1 3 2 1 3 3 3 4 1 2 1 1 2 1 1 3 2 1 3 2 4 6 1 2 1 1 3 1 1 4 1 2 3 1 2 4 1 3 4 1
Sample Output
1 1 2 3
题意:
在ICPCCamp里有n个城镇(即n个点,编号为1~n),然后现在计划建造m条双向道路(m条无向边,可能有重边),每条路<a,b,c>代表这条路连接了a,b两座城镇,建造这条路的花费为c;
实际的建造者会挑选m条路中的n-1条进行建造,这n-1条边满足最小生成树,即n-1条边连通所有的点并且花费总和最少;
现在Bobo想要修改建造计划,从m条计划建造的路中,去掉一些路,得到一个新的计划,使得建造者在这个新的计划下造路的时候(当然,建造者依然按照上面的原则挑路来造),花费比原来大(严格增大)。
另外要注意的是,假如新的计划中只剩下少于n-1条的路,那么就会是一个非连通图,这个时候也算作“严格增大”。
现在要求的是,最少移去几条路就可以满足“严格增大”;
题解:
(https://files.cnblogs.com/files/dilthey/xiangtan-2017-solution.pdf)
真的是简洁明了的题解呢
详细解释一下:
分割线 begin
因为建造者肯定是按最小生成树来造路的;那么通过删边,让最小生成树变大的原因是什么?
必然是删掉了某条(或者某些)边之后,某两个点之间的连接被一定程度上断开,必须要通过寻找权值更大的边来把这两个边连接起来。
那么,就很容易想到是个关于“割”的问题,又因为要删去的边最少,就不难联想到有关于最小割。
就像上面的题解里说的那样,假设所有边中,有相同权值的k条边,他们连接起了一个图(连通的,或者非连通的);
我们把这个图割开(如果是非连通图,就是选取某一个连通分量割开),使得存在两个点,对于任意的weight=k的边,都不能让这两个边重新连通(直接地或间接地);
(⊙v⊙)嗯,看起来很简单的样子。
但是我们又会想到一点,怎么才能确定我们断开了某两个点的连接之后,肯定只能用权值更大的边来代替,而不能是权值更小的边来代替呢?
考虑一种比较简单的情况,假设存在edge1=<a,b>,weight=2,但是又存在edge2=<a,b>,weight=1,那么我们如果想要删掉edge1来增大MST,这显然是不可能的。
意思是说……?对,我们在割图的时候,edge1根本就不能被割,因为就算把它割开了,也等于没割开(听起来是如此的暴躁……
也就是说,更一般的:
那么,怎样才能让最小割不把a,b割开呢?我一开始想到的是,在a,b间连一条cap=INF的边,这固然也是可以的;
当然,之前http://www.cnblogs.com/dilthey/p/7381056.html这篇文章里提到过,两个点间只有一条cap=INF的边连接的话,可以合并;
这就是https://post.icpc-camp.org/d/715-2017-b-roads/2叉姐给我的回复中提到的 “(2) 按照权值从小到大做,做完小的,把这些点缩起来。”
另外还想说的一点,是我当时想到建立一条cap=INF的边之后的顾虑,如果原本是两个连通分量,被我们这么一建边,被连到一块儿了怎么办?
由于当时不确定的东西太多,脑子一团浆糊,外加心态爆炸,想到还有这种问题的时候,吓得立马否决了自己加cap=INF边的想法了,
其实很简单的,两个连通分量,不管你用了多少条INF容量的边连到一起,最小割也不会割到这几条边上去;
那么这个时候的最小割,要么是一刀割在连通分量1上,要么是一刀割在连通分量2上;
而且,像叉姐说的:“(1) 如果某个权值的边,形成的是若干个连通块,那么要对每个连通块分别跑,答案取个小的。因为我们只要割开了某个连通块,整个图的 MST 就变大了。”;
由于如果一个图有多个连通分量,我们本来就要在每个连通分量的最小割里找个最小的;
要是它们连通了,那这个时候的求出最小割,不就省的我们去一个一个连通分量去跑了吗!?这么简单,当时居然没想到,简直了……
而如果像叉姐说那样,把两个点缩成一个点的话,那就更简单了,反正最小割也不能把一个点割开来,和不能割开cap=INF的遍一样,同样不影响。
分割线 end
嗯,这样思路就比较清晰了,就是:
①初始化令ans=INF,按权值从小到大枚举边
②所有weight==k的边构建起一个图(同时要做缩点操作)
③求得最小割(若为非连通图,则求所有连通分量的最小割,取最小的),更新ans(若mincut<ans,则ans=mincut)
④再次遍历所有weight==k的边,若两端点还未连通,则连通两个端点,遍历结束后,k++,回到②
这里给出叉姐给的标程(附解释):
1 #include <cstdio> 2 #include <cstring> 3 #include <climits> 4 #include <numeric> 5 #include <map> 6 #include <queue> 7 #include <utility> 8 #include <vector> 9 10 int find(std::vector<int>& parent, int u) 11 { 12 if (parent.at(u) != u) { 13 parent.at(u) = find(parent, parent.at(u)); 14 } 15 return parent.at(u); 16 }//并查集 17 18 std::vector<int> bfs(const std::vector<std::vector<int>>& cap, int s) 19 { 20 int n = cap.size(); //当前图有n条边 21 std::vector<int> lv(n, -1);//定义level[n]数组,并初始化均为-1 22 lv.at(s) = 0; 23 std::queue<int> q; 24 q.push(s); 25 while (!q.empty()) { 26 int u = q.front(); 27 q.pop(); 28 for (int v = 0; v < n; ++ v) { 29 if (cap.at(u).at(v) > 0 && lv.at(v) == -1) { 30 lv.at(v) = lv.at(u) + 1; 31 q.push(v); 32 } 33 } 34 } 35 //bfs跑出层次图 36 return lv;//返回level[n]数组 37 } 38 39 int dfs(const std::vector<int>& lv, std::vector<std::vector<int>>& cap, std::vector<int>& cur, int u, int t, int left) 40 { 41 if (u == t) { 42 return left; 43 } 44 int n = cap.size(); 45 int delta = 0; 46 for (auto& v = cur.at(u); v < n; ++ v) { 47 if (cap.at(u).at(v) > 0 && lv.at(v) == lv.at(u) + 1) { 48 int tmp = dfs(lv, cap, cur, v, t, std::min(left - delta, cap.at(u).at(v))); 49 delta += tmp; 50 cap.at(u).at(v) -= tmp; 51 cap.at(v).at(u) += tmp; 52 if (delta == left) { 53 break; 54 } 55 } 56 } 57 return delta; 58 } 59 60 int main() 61 { 62 int n, m; 63 while (scanf("%d%d", &n, &m) == 2) { 64 std::map<int, std::vector<std::pair<int, int>>> edges; 65 for (int i = 0; i < m; ++ i) { 66 int a, b, c; 67 scanf("%d%d%d", &a, &b, &c); 68 edges[c].emplace_back(a - 1, b - 1); 69 } 70 int result = INT_MAX; 71 std::vector<int> parent(n); 72 std::iota(parent.begin(), parent.end(), 0);//初始化并查集 73 for (auto&& iterator : edges) {//按边权遍历整个存储边的容器 74 auto&& es = iterator.second;//取出当前边权的所有边,定义为es 75 std::vector<bool> mark(n); 76 std::vector<std::vector<int>> cap(n, std::vector<int>(n)); 77 for (auto&& e : es) { //遍历es中的边 78 auto&& a = find(parent, e.first); 79 auto&& b = find(parent, e.second); 80 if (a != b) {//如果当前边的两个端点还未连通 81 mark.at(a) = mark.at(b) = true;//标记这两个点的父节点 82 cap.at(a).at(b) ++; 83 cap.at(b).at(a) ++; 84 //加入一条边,连着两个点的父节点(相当于之前已经连通的点,都进行了缩点) 85 //值得注意的是,这里用了cap++,这样可以有效地解决重边的问题 86 } 87 } 88 for (int s = 0; s < n; ++ s) {//枚举所有的点(并作为源点) 89 if (mark.at(s)) {//如果这个点属于当前要跑最小割的图 90 auto lv = bfs(cap, s);//bfs遍历图,标记当前连通块的所有点 91 int t = 0; 92 while (t < s && lv.at(t) == -1) { 93 t ++; 94 }//找当前连通块的点集,按升序排序的第一个点 95 if (t == s) { //如果这个点就是s,那代表这个连通块还未跑过,否则这个连通块就肯定已经跑过了 96 for (t = s + 1; t < n; ++ t) {//枚举汇点 97 if (lv.at(t) != -1) {//如果属于当前连通块 98 auto fl = cap; 99 int cut = 0; 100 while (true) { 101 auto lv = bfs(fl, s); 102 if (lv.at(t) == -1) { 103 break; 104 } 105 std::vector<int> cur(n); 106 cut += dfs(lv, fl, cur, s, t, INT_MAX); 107 }//求出最大流或(者说最小割) 108 result = std::min(result, cut);//不断更新,保证result为最小的 109 } 110 } 111 } 112 } 113 } 114 for (auto&& e : es) { //把边加入到最小生成树里 115 auto&& a = find(parent, e.first); 116 auto&& b = find(parent, e.second); 117 if (a != b) { 118 parent.at(a) = b; 119 } 120 } 121 //由于缩点的关系,不加cnt==n-1的判断条件也无所谓 122 //因为当最小生成树完整后,后面不管什么图,都会被缩成一个点,也就没有最小割了 123 } 124 printf("%d\n", result); 125 } 126 }
当然啦,标程的代码太风骚了……而且不是版本很新的编译器还编译不过去,上我自己的AC代码:
1 #include<cstdio> 2 #include<cstring> 3 #include<map> 4 #include<vector> 5 #include<algorithm> 6 #define MAXN 53 7 #define MAXM MAXN*MAXN 8 #define INF 0x3f3f3f3f 9 using namespace std; 10 11 int n,m; 12 struct Node{ 13 int u,v,w; 14 }; 15 //并查集模板开始 16 int par[MAXN],ran[MAXN]; 17 void ufs_init(){for(int i=1;i<=n;i++) par[i]=i,ran[i]=0;} 18 int find(int x) 19 { 20 if(par[x] == x) return x; 21 else return( par[x] = find(par[x]) ); 22 } 23 void unite(int x,int y) 24 { 25 x=find(x),y=find(y); 26 if(x == y) return; 27 if(ran[x] < ran[y]) par[x]=y; 28 else 29 { 30 par[y]=x; 31 if(ran[x] == ran[y]) ran[x]++; 32 } 33 } 34 bool isSame(int x,int y){return( find(x) == find(y) );} 35 //并查集模板结束 36 //stoer-wagner算法模板开始 37 int edge[MAXN][MAXN],dist[MAXN]; 38 bool vis[MAXN],bin[MAXN]; 39 void sw_init() 40 { 41 memset(edge,0,sizeof(edge)); 42 memset(bin,0,sizeof(bin)); 43 } 44 int merge(int &s,int &t) 45 { 46 memset(dist,0,sizeof(dist)); 47 memset(vis,0,sizeof(vis)); 48 int k,mincut,maxc; 49 for(int i=1;i<=n;i++) 50 { 51 k=-1, maxc=-1; 52 for(int j=1;j<=n;j++) if(!bin[j] && !vis[j] && dist[j] > maxc) {k=j;maxc=dist[j];} 53 if(k == -1) return mincut; 54 vis[k]=true; 55 s=t; t=k; 56 mincut=maxc; 57 for(int j=1;j<=n;j++) if(!bin[j] && !vis[j]) dist[j]+=edge[k][j]; 58 } 59 return mincut; 60 } 61 int stoer_wagner() 62 { 63 int mincut=INF,s,t,ans; 64 for(int i=1;i<=n-1;i++) 65 { 66 ans=merge(s,t); 67 bin[t]=1; 68 if(ans!=0 && ans<mincut) mincut=ans; 69 for(int j=1;j<=n;j++) if(!bin[j]) edge[s][j]=(edge[j][s]+=edge[j][t]); 70 } 71 return mincut; 72 } 73 //stoer-wagner算法模板结束 74 bool cmp(Node a,Node b){return a.w<b.w;} 75 int main() 76 { 77 Node E[MAXM]; 78 while(scanf("%d%d",&n,&m)!=EOF) 79 { 80 for(int i=1;i<=m;i++) 81 { 82 int a,b,c; 83 scanf("%d%d%d",&a,&b,&c); 84 E[i].u=a, E[i].v=b, E[i].w=c; 85 } 86 sort(E+1,E+m+1,cmp); 87 int ans=INF; 88 int edge_cnt=0; 89 ufs_init(); 90 for(int i=1;i<=m;) 91 { 92 int now_i=i,now_w=E[i].w; 93 sw_init(); 94 for(;now_w==E[i].w && i<=m;i++)//建图 95 { 96 if(!isSame(E[i].u,E[i].v)) 97 { 98 edge[find(E[i].u)][find(E[i].v)]++; 99 edge[find(E[i].v)][find(E[i].u)]++; 100 } 101 } 102 103 ans=min(ans,stoer_wagner()); 104 105 for(int i=now_i;now_w==E[i].w && i<=m;i++)//逐步建立最小生成树 106 { 107 if(!isSame(E[i].u,E[i].v)) 108 { 109 unite(E[i].u,E[i].v); 110 edge_cnt++; 111 } 112 } 113 if(edge_cnt>=n-1) break; 114 } 115 printf("%d\n",ans); 116 } 117 }
PS.可以看到,stoer-wagner算法稍微做一点点修改,就可以直接用来求多个连通分量最小割的最小者,非常方便,而且相比于标程的枚举源汇点求最大流的方法要快。
PS2.当然,标程中直接用并查集的性质来做缩点,展现出了对于并查集较深的理解,对于我这种只能用来做做模板题的咸鱼来说,是非常值得学习的。
(感觉这道题A了还是要截图纪念一下的
)