【圖論】網絡流問題——最大流入門(Dinic算法)

參考文章:
1. 博客園:Dinic算法(研究總結,網絡流)
2. 洛谷博客:網絡最大流-從入門開始,詳細講到實用易懂的Dinic算法

本文主要是用 Dinic算法 解決最大流問題。

洛谷 P3376 【模板】網絡最大流

最大流問題,Dinic算法,最壞時間複雜度爲O(V2*E),然而一般情況下是達不到這麼大的,要不這題早超時了。

算法流程
1、根據殘量網絡計算層次圖。
2、在層次圖中使用DFS進行增廣直到不存在增廣路。
3、重複以上步驟直到無法增廣。

時間複雜度
因爲在Dinic的執行過程中,每次重新分層,匯點所在的層次是嚴格遞增的,而n個點的層次圖最多有n層,所以最多重新分層n次。假設總共有m條邊,在同一個層次圖中,因爲每條增廣路都有一個瓶頸,而兩次增廣的瓶頸不可能相同,所以增廣路最多m條。搜索每一條增廣路時,前進和回溯都最多n次,所以這兩者造成的時間複雜度是O(nm);綜上所述,Dinic算法時間複雜度的理論上界是O(n*nm)=O(n2*m)。

簡單來說,Dinic算法就是先進行BFS,判斷是否有增廣路(能從源點s達到匯點t)並且記錄圖中每個點所在的廣度優先搜索生成樹的層數,形成分層圖;如果有增廣路,則進行DFS,限制只能從層數小的向層數大的拓展,如果能找到終點並且返回時流量flow大於0,則每條正向邊減去增廣路中的最小流量,每條反向邊加上增廣路中的最小流量(這是算法的關鍵,引入反向邊就是爲了“反悔”,或者說“糾正之前可能錯誤的流量路徑”)。

以下的代碼,是BFS判斷是否有增廣路,DFS每次只能搜索出一條增廣路。BFS分層之後可以多次循環DFS。

#include <bits/stdc++.h>
using namespace std;
//在普通情況下,DINIC算法時間複雜度爲O(V^2*E)
const int N=1e4+10,M=1e5+10,inf=1e9;
int n,m,s,t,cnt,head[N],dep[N];
struct edge
{
    int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
void add_edge(int x,int y,int z)
{
    add(x,y,z);
    add(y,x,0);//反向邊初始爲0
}
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}
int dfs(int u,int flow)
{
    if(u==t)return flow;//到達匯點,返回這條增廣路上的最小流量
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起點到v的最小流量
            if(di>0)//防止dfs結果return 0的情況,如果di=0會死循環
            {
                e[i].w-=di;
                e[i^1].w+=di;//反向邊加上di
                return di;//di表示整條增廣路上的最小流量,回溯的時候一直向上返回,返回的di是不變的
            }
        }
    }
    return 0;//找不到增廣路,到不了匯點
}
int dinic()
{
    int ans=0;
    while(bfs())//先bfs進行“探路”並分層,分層後進行多次dfs
    {
        //每dfs一次,就嘗試找到一條增廣路並返回路上的最小流量
        while(int d=dfs(s,inf))//while循環的意義:只要dfs不爲0,就一直dfs,直到找不到增廣路
            ans+=d;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>n>>m>>s>>t;
    int x,y,z;
    memset(head,-1,sizeof(head));
    for(int i=1;i<=m;i++)
    {
        cin>>x>>y>>z;
        add_edge(x,y,z);
    }
    int ans=dinic();
    printf("%d\n",ans);
    return 0;
}

HDU 4280 Island Transport

模板題,但是Dinic算法需要優化才能過這題。

(1)加邊優化:題目是雙向邊,所以加反向邊的時候加的容量直接寫成與正向邊相同(而不是0),這樣每次就只加了2條邊。
這個加邊優化必須要寫。

void add_edge(int x,int y,int z)
{
    add(x,y,z);
    add(y,x,z);//而不是add(y,x,0);
}

(2)優化DFS:

  1. 當前弧優化,for(int &i=cur[u];i!=-1;i=e[i].next)
  2. 多路增廣,不直接返回一條弧的流量,而是把所有弧都跑完或者沒有流量後再返回res
  3. 炸點,使沒有流量(不能再向後流出)的點不會再被訪問,標記爲dep[u]=-2
//只優化DFS 時間:9516ms
int dfs(int u,int flow)
{
    if(u==t||flow==0)return flow;//到達匯點或者是沒有流量,就返回流量
    int res=0;
    for(int &i=cur[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起點到v的最小流量
            if(!di)continue;
            e[i].w-=di;
            e[i^1].w+=di;//反向邊加上di
            res+=di;//res相當於從v點流出了多少流量(當前的流出流量作爲後面點的供給)
            flow-=di;//flow相當於還可以流入多少流量到v點(給當前點的供給)
            //多路增廣,不直接返回一條弧的流量,而是把所有弧都跑完或者沒有流量後再返回res
            if(flow==0)break;
        }
    }
    if(res==0)dep[u]=-2;//炸點,使沒有流量(不能再向後流出)的點不會再被訪問
    //不寫這句話就會超時!
    return res;
}

(3)還有一種玄學優化,就是優化BFS:在BFS中找到匯點就直接結束。(這個就相當於找到終點就不更新分層圖了,我感覺好像有點問題,但是幾個OJ測了都沒問題)

//只優化BFS 時間:8486ms
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        if(u==t)return 1;//這句話非常重要!!!
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}

