正如学长在一个月前跟我说的,网络流千变万化,在regional这个级别的比赛中经常出现,最小割模型就是其中一个。
其实我也是很早就知道最小割,但是我只是知道最小割等于最大流这个简单的定理。然后那时单纯的认为最小割嘛,跑一个最大流就行了,有什么难的……
然而,在一周以前,我认为最小割让人感到难的或许不是算法,而是构图,因为之前碰到的植物大战僵尸的最大权闭合子图。这个题就是灵活运用最小割的概念取推理证明了很多定理,进而把题目转化成了不容易看出来的最小割。
知道我做了这道题,才发现,最小割完全可以用一个完全不同的方法实现。先说说当时的背景吧。我们程序设计老师让我们做ccf的前三道题目,我嘛一向在程设这种课上不听指挥,自己跑去看最后一道题目。发现17年三月的题目是一道裸的网络流(最小割),但是惊人的发现有快2500W个点,根本破不了。于是去找队友帮忙,然后他就告诉我这道题,又被教育了一波……这是一道很老但是很经典的题目:
1001: [BeiJing2006]狼抓兔子
Time Limit: 15 Sec Memory Limit: 162 MBSubmit: 21787 Solved: 5480
[ Submit][ Status][ Discuss]
Description
现在小朋友们最喜欢的"喜羊羊与灰太狼",话说灰太狼抓羊不到,但抓兔子还是比较在行的,
而且现在的兔子还比较笨,它们只有两个窝,现在你做为狼王,面对下面这样一个网格的地形:
左上角点为(1,1),右下角点为(N,M)(上图中N=4,M=5).有以下三种类型的道路
1:(x,y)<==>(x+1,y)
2:(x,y)<==>(x,y+1)
3:(x,y)<==>(x+1,y+1)
道路上的权值表示这条路上最多能够通过的兔子数,道路是无向的. 左上角和右下角为兔子的两个窝,
开始时所有的兔子都聚集在左上角(1,1)的窝里,现在它们要跑到右下解(N,M)的窝中去,狼王开始伏击
这些兔子.当然为了保险起见,如果一条道路上最多通过的兔子数为K,狼王需要安排同样数量的K只狼,
才能完全封锁这条道路,你需要帮助狼王安排一个伏击方案,使得在将兔子一网打尽的前提下,参与的
狼的数量要最小。因为狼还要去找喜羊羊麻烦.
Input
第一行为N,M.表示网格的大小,N,M均小于等于1000.
接下来分三部分
第一部分共N行,每行M-1个数,表示横向道路的权值.
第二部分共N-1行,每行M个数,表示纵向道路的权值.
第三部分共N-1行,每行M-1个数,表示斜向道路的权值.
输入文件保证不超过10M
Output
输出一个整数,表示参与伏击的狼的最小数量.
Sample Input
3 4
5 6 4
4 3 1
7 5 3
5 6 7 8
8 7 6 5
5 5 5
6 6 6
5 6 4
4 3 1
7 5 3
5 6 7 8
8 7 6 5
5 5 5
6 6 6
Sample Output
14
相信你很容易看出来,参与伏击的狼最少,无疑是选取边集,使得边把网络切断且权值最小,这就刚好是最小割的定义。但是这题的数据最多100W个点(相比于ccf的题目数据还更弱?!)。普通的网络流肯定超时(据说dinic可以水过?!)。那么我们只能换个思路去做。我记得当时队友点拨我时,说把图转向,然后求最短路,当时瞬间有种恍然大悟的感觉,的确,确实比较好理解。如果我能通过一些最短的边把整个网络切断,那么这个边之和必定是最小割。但是背后的具体建图还有理论依据也很值得探讨。
这个问题的本源还是可以追溯到大神的论文。周冬《两极相通——浅析最大—最小定理在信息学竞赛中的应用》
正如大神所说的,根据欧拉公式,如果平面图有n个点,m条边,f个面,那么三个数字之前的关系满足f=m-n+2。那么如果对于同一幅图,我们如果把点和面对调,等到式子n=m-f+2,发现仍然满足欧拉公式。即重新构图,把面变为点,点变为面,边不变,得到的图仍然是平面图,我们称这样的图为原图的对偶图。然后,关于对偶图的边的问题,如果有一条边是原图两面的分界线,那么在对偶图中,它就是连接两个面对应点的边(相当于把边旋转了90°)。
注意到我把那个旋转标记了出来,因为这个旋转可以形象的代表我在原来的边上切了一刀,把原来的边割开了。那么,如果我们能够在对偶图中找到一组边把整个图中间切断,那么这组边就构成了一个割。既然要求最小割,那么我们边把对偶图中旋转过来的边的权值赋值为与原边一样。
那么,现在就要解决起点和终点的问题了。注意到,既然原图是平面图,那么它一定是有边界的,很显然,最小割一定是从图的一个边界贯穿到另外一个边界的,哦对,这里我默认起点和终点把整幅图以及图外的平面区域分成了两个部分。最小割一定能从一部分的外边界贯穿到另一部分的外边界。依然如此,我们就把原图中所有的外边界上的边连上起点和终点。这样,如果我们从起点到重点跑一遍最短路,那么求出来的就是最小割了。这个复杂度可是一下子降了很多,千万的数据理论上用dijkstra都不成问题。
至于本题的话,就是把方格图左边界和下边界与起点(终点)相连,右边界和上边界与终点(起点)相连,其他的按照我说的对偶图的转换方式转换成对偶图,再跑一遍最短路即可。具体代码如下:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstdlib>
#include<cmath>
#include<cstring>
#include<iomanip>
#include<queue>
#define LL long long
#define INF 2147483647
#define MAX_V 2001000
using namespace std;
int num[1010][1010][2],d[MAX_V];
int m,n,p=0,s,t;
struct Edge
{
int y,w;
};
struct cmp
{
bool operator()(int a,int b)
{
return d[a]>d[b];
}
};
priority_queue<int,vector<int>,cmp> q;
vector<Edge> g[MAX_V];
bool v[MAX_V];
inline void dijkstra(int s)
{
for(int i=1;i<=t;i++)
{
v[i]=0; d[i]=INF;
}
d[s]=0;
q.push(s);
while (!q.empty())
{
int j=q.top(); q.pop();
if (v[j]) continue; v[j]=1;
for(int k=0;k<g[j].size();k++)
if (d[g[j][k].y]>d[j]+g[j][k].w)
{
d[g[j][k].y]=d[j]+g[j][k].w;
q.push(g[j][k].y);
}
}
}
inline void addedge(int x,int y,int w)
{
Edge node;
node.y=y;
node.w=w;
g[x].push_back(node);
}
int main()
{
scanf("%d%d",&n,&m);
if (n==1||m==1)
{
int ans=INF;
for(int i=1;i<max(n,m);i++)
{
int x; scanf("%d",&x);
ans=min(ans,x);
}
printf("%d\n",ans);
return 0;
}
for(int i=1;i<n;i++)
for(int j=1;j<m;j++)
{
num[i][j][0]=++p;
num[i][j][1]=++p;
}
s=++p; t=++p;
for(int i=1;i<=n;i++)
for(int j=1;j<m;j++)
{
int x; scanf("%d",&x);
if (i==1) addedge(num[i][j][1],t,x);
if (i==n) addedge(s,num[i-1][j][0],x);
if (i!=n&&i!=1)
{
addedge(num[i-1][j][0],num[i][j][1],x);
addedge(num[i][j][1],num[i-1][j][0],x);
}
}
for(int i=1;i<n;i++)
for(int j=1;j<=m;j++)
{
int x; scanf("%d",&x);
if (j==1) addedge(s,num[i][j][0],x);
if (j==m) addedge(num[i][j-1][1],t,x);
if (j!=m&&j!=1)
{
addedge(num[i][j-1][1],num[i][j][0],x);
addedge(num[i][j][0],num[i][j-1][1],x);
}
}
for(int i=1;i<n;i++)
for(int j=1;j<m;j++)
{
int x; scanf("%d",&x);
addedge(num[i][j][0],num[i][j][1],x);
addedge(num[i][j][1],num[i][j][0],x);
}
dijkstra(s);
printf("%d\n",d[t]);
return 0;
}