最小割淺談


最小割淺談

注:此淺談中借用了一些博客和課件中的理論,模型描述。

一、最小割問題

 

  最小割問題是指:給出一種有向圖(無向圖)和兩個點$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);
}

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM