最小割浅谈

最小割浅谈

注:此浅谈中借用了一些博客和课件中的理论,模型描述。

一、最小割问题

 

  最小割问题是指:给出一种有向图(无向图)和两个点$S$、$T$,以及图中的边的边权,求一个权值和最小的边集,使得删除这些边之后,$S$和$T$不连通。这类问题,一般运用最大流等于最小流定理,求出$S$到$T​$的最大流来解决。

  例题一:[bzoj2521 [Shoi2010]最小生成树]

  分析与题解:题目中所说的操作——对于一条边,我们将其不变,其他的边减一。在做题的时候不太方便,所以我们将其进行转化,我们把这个操作转换成为:对于当前边,我们将其加一,其他的边不变。这样我们做题就方便多了。

  考虑最小生成树的求法:$kruskal$,要想让指定边一定出现在最小生成树中,我们就要让边权小于等于指定边的所有边都加入到图中之后,指定边的两个端点依旧不连通,这样我们就可以转化问题,并建立模型。我们将边权小于等于指定边的边连到图中,并以指定边的两个端点为$S$和$T$求最小割。我们所连的边权并不是原图中的边权,因为我们在这里要求的代价最小,所以我们的每一个边的边权要改为代价。因为我们如果要使当前边不出现在图中,我们就要让他的边权在一通操作之后大于指定变边,所以一个边的代价为$d_i-d_{lab}+1$。

#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 1000
#define inf 1000000000
int n,m,lab,s,t,dis[N],ans,a[N],b[N],c[N];
int cur[N],head[N],to[N<<5],nxt[N<<5],val[N<<5],idx=1;
void add(int a,int b,int c)
    {nxt[++idx]=head[a],to[idx]=b,val[idx]=c,head[a]=idx;}
bool bfs()
{
    memset(dis,-1,sizeof dis);
    queue <int> q;q.push(s),dis[s]=0;
    while(!q.empty())
    {
        int p=q.front();q.pop();
        if(p==t) return true;
        for(int i=head[p];i;i=nxt[i])
            if(val[i]>0&&dis[to[i]]==-1)
                dis[to[i]]=dis[p]+1,q.push(to[i]);
    } return false;
}
int dfs(int p,int flow)
{
    int now,temp=flow;
    if(p==t) return flow;
    for(int i=cur[p];i;i=nxt[i])
        if(val[i]>0&&dis[to[i]]==dis[p]+1)
        {
            now=dfs(to[i],min(val[i],temp));
            if(!now) dis[to[i]]=-1;
            temp-=now,val[i]-=now,val[i^1]+=now;
            if(val[i]) cur[p]=i;
            if(!temp) break;
        } return flow-temp;
}
void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans+=dfs(s,inf);}
int main()
{
    scanf("%d%d%d",&n,&m,&lab);
    for(int i=1;i<=m;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]); s=a[lab],t=b[lab];
    for(int i=1;i<=m;i++) if(i!=lab&&c[i]<=c[lab])
        add(a[i],b[i],-c[i]+c[lab]+1),add(b[i],a[i],-c[i]+c[lab]+1);
    dinic(),printf("%d\n",ans);
}

  例题二:[bzoj 2561最小生成树]

 

  分析与题解:此题和上一题的思路相近,只是,这道题我们是直接删边,所以代价就改为了$1$,并且题中所说是可能出现在最小生成树中,和最大生成树中,所以添加边就是严格小于和严格大于。最后答案就是严格小于求一遍的最小割加上严格大于的最小割。

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=m+1;i++) scanf("%d%d%d",&x[i],&y[i],&z[i]);
    for(int i=1;i<=m;i++) if(z[i]<z[m+1]) add(x[i],y[i],1),add(y[i],x[i],1);
    s=x[m+1],t=y[m+1],dinic(),memset(head,0,sizeof head),idx=1;
    for(int i=1;i<=m;i++) if(z[i]>z[m+1]) add(x[i],y[i],1),add(y[i],x[i],1);
    s=x[m+1],t=y[m+1],dinic(),printf("%d\n",ans);
}

  对于最小割,我们还可以求最大收益:详见下方例题。

  例题三:[bzoj 4177Mike的农场]        注:这个会用到后面的模型,所以建议看完后面再看前面。

  分析与题解:对于这道题,我们设$S$点为养羊,设$T$点为养牛,对于$i$号点,我们把他拆成两个点$i$和$i+n$,$i$和$S$连边,边权为第$i$个围栏养羊的收益,$i+n$和$T$连边,边权为第$i$个围栏养牛的收益。对于$i$和$i+n$这两个点,我们用边权为$inf$的边将他们连接在一起。对于三元组$(i,j,k)$,我们把$i$和$j+n$用边权为$k$的边连在一起,同样,我们也需要用边权为$k$的边将$j$和$i+n$连接在一起。对于每一个规则,我们新开一个节点,如果这个节点是养羊,我们就把它和$S$相连,反之和$T$相连,边权都是这个规则的收益,这个新开的节点还要和所给的集合中的点相连,边权为$inf$,注意如果这个规则是养羊,我们要和所给集合中的点编号相连,反之要和其编号加$n$的点相连。

  建图建完了,我们就要统计答案了,答案是什么呢?我们观察建图,我们割下去的边是不要的,也就是花费和舍掉的不够优秀的方案,所以我们要用总收益减去最小割,即本题的$\sum a_i+\sum b_i+\sum 规则收益-最小割$。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
int main()
{
    scanf("%d%d%d",&n,&m,&k),s=n+k+1,t=n+k+2;
    for(int i=1,a;i<=n;i++) scanf("%d",&a),add(s,i,a),add(i,s,0),ans+=a;
    for(int i=1,a;i<=n;i++) scanf("%d",&a),add(i,t,a),add(t,i,0),ans+=a;
    for(int i=1,a,b,c;i<=m;i++)
        scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,0),add(b,a,c),add(a,b,0);
    for(int i=1,a,b,c;i<=k;i++)
    {
        scanf("%d%d%d",&a,&b,&c),ans+=c;
        if(b) add(i+n,t,c),add(t,i+n,0);
        else add(s,i+n,c),add(i+n,s,0);
        for(int j=1,d;j<=a;j++)
        {
            scanf("%d",&d);
            if(b) add(d,i+n,inf),add(i+n,d,0);
            else add(i+n,d,inf),add(d,i+n,0);
        }
    }
    dinic(),printf("%d\n",ans);
}

  对于点的分割,我们有时可以转化为对于边的分割:

  例题四:[bzoj 3144[Hnoi2013]切糕]

  分析与题解:对于此题,我们观察对于点的分割不太好操作,我们考虑转化模型。由于我们是要拦腰切成两半,所以我们在高上入手,我们将高的$r$层多加一层,将点转化为边,第一层的点就相当于连接转化后的第一层和第二层的边,第二层的点就相当于连接转化后的第二层和第三层的边……第$r$层的点就相当于连接转化后的第$r$层和第$r+1$层的边。

  如果不考虑$D$这道题现在就解决了。对于$D$的限制,我们发现不成立只需要考虑$f(x,y)-f(x',y')\gt D$的情况,因为$f(x,y)-f(x',y')\lt -D$的情况在考虑点$(x',y',f(x',y'))$的时候就转化为上述形式。我们现在要将$(x,y,z)$向$(x',y',z’-k),k\in[D+1,R+1]$连边,边权为$inf$,这样在不合法的时候,我们依旧能使$S$和$T$联通。

  建图建完,直接用$Dinic$跑最小割就可以了。

int pla(int x,int y,int z) {return (z-1)*p*q+(x-1)*q+y;}
int main()
{
    scanf("%d%d%d%d",&p,&q,&r,&D),s=p*q*(r+1)+1,t=p*q*(r+1)+2;
    for(int i=1;i<=r;i++) for(int j=1;j<=p;j++) for(int k=1;k<=q;k++)
        scanf("%d",&num[j][k][i]);r++;
    for(int i=1;i<=p;i++) for(int j=1;j<=q;j++)
        add(s,pla(i,j,1),inf),add(pla(i,j,1),s,0);
    for(int i=1;i<=p;i++) for(int j=1;j<=q;j++)
        add(pla(i,j,r),t,inf),add(t,pla(i,j,r),0);
    for(int i=1;i<=p;i++) for(int j=1;j<=q;j++) for(int k=1;k<r;k++)
        add(pla(i,j,k),pla(i,j,k+1),num[i][j][k]),add(pla(i,j,k+1),pla(i,j,k),0);
    for(int i=1;i<p;i++) for(int j=1;j<=q;j++) for(int t=D+1;t<=r;t++)
            add(pla(i,j,t),pla(i+1,j,t-D),inf),add(pla(i+1,j,t-D),pla(i,j,t),0);
    for(int i=1;i<=p;i++) for(int j=1;j<q;j++) for(int t=D+1;t<=r;t++)
            add(pla(i,j,t),pla(i,j+1,t-D),inf),add(pla(i,j+1,t-D),pla(i,j,t),0);
    for(int i=2;i<=p;i++) for(int j=1;j<=q;j++) for(int t=D+1;t<=r;t++)
            add(pla(i,j,t),pla(i-1,j,t-D),inf),add(pla(i-1,j,t-D),pla(i,j,t),0);
    for(int i=1;i<=p;i++) for(int j=2;j<=q;j++) for(int t=D+1;t<=r;t++)
            add(pla(i,j,t),pla(i,j-1,t-D),inf),add(pla(i,j-1,t-D),pla(i,j,t),0);
    dinic(),printf("%d\n",ans);
}

二、最小割的几个小模型(一)——   最小点割集

  最小点割集是指:给出一张有向图(无向图)和两个点$S$、$T$,每个点都有一个正数点权,求一个不包含点$S$、$T$的权值和最小的点集使得删掉点集中的所有点后,$S$无法到达$T$。

  求法:对于这个问题,我们将每一个点拆成两个点,一个为入点,一个为出点,这两个点之间有一条边权为原图中点权的有向边,从入点指向出点。对于原图中的边$x\rightarrow y$,我们将其更改为$x$的出点$\rightarrow y$的入点。在转化完的图上跑最小割就是原图的最小点割集。

  例题一:[bzoj 3630[JLOI2014]镜面通道]

  分析与题解:在物理上有一个定理,只要水能通过的地方,光就能通过。对于这道题,就是只要$S$和$T$联通光就能通过。这样我们就可以将问题从“最少拿走多少个光学元件后,存在一条光线线路可以从CD射出”转化为“最少拿走多少个关学元件后,$AC$和$BD$不连通”。

  转化后的问题似乎就好解决了。我们现在考虑,转化后的问题怎么建图,我们把每一个光学元件拆成入点和出点,这两个点之间连边,边权为$1$。对于$S$和元件、$T$和元件之间的连边,如果当前元件和$BD$有交点,测当前元件和$S$连边,边权为$inf$;如果当前元件和$AC$有交点,则当前元件和$T$连边,边权为$inf$。对于两个光学元件之间,如果这两个元件之间相交我们就把这两个点用一条边权为$inf$的边连在一起。现在难点不是如何建图,而是怎么判断两个光学元件相交。

  对于两个光学元件是同一种光学元件的这种情况比较好判断,分别就是矩形相交的判断和圆形相交的判断,但是对于这两个混在一起就不好判断了。圆形和矩形相交一共分为四种情况:1.矩形的一个角在圆形之中,这样我们直接用点和圆的关系进行判断就可以了。2.圆形在矩形两侧,但是矩形没有角在圆形里,对于这种情况我们先要判断圆心是不是在矩形的上下边所在直线之间,若不是,这种情况一定不成立,若是我们还需要判断矩形的左右边所在直线直线是不是有一条经过圆形,这个直接用直线于圆的位置关系就可以。3.第三种情况和第二种情况基本一样,就是把圆形在矩形的左右变成圆形在矩形的上下。4.圆形在矩形中间,对于这种情况,我们就只需要判断圆心是不是在矩形之内就好。

long long squ(int x) {return 1ll*x*x;}
bool line1(int i,int j)
{
    if(min(a[j],a[i])!=a[i]||max(a[j],c[i])!=c[i]) return false;
    if(min(b[j],b[i])==b[i]&&max(b[j],d[i])==d[i]) return true;
    return squ(b[j]-b[i])<=squ(c[j])||squ(b[j]-d[i])<=squ(c[j]);
}
bool line2(int i,int j)
{
    if(min(b[j],b[i])!=b[i]||max(b[j],d[i])!=d[i]) return false;
    return squ(a[j]-a[i])<=squ(c[j])||squ(a[j]-c[i])<=squ(c[j]);
}
bool point(int i,int j)
{
    return squ(a[i]-a[j])+squ(b[i]-b[j])<=squ(c[j])||
        squ(a[i]-a[j])+squ(d[i]-b[j])<=squ(c[j])||
        squ(c[i]-a[j])+squ(d[i]-b[j])<=squ(c[j])||
        squ(c[i]-a[j])+squ(b[i]-b[j])<=squ(c[j]);
}
bool meet(int i,int j)
{
    if(kind[i]==1&&kind[j]==1) return squ(a[i]-a[j])+squ(b[i]-b[j])<=squ(c[i]+c[j]);
    if(kind[i]==2&&kind[j]==2)
        return min(c[i],c[j])>=max(a[i],a[j])&&min(d[i],d[j])>=max(b[i],b[j]);
    if(kind[i]==1) swap(i,j);
    return line1(i,j)||line2(i,j)||point(i,j);
}
bool up(int i)
    {if(kind[i]==1) return b[i]+c[i]>=y; return d[i]>=y;}
bool down(int i)
    {if(kind[i]==1) return b[i]-c[i]<=0; return b[i]<=0;}
int main()
{
    scanf("%d%d%d",&x,&y,&n),s=n*2+1,t=n*2+2;
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d%d",&kind[i],&a[i],&b[i],&c[i]);
        if(kind[i]==2) scanf("%d",&d[i]);
    }
    for(int i=1;i<=n;i++) add(i,i+n,1),add(i+n,i,0);
    for(int i=1;i<=n;i++) if(down(i)) add(s,i,inf),add(i,s,0);
    for(int i=1;i<=n;i++) if(up(i)) add(i+n,t,inf),add(t,i+n,0);
    for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++) if(meet(i,j))
        add(i+n,j,inf),add(j,i+n,0),add(j+n,i,inf),add(i,j+n,0);
    dinic(),printf("%d\n",ans);
}

  例题二:[bzoj 1339[Baltic2008]Mafia]

  分析与题解:这道题显然是一道最小点割集,对于建图就是每一个点分成入点和出点,这两个点之间连的边的边权为原图中的点权。对于原图中的边$x\rightarrow y$,我们将其更改为$x$的出点$\rightarrow y$的入点。在转化完的图上跑最小割就是原图的最小点割集。这道题的$S$和$T$就是匪徒的出发点和目标点。

int main()
{
    scanf("%d%d%d%d",&n,&m,&s,&t),t+=n;
    for(int i=1,a;i<=n;i++) scanf("%d",&a),add(i,i+n,a),add(i+n,i,0);
    for(int i=1,a,b;i<=m;i++) scanf("%d%d",&a,&b),
        add(a+n,b,inf),add(b,a+n,0),add(b+n,a,inf),add(a,b+n,0);
    dinic(),printf("%d\n",ans);
}

三、最小割的几个小模型(二)——   最小割树

  一张图的最小割树是类似于最小生成树的东西,只是这棵树中任意两点之间的简单路径上的边权最小值为这两点在原图中的最小割。

  求法:运用到分治算法,对于一个分治层,我们先在其中选出任意两个点,以这两个点为源点和汇点做最小割,在另一个图中把这两个点的对应点之间用一条边权为求出的最小割边连接起来,运用搜索,将当前分治层的点分成两个集合,源点能到达的点集,汇点能到达的点集,在向下分治这两个点集。代码详见例题。

  例题一:[bzoj 4519[Cqoi2016]不同的最小割]

  分析与题解:这道题是最小割树的模板题,对于题目中给出的图,我们求出这个图的最小割树,在求出最小割树的同时维护两点之间的最小割,这样我们就能通过排序来求得一共有多少不同的最小割。

#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 1000
#define M 10000
#define inf 1000000000
int n,m,s,t,mincut,ans,point[N],tmp[N],dis[N],cut[N][N],number[N*N],cnt;
int cur[N],head[N],to[M<<1],val[M<<1],nxt[M<<1],idx=1;bool vis[N];
void add(int a,int b,int c)
    {nxt[++idx]=head[a],to[idx]=b,val[idx]=c,head[a]=idx;}
bool bfs() 
{
    memset(dis,-1,sizeof dis);
    queue <int> q;q.push(s),dis[s]=0;
    while(!q.empty())
    {
        int p=q.front();q.pop();
        if(p==t) return true;
        for(int i=head[p];i;i=nxt[i])
            if(val[i]>0&&dis[to[i]]==-1)
                dis[to[i]]=dis[p]+1,q.push(to[i]);
    } return false;
}
int dfs(int p,int flow)
{
    int now,temp=flow;
    if(p==t) return flow;
    for(int i=cur[p];i;i=nxt[i])
        if(val[i]>0&&dis[to[i]]==dis[p]+1)
        {
            now=dfs(to[i],min(val[i],temp));
            if(!now) dis[to[i]]=-1;
            temp-=now,val[i]-=now,val[i^1]+=now;
            if(val[i]) cur[p]=i;
            if(!temp) break;
        } return flow-temp;
}
void dinic() {while(bfs()) memcpy(cur,head,sizeof head),mincut+=dfs(s,inf);}
void dfs1(int p)
{
    vis[p]=true;
    for(int i=head[p];i;i=nxt[i])
        if(val[i]&&vis[to[i]]==false) dfs1(to[i]);
}
void build(int l,int r)
{
    if(l==r) return;
    for(int i=2;i<=idx;i+=2) val[i]=val[i^1]=(val[i]+val[i^1])/2;
    int lx=l,rx=r;s=point[l],t=point[r];
    mincut=0,dinic(),memset(vis,0,sizeof vis),dfs1(s);
    for(int i=1;i<=n;i++) if(vis[i])
        for(int j=1;j<=n;j++) if(!vis[j])
            cut[i][j]=cut[j][i]=min(cut[i][j],mincut);
    for(int i=l;i<=r;i++)
        if(vis[point[i]]) tmp[lx++]=point[i]; else tmp[rx--]=point[i];
    for(int i=l;i<=r;i++) point[i]=tmp[i];
    build(l,lx-1),build(rx+1,r);
}
int main()
{
    scanf("%d%d",&n,&m),memset(cut,0x7f7f,sizeof cut);
    for(int i=1,a,b,c;i<=m;i++)
        scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c);
    for(int i=1;i<=n;i++) point[i]=i; build(1,n);
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
        if(i!=j) number[++cnt]=cut[i][j];
    sort(number+1,number+cnt+1);
    for(int i=1,tmp=0;i<=cnt;i+=tmp,ans++,tmp=0)
        while(number[i]==number[i+tmp]&&i+tmp<=cnt) tmp++;
    printf("%d\n",ans);
}

  例题二:[bzoj 2229[Zjoi2011]最小割]

 

  分析与讲解:这道题和上一道例题差不多,只是不是排序求了,是直接遍历二维数组求有多少比所给的$x$小的。

四、最小割的几个小模型(三)——   最大权闭合子图

  闭合子图指的是,对于有向图,我们选择一些点,在这个点集之中,没有一个点的出边指向非此点集中的点,但是可以有其他点的出边指向这个点集之中。所说的最大权闭合子图,就是在这个图的所有闭合子图中点权和最大的。

  求法:建立源点,向每一个点权为正的点连边,边权为该点的权值。建立汇点,向每一个点权为负连边,边权为该点的权值的绝对值。原图中的边进行保留,边权为$inf$。最大权闭合子图就是所有的点权为正的点权和减去最小割。

  例题一:[bzoj 1497[NOI2006]最大获利]

  分析与题解:建立源点$S$点,将所有的用户和$S$相连,边权就是当前用户的收益,建立汇点$T$点,将所有的中转站和$T$相连,边权就是当期中转站的花费。对于一个用户,他需要两个中转站,所以如果选择当前用户就一定要选择这两个中转站,根据这条性质我们知道,用户应该跟他的中转站相连,由用户指向中转站,边权为$inf$。

void dinic() {while(bfs()) ans-=dfs(s,inf);}
int main()
{
    scanf("%d%d",&n,&m); s=n+m+1,t=n+m+2;
    for(int i=1,a;i<=n;i++) scanf("%d",&a),add(i,t,a),add(t,i,0);
    for(int i=1,a,b,c;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c);ans+=c;
        add(s,i+n,c),add(i+n,s,0);
        add(i+n,b,inf),add(b,i+n,0);
        add(i+n,a,inf),add(a,i+n,0);
    } dinic(),printf("%d\n",ans);
}

  例题二:[bzoj 5037[Jsoi2014]电信网络]

  分析与题解:对于这道题,对于一个通信基站,如果他的收益是正的,则将他和$S$相连,反之和$T$点相连。对于两个基站$x、y$,如果$x$在$y$的通信范围则选择$y$基站就一定要选择$x$基站,这样的话我们就要将$y$和$x$相连,由$y$指向$x$,边权为$inf$。

bool check(int i,int j) {return (place_x[i]-place_x[j])*(place_x[i]-place_x[j])
    +(place_y[i]-place_y[j])*(place_y[i]-place_y[j])<=range[i]*range[i];}
int main()
{
    scanf("%d",&n),s=n+1,t=n+2;
    for(int i=1;i<=n;i++)
        scanf("%lld%lld%lld%d",&place_x[i],&place_y[i],&range[i],&money[i]);
    for(int i=1;i<=n;i++)
        if(money[i]<0) add(i,t,-money[i]),add(t,i,0);
        else ans+=money[i],add(s,i,money[i]),add(i,s,0);
    for(int i=1;i<=n;i++) for(int j=1;j<=n;j++)
        if(i!=j) if(check(i,j)) add(i,j,inf),add(j,i,0);
    dinic(),printf("%d\n",ans);
}

  例题三:[bzoj 1565[NOI2009]植物大战僵尸]

  分析与题解:这道题目是一道经典的最大权闭合子图,对于每一个植物我们首先想到应该是向上一道题一样,正连$S$,负连$T$,植物之间互相连。现在讨论一下植物之间连边的情况。对于两个植物$x$和$y$,如果$x$保护$y$,则在吃掉$y$之前一定要吃掉$x$,所以我们要从$y$向$x$连一条边,边权为$inf$。题中说了,僵尸只能从右侧进入,并且吃能横着走,所以这相当于要吃掉当前的植物,就必须要吃掉当前植物右面的植物,所以我们还需要将当前植物向他右面的植物连上一条边,边权为$inf$。这样我们就可以在建完的图上跑最大权闭合子图。

  但是这样建图有问题,现在有一种情况:$x$植物保护$y$植物,并且$y$植物还保护$x$植物。在这种情况下,$x$和$y$都是不能被吃掉的,并且他们保护的所有植物都不能被吃掉。但是在我们上述的建图中,这种情况是不能判断出来的,这个环和之后的点是能够被吃掉的,所以我们要更改一下建图。我们在建图之前应该先判断这个点是否有资格被吃掉。

  对于$x$保护$y$,我们从$x$向$y$连一条有向边。在连完所有的边之后,跑拓扑序,若当前点能进入到队列里,这个点才能被建到最大权闭合子图的全图中,反之则不能。这样更改后的图才是一个可以运用的图。

#include <queue>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define N 1010
#define inf 1000000000
int n,m,s,t,idx,num[N],belong[N],have[N][N],dis[N],ans;
int cur[N],head[N],to[N*N],nxt[N*N],val[N*N],in[N];bool is[N];
int pla(int a,int b) {return (a-1)*m+b;}
void add(int a,int b,int c)
    {nxt[++idx]=head[a],to[idx]=b,val[idx]=c,head[a]=idx;}
void build()
{
    queue <int> q;
    for(int i=1;i<=n*m;i++) for(int j=1;j<=belong[i];j++)
        add(i,have[i][j],0),in[have[i][j]]++;
    for(int i=1;i<=n*m;i++) if(!in[i]) q.push(i),is[i]=true;
    while(!q.empty())
    {
        int p=q.front();q.pop();
        for(int i=head[p];i;i=nxt[i])
            {in[to[i]]--; if(!in[to[i]]) q.push(to[i]),is[to[i]]=true;}
    } idx=1;
    memset(head,0,sizeof head),memset(to,0,sizeof to);
    memset(nxt,0,sizeof nxt),memset(val,0,sizeof val);
}
void init()
{
    for(int i=1;i<=n*m;i++)
    {
        if(!is[i]) continue;
        if(num[i]<0) add(i,t,-num[i]),add(t,i,0);
        else add(s,i,num[i]),add(i,s,0),ans+=num[i];
    }
    for(int i=1;i<=n*m;i++)
    {
        if(!is[i]) continue;
        for(int j=1;j<=belong[i];j++)
            if(is[have[i][j]]) add(i,have[i][j],0),add(have[i][j],i,inf);
    }
}
bool bfs()
{
    memset(dis,-1,sizeof dis);
    queue <int> q; q.push(s),dis[s]=0;
    while(!q.empty())
    {
        int p=q.front();q.pop();
        if(p==t) return true;
        for(int i=head[p];i;i=nxt[i])
            if(val[i]>0&&dis[to[i]]==-1)
                dis[to[i]]=dis[p]+1,q.push(to[i]);
    } return false;
}
int dfs(int p,int flow)
{
    int now,temp=flow;
    if(p==t) return flow;
    for(int i=cur[p];i;i=nxt[i])
        if(val[i]>0&&dis[to[i]]==dis[p]+1)
        {
            now=dfs(to[i],min(val[i],temp));
            if(!now) dis[to[i]]=-1;
            temp-=now,val[i]-=now,val[i^1]+=now;
            if(val[i]) cur[p]=i;
            if(!temp) break;
        } return flow-temp;
}
void dinic() {while(bfs()) memcpy(cur,head,sizeof cur),ans-=dfs(s,inf);}
int main()
{
    scanf("%d%d",&n,&m),s=n*m+1,t=n*m+2;
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
    {
        scanf("%d%d",&num[pla(i,j)],&belong[pla(i,j)]);
        for(int k=1,a,b;k<=belong[pla(i,j)];k++)
            scanf("%d%d",&a,&b),have[pla(i,j)][k]=pla(a+1,b+1);
        if(j>1) have[pla(i,j)][++belong[pla(i,j)]]=pla(i,j-1);
    } build(),init(),dinic(),printf("%d\n",max(ans,0));
}

  例题四:[bzoj 4873[Shoi2017]寿司餐厅]

  分析与题解:如果我们选择了$[l,r]$的寿司,则我们就相当于选择了$[l+1,r]、[l,r-1]$的寿司,所以我们就可已经问题转化成如果要选择$[l,r]$的寿司,我们就必须选择$[l+1,r]、[l,r-1]$的寿司,这样我们就可以连边了。我们对每一个区间开一个点。对于$[l,r]$区间所对应点连出一条边,指向$[l+1,r]、[l,r-1]$所对应的点,边权为$inf$。对于每一个区间的点我们都用一条边权为当前区间美味值的边和$S$连接起来。对于花销,每一个寿司都和$T$相连,边权为他的编号。每一种寿司都开一个节点,每个寿司和自己种类所对应的点相连,边权为$inf$,这个种类还要和$T$相连,边权为$m\times x ^2$。我们在建出来的图上跑最大权闭合子图就可以了。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
int main()
{
    scanf("%d%d",&n,&m),s=n*(n+1)/2+1,t=n*(n+1)/2+2;
    for(int i=1;i<=n;i++) scanf("%d",&num[i]),mx=max(mx,num[i]); s+=mx,t+=mx;
    for(int i=1;i<=n;i++) if(!is[num[i]])
        is[num[i]]=true,add(n*(n+1)/2+num[i],t,m*num[i]*num[i]),add(t,n*(n+1)/2,0);
    for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) ord[i][j]=++cnt;
    for(int i=1;i<=n;i++)
        for(int j=i,a;j<=n;j++)
        {
            scanf("%d",&a);
            if(i==j) (a-num[i]<0)?(add(ord[i][j],t,num[i]-a),add(t,ord[i][j],0)):
                (ans+=a-num[i],add(s,ord[i][j],a-num[i]),add(ord[i][j],s,0));
            else (a<0)?(add(ord[i][j],t,-a),add(t,ord[i][j],0)):
                (ans+=a,add(s,ord[i][j],a),add(ord[i][j],s,0));
            if(i==j) add(ord[i][j],n*(n+1)/2+num[i],inf),add(n*(n+1)/2+num[i],ord[i][j],0);
            else add(ord[i][j],ord[i+1][j],inf),add(ord[i+1][j],ord[i][j],0),
                add(ord[i][j],ord[i][j-1],inf),add(ord[i][j-1],ord[i][j],0);
        } dinic();printf("%d\n",ans);
}

五、最小割的几个小模型(四)——   自己与好朋友

  这类问题是本人自己总结出来的一类问题:这类问题有一道经典例题,这道例题也正是这个模型名字的由来。

  例题一:[bzoj 1934[Shoi2007]Vote 善意的投票]&&[bzoj 2768[JLOI2010]冠军调查]

  分析与题解:我们定义和$S$直接相连的点表示睡午觉,和$T$直接相连的点表示不睡午觉。若第$i$个小朋友意见是睡午觉,我们便把$i$和$S$相连,边权为$0$,把$i+n$和$T$相连,边权为$1$,这样表示这个小朋友要是选择睡午觉便不会对答案贡献$1$,反之会对答案贡献$1$。若第$i$个小朋友意见是不睡午觉,我们便把$i$和$S$相连,边权为$1$,把$i+n$和$T$相连,边权为$0$,这样表示这个小朋友要是选择不睡午觉便不会对答案贡献$1$,反之会对答案贡献$1$。这样我们便把这个小朋友和自己的意愿相反的情况对于答案的贡献处理完毕。

  对于两个不同的小朋友,我们考虑把他们互相连在一起,因为是只有不同的情况下才会产生贡献,所以$i$和$j+n$连一条边权为$1$的边,$j$和$i+n$连一条边权为$1$的边($i$和$j$是好朋友)。这样建出来的图就可以处理这个问题了,最小割就是答案。

int main()
{
    scanf("%d%d",&n,&m),s=n*2+1,t=n*2+2;
    for(int i=1,a;i<=n;i++)
        scanf("%d",&a),(a==1)?(add(s,i,1),add(i,s,0)):(add(i+n,t,1),add(t,i+n,0));
    for(int i=1,a,b;i<=m;i++)
        scanf("%d%d",&a,&b),add(a,b+n,1),add(b+n,a,0),add(b,a+n,1),add(a+n,b,0);
    dinic();printf("%d\n",ans);
}

  例题二:[bzoj 4439[Swerc2015]Landscaping]

  分析与题解:这个题目可以转化为例题一的模型,我们相当于把小朋友变成土地,睡不睡午觉变成高地和低地,好朋友变成相邻,违背自己的意愿对于答案的贡献变为$A$,和好朋友不同对于答案的贡献变为$B$,求最小割。

int main()
{
    scanf("%d%d%d%d",&n,&m,&A,&B),s=n*m*2+1,t=n*m*2+2;
    for(int i=1;i<=n;i++) scanf("%s",map[i]+1);
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
        if(map[i][j]=='.') add(s,(i-1)*m+j,B),add((i-1)*m+j,s,0);
        else add((i-1)*m+j,t,B),add(t,(i-1)*m+j,0);
    for(int i=1;i<=n;i++) for(int j=1;j<m;j++)
        add((i-1)*m+j,(i-1)*m+j+1,A),add((i-1)*m+j+1,(i-1)*m+j,A);
    for(int i=1;i<n;i++) for(int j=1;j<=m;j++)
        add((i-1)*m+j,i*m+j,A),add(i*m+j,(i-1)*m+j,A);
    dinic();printf("%d\n",ans);
}

六、最小割的几个小模型(五)——   组合收益

  组合收益是指在正常的收益问题上加上一些限制条件:对于$x$和$y$两个物品,若这两个物品都选择就会在获得这两个物品原本收益之后额外获得一个收益,求能获得最大收益。

  求法:对于每一个单个物品,我们把它和$S$用一条边权为当前物品收益的边相连,和$T$用一条边权为当前物品花销的边相连。对于每一个组合的收益,我们新建一个节点,表示当前的组合收益,这样我们就需要把这个组合和$S$相连,边权为组合的收益,这个代表组合的点还要和组合中的代表物品的点相连,边权为$inf$。对于建出来的图,我们跑最小割,最后用所有的收益之和减去最小割就是答案。

  例题一:[bzoj 3438小M的作物]

 

  分析与题解:这道题就是经典的组合收益模型。我们对于所有的作物先向$S$和$T$连边,边权分别为$A_i$和$B_i$。对于一种组合,我们就新开一个节点表示当前组合,若是种在$A$耕地里,就把这个点连向$S$,边权为当前组合的收益,反之若是这个组合是种在$B$耕地里,就把这个点连向$T$,边权为当前组合的收益。对于这个点,我们还把其和当前组合中的所有作物相连,边权为$inf$。对于这个图,我们求出其最小割,再用所有的收益减去最小割就可以了。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