(4) STL中的queue用數組模擬,能快400ms左右,在這題用處不大。

AC代碼:

//BFS,DFS同時優化 未優化STL隊列 時間:7675ms
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10,M=1e5+10,inf=0x7f7f7f7f;
int T,n,m,s,t,cnt,head[N],dep[N],cur[N];
struct edge
{
    int to,w,next;
}e[M<<1];
inline void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
inline void add_edge(int x,int y,int z)
{
    add(x,y,z);
    add(y,x,z);
}
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}
int dfs(int u,int flow)
{
    if(u==t||flow==0)return flow;//到達匯點或者是沒有流量,就返回流量
    int res=0;
    for(int &i=cur[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起點到v的最小流量
            if(!di)continue;
            e[i].w-=di;
            e[i^1].w+=di;//反向邊加上di
            res+=di;//res相當於從v點流出了多少流量(當前的流出流量作爲後面點的供給)
            flow-=di;//flow相當於還可以流入多少流量到v點(給當前點的供給)
            //多路增廣,不直接返回一條弧的流量,而是把所有弧都跑完或者沒有流量後再返回res
            if(flow==0)break;
        }
    }
    if(res==0)dep[u]=-2;//炸點,使沒有流量(不能再向後流出)的點不會再被訪問
    //不寫這句話就會超時!
    return res;
}
int dinic()
{
    int ans=0;
    while(bfs())//先bfs進行“探路”並分層,分層後進行多次dfs
    {
        for(int i=1;i<=n;i++)//比這個memcpy(cur,head,sizeof(head)) 要快一點
            cur[i]=head[i];
        while(int d=dfs(s,inf))//while循環的意義:只要dfs不爲0,就一直dfs,直到找不到增廣路
            ans+=d;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>T;
    while(T--)
    {
        cnt=0;
        memset(head,-1,sizeof(head));
        cin>>n>>m;
        int x,y;
        int mi=inf,mx=-inf;
        for(int i=1;i<=n;i++)
        {
            cin>>x>>y;
            if(x<mi)//找橫座標最小的點,是源點
            {
                mi=x;
                s=i;
            }
            if(x>mx)//找橫座標最大的點,是匯點
            {
                mx=x;
                t=i;
            }
        }
        int a,b,w;
        for(int i=1;i<=m;i++)
        {
            cin>>a>>b>>w;
            add_edge(a,b,w);
        }
        int ans=dinic();
        printf("%d\n",ans);
    }
    return 0;
}
/*
2
5 7
4 5
3 1
3 3
3 0
0 0
1 3 3
2 3 4
2 4 3
1 5 6
4 5 3
1 4 4
3 4 2

ans:9
*/

POJ 3281 Dining

我覺得這題的難點就是建圖,以及把這個看起來像二分圖匹配的問題轉化爲最大流問題(不看題解,真以爲是二分圖匹配)。

限制條件:每種食物和水都只能被一頭牛用一次,求最多能餵飽多少頭牛。

把一頭牛拆成兩頭(你沒看錯,就是這種拆點的神奇操作),然後建圖:食物->牛->牛->飲料,最後再加一個超級源點連上每種食物,加一個超級匯點連上每種飲料,圖中的任意一條增廣路就變成了:超級源點->食物->牛(左)->牛(右)->飲料->超級匯點。

建立網絡流模型:
1.對每種食物建立從源點指向它的一條邊,流量爲1
2.在牛與它所喜愛的食物間建立一條邊,流量爲1
3.在牛與它所喜歡的飲料間建立一條邊,流量爲1
4.對每種飲料建立一條指向匯點的邊,流量爲1
5.在上面的基礎上,將牛拆點,在拆開的點間建立一條流量爲1的邊
在以上條件下,從源點到匯點的最大流即爲答案

爲什麼要這樣建圖?原因如下:

模型的分析:
條件1使得滿足每種食物有且只有一個,條件4類似
條件2使得每頭牛隻能選擇自己喜歡的食物,條件3類似
條件5使得每頭牛最多隻能選擇一種飲料和食物

還注意一個小細節:對牛的編號、食物編號、飲料編號要進行處理,防止它們編號重合。設食物 f 種,飲料 d 種,牛 n 頭,則設左邊牛的編號爲[1,n]不變,拆點得到的右邊牛的編號在原有基礎上 +n ,食物編號在原有基礎上 +2n ,飲料編號在原有基礎上 +2n+f 。源點 s 編號爲0,匯點 t 編號爲2n+f+d+1。

AC代碼:

//#include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N=1e3+10,M=1e5+10,inf=1e9;
bool vis1[N],vis2[N];
int n,s,t,cnt,head[N],dep[N];
struct edge
{
    int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
void add_edge(int x,int y,int z)//加正向邊和初始的反向邊
{
    add(x,y,z);
    add(y,x,0);
}
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}
int dfs(int u,int flow)
{
    if(u==t)return flow;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));
            if(di>0)
            {
                e[i].w-=di;
                e[i^1].w+=di;
                return di;
            }
        }
    }
    return 0;
}
int dinic()
{
    int ans=0;
    while(bfs())
    {
        while(int d=dfs(s,inf))
            ans+=d;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    memset(head,-1,sizeof(head));
    int f,d,q1,q2,x,y;
    cin>>n>>f>>d;
    s=0,t=2*n+f+d+1;
    for(int i=1;i<=n;i++)
    {
        cin>>q1>>q2;
        int cow1=i;//左邊牛的編號
        int cow2=i+n;//右邊牛的編號(拆點)
        while(q1--)
        {
            cin>>x;//食物
            x+=2*n;
            if(!vis1[x])
            {
                add_edge(s,x,1);//源點與食物連邊
                vis1[x]=1;
            }
            add_edge(x,cow1,1);//食物與牛連邊
        }
        add_edge(cow1,cow2,1);//牛與牛自身連邊
        while(q2--)
        {
            cin>>y;//飲料
            y+=2*n+f;
            add_edge(cow2,y,1);//牛與飲料連邊
            if(!vis2[y])
            {
                vis2[y]=1;
                add_edge(y,t,1);//飲料與匯點連邊
            }
        }
    }
    int ans=dinic();
    printf("%d\n",ans);
    return 0;
}

POJ 3436 ACM Computer Factory

題意:

有N臺組裝電腦的機器。電腦的組成部分共有P部分。
每臺機器有P個輸入輸出規格。還有一個容量Q;
其中輸入規格有三種情況:0,1,2
0:該部分不能存在
1:該部分必須保留
2:該部分可有可無
輸出規格有2種情況:0,1
0:該部分不存在
1:該部分存在
要求的是生產電腦最大的臺數,就是求網絡中的最大流。

建圖:

  1. 要增加源點和匯點。輸入沒有1的連接源點,輸出全部是1的連接匯點。
  2. 考慮到每臺機器都有容量,所以把一臺機器分成兩個點,中間建一條容量的邊。
  3. 如果一臺機器的輸出符合另一臺機器的輸入(只要不是0->1和1->0就符合),則建一條容量無窮大的邊。

最後考慮處理輸出邊數問題,只要考慮退回邊和原邊的關係,退回邊的權值就是原邊流過的流量值,那麼遍歷所有反向邊(邊的編號爲奇數的),考慮其是否>0,如果是,則說明這條邊有流流過,那麼對應將其記錄下來一起輸出即可。

這題wa了無數次,最後發現是輸出的順序錯了,寫成了原料到產出,答案必須是寫產出到原料,我以爲這個順序沒關係(If several solutions exist, output any of them. 題意應該是說答案的行的順序可以互換,沒說行的內部順序可以寫反)

AC代碼:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
const int N=52,M=1e5+10,P=12,inf=2e9;
int s,t,p,n,w,num,cnt,head[N],dep[N];
int a[N][P],b[N][P];//兩個下標是行號和列號
struct node
{
    int u,v,w;
};
vector<node>path;
struct edge
{
    int from,to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
    e[cnt].from=x;
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
void add_edge(int x,int y,int z)//加正向邊和初始的反向邊
{
    add(x,y,z);
    add(y,x,0);
}
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}
int dfs(int u,int flow)
{
    if(u==t)return flow;
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));
            if(di>0)
            {
                e[i].w-=di;
                e[i^1].w+=di;
                return di;
            }
        }
    }
    return 0;
}
int dinic()
{
    int ans=0;
    while(bfs())
    {
        while(int d=dfs(s,inf))
            ans+=d;
    }
    return ans;
}
bool judge(int numa,int numb)//判斷從某個機器的產出能否連到其他機器的原料
{
    for(int i=1;i<=p;i++)
    {
        int x=a[numa][i];
        int y=b[numb][i];
        if((x==0&&y==1)||(x==1&&y==0))return 0;
    }
    return 1;
}
int main()
{
    ios::sync_with_stdio(false);
    cin>>p>>n;
    memset(head,-1,sizeof(head));
    s=0,t=2*n+1;
    for(int i=1;i<=n;i++)
    {
        cin>>w;
        int sum1=0;//統計每個原料結點或者產出結點中1的個數
        int numa=i;//原料結點編號
        int numb=i+n;//產出結點編號
        for(int j=1;j<=p;j++)
        {
            cin>>a[i][j];
            if(a[i][j]==1)sum1++;
        }
        if(sum1==0)add_edge(s,numa,inf);//將源點與不存在1的原料結點連邊
        add_edge(numa,numb,w);//將同一機器的原料結點和產出結點連邊
        sum1=0;
        for(int j=1;j<=p;j++)
        {
            cin>>b[i][j];
            if(b[i][j]==1)sum1++;
        }
        if(sum1==p)add_edge(numb,t,inf);//將全是1的產出結點與匯點連邊
    }
    for(int i=1;i<=n;i++)//原料結點行號
    {
        for(int j=1;j<=n;j++)//產出結點行號
        {
            if(i==j)continue;
            if(judge(i,j))
            {
                //printf("link edge j=%d i=%d\n",j,i);
                add_edge(j+n,i,inf);//將機器j的產出結點連到機器i的原料結點
            }
        }
    }
    int ans=dinic();
    printf("%d ",ans);
    for(int i=1;i<=cnt;i+=2)//遍歷反向邊
    {
        int from=e[i].from;
        int to=e[i].to;
        int w=e[i].w;
        if(w>0&&from>=1&&from<=n&&to>=n+1&&to<=2*n&&from!=to-n)
            path.push_back({to-n,from,w});
            //就這個地方我寫成了path.push_back({from,to-n,w}),wa了無數次!
    }
    printf("%d\n",path.size());
    for(int i=0;i<path.size();i++)
        printf("%d %d %d\n",path[i].u,path[i].v,path[i].w);
    return 0;
}
/*
3 4
15  0 0 0  0 1 0
10  0 0 0  0 1 1
30  0 1 2  1 1 1
3   0 2 1  1 1 1

ans:
25 3
1 3 15(不能寫成3 1 15)
2 3 7
2 4 3
*/