int main()
{
    scanf("%d",&n),s=1,t=2;
    for(int i=1,a;i<=n;i++) scanf("%d",&a),ans+=a,add(s,i+2,a),add(i+2,s,0);
    for(int i=1,a;i<=n;i++) scanf("%d",&a),ans+=a,add(i+2+n,t,a),add(t,i+2+n,0);
    for(int i=1;i<=n;i++) add(i+2,i+n+2,inf),add(i+n+2,i+2,0);
    scanf("%d",&m);
    for(int i=1,a,b,c;i<=m;i++)
    {
        scanf("%d%d%d",&a,&b,&c),ans+=b+c;
        add(s,i+n*2+2,b),add(i+n*2+2,s,0);
        add(i+n*2+m+2,t,c),add(t,i+n*2+m+2,0);
        for(int j=1,d;j<=a;j++) scanf("%d",&d),
            add(i+n*2+2,d+2,inf),add(d+2,i+n*2+2,0),
            add(d+2+n,i+n*2+2+m,inf),add(i+n*2+2+m,d+2+n,0);
    } dinic(),printf("%d\n",ans);
}

  例题二:[bzoj 3894文理分科]

  分析与题解:对于这道题,就相当于上一道题目,把作物改成同学,$A$耕地改为文科,$B$耕地改为理科,每一个组合就是当前同学与他所有相邻的同学都选文科和理科。然后就是向上一道题一样建图就可以了。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
bool in(int x,int y) {return x&&x<=n&&y&&y<=m;}
int pla(int i,int j) {return (i-1)*m+j;}
int main()
{
    scanf("%d%d",&n,&m),s=n*m*4+1,t=n*m*4+2;
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
        add(pla(i,j),pla(i,j)+n*m,inf),add(pla(i,j)+n*m,pla(i,j),0);
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++) scanf("%d",&a),
        add(s,pla(i,j),a),add(pla(i,j),s,0),ans+=a;
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++) scanf("%d",&a),
        add(pla(i,j)+n*m,t,a),add(t,pla(i,j)+n*m,0),ans+=a;
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++) { scanf("%d",&a),
        add(s,pla(i,j)+n*m*2,a),add(pla(i,j)+n*m*2,s,0),ans+=a;
        for(int k=0;k<=4;k++) if(in(i+dir1[k],j+dir2[k]))
            add(pla(i,j)+n*m*2,pla(i+dir1[k],j+dir2[k]),inf),
            add(pla(i+dir1[k],j+dir2[k]),pla(i,j)+n*m*2,0);}
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++) { scanf("%d",&a),
        add(pla(i,j)+n*m*3,t,a),add(t,pla(i,j)+n*m*3,0),ans+=a;
        for(int k=0;k<=4;k++) if(in(i+dir1[k],j+dir2[k]))
            add(pla(i+dir1[k],j+dir2[k])+n*m,pla(i,j)+n*m*3,inf),
            add(pla(i,j)+n*m*3,pla(i+dir1[k],j+dir2[k])+n*m,0);}
    dinic(),printf("%d\n",ans);
}

  例题三:[bzoj 2127happiness]

 

  分析与题解:这就是例题二的限制条件进行一下小更改,对于每一个同学,不是要和相邻同学都选择一个才能有额外收益,而是只有一个一样就能有额外收益。

void dinic() {while(bfs()) memcpy(cur,head,sizeof(head)),ans-=dfs(s,inf);}
int main()
{
    scanf("%d%d",&n,&m),s=n*m+1,t=n*m+2;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&math[i][j]),ans+=2*math[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            scanf("%d",&chin[i][j]),ans+=2*chin[i][j];
    for(int i=1;i<n;i++)
        for(int j=1;j<=m;j++) 
            scanf("%d",&ea_ma_x[i][j]),ans+=2*ea_ma_x[i][j];
    for(int i=1;i<n;i++)
        for(int j=1;j<=m;j++) 
            scanf("%d",&ea_ch_x[i][j]),ans+=2*ea_ch_x[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<m;j++) 
            scanf("%d",&ea_ma_y[i][j]),ans+=2*ea_ma_y[i][j];
    for(int i=1;i<=n;i++)
        for(int j=1;j<m;j++) 
            scanf("%d",&ea_ch_y[i][j]),ans+=2*ea_ch_y[i][j];
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
    {
        add(s,find_id(i,j),math[i][j]*2+ea_ma_x[i-1][j]
            +ea_ma_x[i][j]+ea_ma_y[i][j-1]+ea_ma_y[i][j]);
        add(find_id(i,j),s,0);
        add(find_id(i,j),t,chin[i][j]*2+ea_ch_x[i-1][j]
            +ea_ch_x[i][j]+ea_ch_y[i][j-1]+ea_ch_y[i][j]);
        add(t,find_id(i,j),0);
        if(i!=n) add(find_id(i,j),find_id(i+1,j),ea_ch_x[i][j]+ea_ma_x[i][j]),
            add(find_id(i+1,j),find_id(i,j),ea_ch_x[i][j]+ea_ma_x[i][j]);
        if(j!=m) add(find_id(i,j),find_id(i,j+1),ea_ch_y[i][j]+ea_ma_y[i][j]),
            add(find_id(i,j+1),find_id(i,j),ea_ch_y[i][j]+ea_ma_y[i][j]);
    } dinic();
    printf("%d\n",ans/2);
}