POJ 1087 A Plug for UNIX

題意:

  1. 在一個會議室裏有n個插座(可能重複);

  2. 每個插座只能插一個電器的插頭(或者適配器);

  3. 有m個電器,每個電器有一個插頭需要插在相應一種插座上;

  4. 不是所有電器都能在會議室找到相應插座;

  5. 有k種適配器,每種適配器有無限多數量;

  6. 每種適配器(s1, s2)可以把s1類插座變爲s2類插座;

  7. 問最後最少有多少個電器無法使用。

建圖:

  1. 各個電器各自爲一個節點,和源點(不存在,自己構造)相連,且源點到電器的容量爲1。

  2. 將室內已有的插座各自爲一個節點,和匯點(不存在,自己構造)相連,且到匯點的容量爲1。

  3. 將電器到其應該插的插座類型的點做邊,且容量爲1。

  4. 將適配器(a, b)從a點到b點做邊,且邊的容量爲INF。

需要注意,在輸入電器以及輸入適配器的過程中,如果遇到室內沒有的插座類型,則需要在圖中添加新的節點。

樣例輸入:

4
A
B
C
D
5
laptop B
phone C
pager B
clock B
comb X
3
B X
X A
X D

畫圖理解樣例:
在這裏插入圖片描述

AC代碼:

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <map>
#include <queue>
using namespace std;
const int N=1e4,M=1e5+10,inf=0x7f7f7f7f;
int n,m,k,s,t,cnt,head[N+10],dep[N+10];
map<string,int>vis;//存放電器的插頭和提供的插座(包括適配器轉換得到的插座)
struct edge
{
    int to,w,next;
}e[M<<1];
void add(int x,int y,int z)
{
    e[cnt].to=y;
    e[cnt].w=z;
    e[cnt].next=head[x];
    head[x]=cnt++;
}
void add_edge(int x,int y,int z)
{
    add(x,y,z);
    add(y,x,0);
}
bool bfs()
{
    queue<int>q;
    memset(dep,0,sizeof(dep));
    q.push(s);
    dep[s]=1;
    while(!q.empty())
    {
        int u=q.front();q.pop();
        for(int i=head[u];i!=-1;i=e[i].next)
        {
            int v=e[i].to;
            if(e[i].w>0&&dep[v]==0)
            {
                dep[v]=dep[u]+1;
                q.push(v);
            }
        }
    }
    if(dep[t])return 1;
    return 0;
}
int dfs(int u,int flow)
{
    if(u==t)return flow;//到達匯點,返回這條增廣路上的最小流量
    for(int i=head[u];i!=-1;i=e[i].next)
    {
        int v=e[i].to;
        if(e[i].w>0&&dep[v]==dep[u]+1)
        {
            int di=dfs(v,min(flow,e[i].w));//min(flow,e[i].w)表示起點到v的最小流量
            if(di>0)//防止dfs結果return 0的情況,如果di=0會死循環
            {
                e[i].w-=di;
                e[i^1].w+=di;//反向邊加上di
                return di;//di表示整條增廣路上的最小流量,回溯的時候一直向上返回,返回的di是不變的
            }
        }
    }
    return 0;//找不到增廣路,到不了匯點
}
int dinic()
{
    int ans=0;
    while(bfs())//先bfs進行“探路”並分層,分層後進行多次dfs
    {
        //每dfs一次,就嘗試找到一條增廣路並返回路上的最小流量
        while(int d=dfs(s,inf))//while循環的意義:只要dfs不爲0,就一直dfs,直到找不到增廣路
            ans+=d;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(false);
    string s1,s2;
    memset(head,-1,sizeof(head));
    s=0,t=N;//源點s,匯點t(匯點編號大於所有點的個數就行)
    cin>>n;
    int num=0;
    for(int i=1;i<=n;i++)//遍歷插座(可能有重複名字)
    {
        cin>>s1;//已提供的插座名字s1
        if(!vis[s1])vis[s1]=++num;
        //合併相同名字的插座,只留插座第一次出現的名字,其對應的編號爲vis[s1]
        add_edge(vis[s1],t,1);//插座與匯點連邊,容量爲1
    }
    cin>>m;
    for(int i=1;i<=m;i++)//遍歷插頭(可能有重複)
    {
        cin>>s1>>s2;//電器名s1,電器對應的插頭名s2
        if(!vis[s2])vis[s2]=++num;
        add_edge(s,vis[s2],1);//源點與插頭連邊,容量爲1
    }
    cin>>k;
    for(int i=1;i<=k;i++)
    {
        cin>>s1>>s2;//插座s1可以轉換到插座s2
        if(!vis[s1])vis[s1]=++num;//已經存在的名字中找不到s1,則將s1插入
        if(!vis[s2])vis[s2]=++num;
        //printf("link edge %d %d\n",vis[s1],vis[s2]);
        add_edge(vis[s1],vis[s2],inf);//將可以轉換的兩個插座相連,容量爲無限
    }
    int maxflow=dinic();
    printf("%d\n",m-maxflow);
    return 0;
}
/*
16
A
D
X
A
D
A
D
X
D
A
D
D
X
D
X
D
14
CLOCK B
CLOCK B
CLOCK B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
LAPTOP B
PAGER B
PAGER B
COMB X
CELL C
CELL C
4
C D
X D
B X
B A

ans:0
*/

UPD 2020.4.12

LOJ 127 / 洛谷 P4722【模板】最大流 加強版 / 預流推進

解決最大流問題有很多算法,諸如EK,Dinic,ISAP,HLPP等等。
今天看到了一篇很好的文章,比對了各個算法的適用場景和複雜度,可以看一下這篇博客:P4722 -【模板】最大流 加強版 / 預流推進

一般來說,Dinic算法是夠用的,但是這個加強版的題數據很毒瘤,得用HLPP。

我先挖個坑,看什麼時候HLPP算法給填了

看到一個用Dinic優化了一下卡過去的,(不是我寫的)代碼:

//C++ 17
#include<bits/stdc++.h>
using namespace std;
const int maxn=1210,maxm=120010;
struct edge
{
    int u,v,cap;
}e[maxm];
struct Dinic
{
    int tp,s,t,dis[maxn],cur[maxn],que[maxn];
    vector<edge>e;vector<int>v[maxn];
    void AddEdge(int x,int y,int flw)
    {
        e.push_back(edge{x,y,flw});
        e.push_back(edge{y,x,0});
        v[x].push_back(e.size()-2);
    }
    int bfs()
    {
        memset(dis,0x3f,sizeof(dis));
        int l=1,r=1;que[1]=s;dis[s]=0;
        while(l<=r)
        {
            int p=que[l++],to;
            for(int i:v[p])if(e[i].cap&&dis[to=e[i].v]>1e9)
                dis[to]=dis[p]+1,que[++r]=to;
        }
        return dis[t]<1e9;
    }
    int dfs(int p,int a)
    {
        if(p==t||!a)return a;
        int sf=0,flw;
        for(int &i=cur[p],to;i<(int)v[p].size();++i)
        {
            edge &E=e[v[p][i]];
            if(dis[to=E.v]==dis[p]+1&&(flw=dfs(to,min(a,E.cap))))
            {
                E.cap-=flw;e[v[p][i]^1].cap+=flw;
                a-=flw;sf+=flw;
                if(!a)break;
            }
        }
        return sf;
    }
    int dinic(int s,int t,int tp=1)
    {
        this->s=s;this->t=t;this->tp=tp;
        int flw=0;
        while(bfs())
        {
            memset(cur,0,sizeof(cur));
            flw+=dfs(s,INT_MAX);
        }
        return flw;
    }
}sol;
int n,m,i,s,t,ans;
int main(){
    scanf("%d%d%d%d",&n,&m,&s,&t);
    for(i=0;i<m;i++)scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].cap);
    sort(e,e+m,[](edge a,edge b){return a.cap>b.cap;});
    for(int tp:{0,1})
        for(int p=1<<30,i=0;p;p/=2)
        {
            while(i<m&&e[i].cap>=p)
            {
                if(tp)sol.v[e[i].v].push_back(i*2+1);
                else sol.AddEdge(e[i].u,e[i].v,e[i].cap);
                i++;
            }
            ans+=sol.dinic(s,t,tp);
        }
    printf("%d\n",ans);
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章