七、最小割的几个小模型(六)——   黑白染色

  这类问题的限制条件:如果当前点$x$和$y$不同则会获得一个值,反之会付出一个代价。

  若当前问题能转化为二分图的问题,即能将所有的点分成两类。我们把这些点一类染成黑色,另一类染成白色。对于黑色我们正常连,对于白色我们进行翻转源汇,即原本应该连向$S$的边连向$T$,原本应该连向$T$的边连向$S$。这样我们就能将问题转化为,$x$和$y$在同一个集合中会获得一个收益,反之会付出一个代价。这样我们就直接在这两个点上进行连边。

  例题一:[bzoj 1324Exca王者之剑]&&[bzoj 1475方格取数]

  分析与题解:对于这个矩形,我们进行黑白染色,对于黑点,我们向$S$连一条边,边权为当前点的点权;对于白点,我们向$T$连一条边,边权为当前点的点权。对于两个相邻的点,我们在他们之间连一条边,边权为$inf$。对于建出来的图跑最小割,最后答案就是所有点的点权和减去最小割。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
bool in(int x,int y) {return x&&x<=n&&y&&y<=m;}
int pla(int i,int j) {return (i-1)*m+j;}
int main()
{
    scanf("%d",&n),m=n,s=n*m+1,t=n*m+2;
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++) { scanf("%d",&a),ans+=a;
        if((i+j)%2) add(s,pla(i,j),a),add(pla(i,j),s,0);
        else add(pla(i,j),t,a),add(t,pla(i,j),0); }
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
        if((i+j)%2) for(int k=1;k<=4;k++) if(in(i+dir1[k],j+dir2[k]))
            add(pla(i,j),pla(i+dir1[k],j+dir2[k]),inf),
            add(pla(i+dir1[k],j+dir2[k]),pla(i,j),0);
    dinic(),printf("%d\n",ans);
}

  例题二:[bzoj 2132圈地计划]

  分析与讲解:对于整个图我们进行黑白染色,对于黑色的点,我们把其和$S$相连,边权为当前点开发成为商业区的收益,和$T$相连,边权为当前点开发成为工业区的收益,反之我们和$S$相连,边权为当前点开发成为工业区的收益,和$T$相连,边权为当前点开发成为商业区的收益。因为我们将其进行黑白染色之后翻转源汇,所以两个相邻的土地若开发成为不同的类型会在同一个集合,这样我们就能在相邻的两个点之间进行连线,因为这两个点如果不同会分别贡献两个值,所以这两个点之间的边为双向边,边权为这两个值相加。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
int pla(int i,int j) {return (i-1)*m+j;}
int main()
{
    scanf("%d%d",&n,&m),s=n*m+1,t=n*m+2;
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++)
    {
        scanf("%d",&a),ans+=a;
        if((i+j)%2) add(s,pla(i,j),a),add(pla(i,j),s,0);
        else add(pla(i,j),t,a),add(t,pla(i,j),0);
    }
    for(int i=1;i<=n;i++) for(int j=1,a;j<=m;j++)
    {
        scanf("%d",&a),ans+=a;
        if((i+j)%2) add(pla(i,j),t,a),add(t,pla(i,j),0);
        else add(s,pla(i,j),a),add(pla(i,j),s,0);
    }
    for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) scanf("%d",&c[i][j]);
    for(int i=1;i<=n;i++) for(int j=1;j<m;j++) ans+=c[i][j]+c[i][j+1],
        add(pla(i,j),pla(i,j+1),c[i][j]+c[i][j+1]),
        add(pla(i,j+1),pla(i,j),c[i][j]+c[i][j+1]);
    for(int i=1;i<n;i++) for(int j=1;j<=m;j++) ans+=c[i][j]+c[i+1][j],
        add(pla(i,j),pla(i+1,j),c[i][j]+c[i+1][j]),
        add(pla(i+1,j),pla(i,j),c[i][j]+c[i+1][j]);
    dinic(),printf("%d\n",ans);
}

  例题三:[bzoj 3275Number]

  分析与题解:对于两个数字,如果都是偶数,则他俩可以都选,因为这两个数的$gcd$不为$1$;对于都是奇数,也一定可以都选,因为这两个数的平方和不为一个数的平方,对于一个奇数的平方一定是$4$的倍数加$1$,两个奇数平方和就是$4$的倍数加$2$,因为一个数的平方一定是$4$的倍数或者是$4$的倍数加$1$,所以两个奇数的平方和一定不是另一个数的平方。所以我们对这些数字进行奇偶染色,所有的偶数连向$S$,边权为数字,反之所有的奇数连向$T$,边权为数字。在两个不能同时选择两个数之间连上一条边,边权为$inf$。答案就是所有数字和减去最小割。

void dinic() {while(bfs()) memcpy(cur,head,sizeof head),ans-=dfs(s,inf);}
long long squ(int x) {return 1ll*x*x;}
int main()
{
    scanf("%d",&n),s=n+1,t=n+2;
    for(int i=1;i<=n;i++) { scanf("%d",&num[i]),ans+=num[i];
        if(num[i]%2) add(s,i,num[i]),add(i,s,0);
        else add(i,t,num[i]),add(t,i,0); }
    for(int i=1;i<=n;i++) for(int j=i+1;j<=n;j++)
        if(i!=j&&__gcd(num[i],num[j])==1&&
            squ((int)sqrt(num[i]*num[i]+num[j]*num[j]))==num[i]*num[i]+num[j]*num[j])
        {
            if(num[i]%2) add(i,j,inf),add(j,i,0);
            else add(j,i,inf),add(i,j,0);
        }
    dinic(),printf("%d\n",ans);
}

  

posted @ 2018-12-24 10:18  Yang1208  阅读(6662)  评论(1编辑  收藏  举报