1. 程式人生 > >圖論相關知識

圖論相關知識

簡單介紹

就我2018年暑假這陣子練過的區域賽題目來看

  • 圖論題網路流居多,一般是稍難的簽到(需要多做點網路流的題目)
  • 另外由於DAG的性質,很容易的能夠有一些經典的DP,也可以注意一下。
  • 其他的主要還是會套模板吧。
    一定要理解圖論演算法的核心思想以及一些規律,比較難的題目(銅牌往上)可能就是這樣考
  • 其他的題目就見地不多了。可能很難,都做不到。

圖論知識以及模板程式碼

0、前向星

const int N=1e3+10;
const int M=2*N;
int tot,head[N];
void init(){
    tot=0;memset(head,-1,sizeof head);
}
struct Edge{
    int to,next;
}edge[M];
void addedge(int u,int v){
    edge[tot].to=v;edge[tot].next=head[u];
    head[u]=tot++;
}

1、拓撲排序

  • 注意,DAG才存在topo排序,
bool vis[N];
int ind[N];
int que[N];
void topo(int root){
    int q=0,p=0;//佇列指標
    que[q++]=root;
    while(p<q){
        int u=que[p++];
        for(int i=head[u];~i;i=edge[i].next){
            int v=edge[i].to;
            if(!vis[v]){
                ind[v]--;
                if(ind[v]==0){
                    vis[v]=true;
                    que[q++]=v;
                }
            }
        }
    }
}

  • 使用DAG的拓撲序求最短路

    實際上就是BF的鬆弛操作,不過由於DAG的性質,可以降低複雜度到m。並且可以處理負邊權。

int dist[N];
Rep(i,1,n)dist[i]=INF;
dist[s]=0;
for(int i=0;i<n;i++){
    int v;
    for(int j=head[que[i]];~j;j=edge[j].next){
        v=edge[j].to;
        if(dist[v]>dist[u]+edge[j].w)dist[v]=dist[u]+edge[j].w;
    }
}

2、生成樹

(1)最小生成樹

(2)次小生成樹

前兩個kuangbin 模板有

(3)有向圖的最小樹形圖

  1. 首先我們來考慮一下DAG的最小樹形圖

    對於每個點來說,連通這個點的最小花費,就是找一條最小的邊到這個點。

    思考一下DAG和拓撲排序,按照拓撲序來考慮每一個點,其都能通過選擇任何一條前驅邊使這個點連通。所以我們選擇最小的那條邊。

    所以就使用拓撲序遍歷所有遍,然後去更新連通每個點的貢獻

  2. 如果不是DAG的話,除非能縮點,否則只能用下面的板子了。

//來自kuangbin的最小樹形圖模版:
//UVA - 11183 
//最小樹形圖 求有向圖的最小生成樹
//複雜度 O(VE)
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int INF = 0x3f3f3f3f;
const int MAXN = 1000+10;
const int MAXM = 40000+10;

struct Edge{
    //分別為起點,終點,花費 
    int u,v,cost;
};
Edge edge[MAXM];
//pre[i]表示i節點的入邊的起點,in[i]表示該邊的權值 
int pre[MAXN],id[MAXN],visit[MAXN],in[MAXN];

//root為根節點,n為節點數量,m為邊數量 
int zhuliu(int root,int n,int m,Edge edge[]){
    //最小樹形圖的總權值 
    int res=0,u,v;
    while(1){
        //找每個節點的最小入邊
        //初始化所有入邊邊權無窮大 
        for(int i=0;i<n;i++)
            in[i]=INF;
        //對於每個邊 
        for(int i=0;i<m;i++)
            //如果該邊不是自環邊,該邊的終點v頂點的入邊邊權比這條邊大,那麼這條邊作為v頂點的入邊 
            if(edge[i].u!=edge[i].v && edge[i].cost<in[edge[i].v]){
                //入邊的起始頂點 
                pre[edge[i].v]=edge[i].u;
                //入邊的邊權 
                in[edge[i].v]=edge[i].cost;
            }
        //如果有除根節點以外的點的入邊邊權無窮大,那麼不存在最小樹形圖 
        for(int i=0;i<n;i++)
            if(i!=root && in[i]==INF)
                return -1;

        //找環
        //環的數量 
        int tn=0;
        memset(id,-1,sizeof(id));
        memset(visit,-1,sizeof(visit));
        in[root]=0;
        //標記每個環 
        for(int i=0;i<n;i++){
            //記錄權值 
            res+=in[i];
            v=i;
            //尋找v節點所在的環
            //visit保證不會無限迴圈,並且用i標記了該環是那個所有的頂點 
            while(visit[v]!=i && id[v]==-1 && v!=root){
                visit[v]=i;
                v=pre[v];
            }
            //標記環上的頂點是屬於第tn個環 
            if(v!=root && id[v]==-1){
                for(int u=pre[v];u!=v;u=pre[u])
                    id[u]=tn;
                id[v]=tn++;
            }
        }
        //無環,當前生成樹就是最小樹形圖 
        if(tn==0) break;

        //有環建立新圖 
        for(int i=0;i<n;i++)
            if(id[i]==-1)
                id[i]=tn++;
        for(int i=0;i<m;){
            v=edge[i].v;
            //用環號代替起始點,邊是建立在兩個還之間 
            edge[i].u=id[edge[i].u];
            edge[i].v=id[edge[i].v];
            //i邊的權值要減去v所在環的入邊權值 
            if(edge[i].u!=edge[i].v)
                edge[i++].cost-=in[v];
            //i邊在連線的是同一個環裡面的連個節點,該邊捨去 
            else
                swap(edge[i],edge[--m]);
        }
        //對新圖求最小樹形圖
        //新圖的節點數量 
        n=tn;
        //新圖的根節點位置,縮點所在的位置 
        root=id[root];
    }
    //最小樹形圖的權值 
    return res;
}

int g[MAXN][MAXN];
int main(){
    int n,m;
    int t;
    scanf("%d",&t);
    for(int casei=1;casei<=t;casei++){
        scanf("%d%d",&n,&m);
        //初始化 
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
                g[i][j]=INF;


        int u,v,cost;
        while(m--){
            scanf("%d%d%d",&u,&v,&cost);
            if(u==v)continue;
            g[u][v]=min(g[u][v],cost);
        }


        int L=0;
        for(int i=0;i<n;i++)
            for(int j=0;j<n;j++)
            if(g[i][j]<INF){
                edge[L].u=i;
                edge[L].v=j;
                edge[L++].cost=g[i][j];
            }

        //引數分別為根節點,總結點數邊數,邊集合 
        int ans=zhuliu(0,n,L,edge);

        printf("Case #%d: ",casei);
        if(ans==-1)
            printf("Possums!\n");
        else
            printf("%d\n",ans);
    }
}

2.5、最小點基,最小權點基

針對有向圖的一個支配集,當然也不算支配集,
從這個點集合出發,可以到達有向圖的任意一個點。稱作為一個點基本

我們只要從所有的最高強連通分量中各選一個點組成集合就能組成 **最小點基 **
取權值最小的就是最小權點基

縮點後重新建邊就可以了

3、最短路

  • dij mlogn

  • spfa mk

  • floyd n^3

Floyd求最小環

Floyd基於鬆弛的動態規劃
針對每一個k去鬆弛i-j的路徑。

for(k,1,n)for(i,1,n)for(j,1,n)if(可鬆弛)鬆弛

最小環為負就是有負環

const int MAXN = 110;
const int INF = 0xffffff0;
int temp,Map[MAXN][MAXN],Dist[MAXN][MAXN],pre[MAXN][MAXN],ans[MAXN*3];

void Solve(int i,int j,int k)
{
    temp = 0;   //回溯,儲存最小環
    while(i != j)
    {
        ans[temp++] = j;
        j = pre[i][j];
    }
    ans[temp++] = i;
    ans[temp++] = k;
}
void Floyd(int N)
{
    for(int i = 1; i <= N; ++i)
        for(int j = 1; j <= N; ++j)
        {
            Dist[i][j] = Map[i][j];
            pre[i][j] = i;
        }
    int MinCircle = INF;    //最小環
    for(int k = 1; k <= N; ++k)
    {
        for(int i = 1; i <= N; ++i)
        {
            for(int j = 1; j <= N; ++j)
            {
                if(i != j && Dist[i][j] != INF && Map[i][k] != INF && Map[k][j] != INF
                   && Dist[i][j] + Map[i][k] + Map[k][j] < MinCircle)
                {
                    MinCircle = min(MinCircle, Dist[i][j] + Map[i][k] + Map[k][j]);
                    Solve(i,j,k);   //回溯儲存最小環
                }
            }
        }

        for(int i = 1; i <= N; ++i)
        {
            for(int j = 1; j <= N; ++j)
            {
                if(Dist[i][k] != INF && Dist[k][j] != INF &&
                   Dist[i][k] + Dist[k][j] < Dist[i][j])
                {
                    Dist[i][j] = Dist[i][k] + Dist[k][j];
                    pre[i][j] = pre[k][j];  //記錄點i到點j的路徑上,j前邊的點
                }
            }
        }
    }

    if(MinCircle == INF)    //不存在環
    {
        printf("No solution.\n");
        return;
    }
    //如果求出最小環為負的,原圖必定存在負環
    for(int i = 0;i < temp; ++i)    //輸出最小環
        if(i != temp-1)
            printf("%d ",ans[i]);
        else
            printf("%d\n",ans[i]);
}

4、k短路

使用spfa+A*可以處理負邊權

#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int maxn=100010;
int n,m,dis[maxn];
int tot,head1[maxn],head2[maxn];
bool flag[maxn];
struct edge{
    int to,next,w;
}e[maxn*2],e2[maxn*2];
struct node{
    int from,f,g;
    bool operator < (node rhs)const{
        return rhs.f==f?g>rhs.g:f>rhs.f;
    }
};
void init(){
    memset(head1,-1,sizeof head1);
    memset(head2,-1,sizeof head2);
    tot=0;
    memset(flag,false,sizeof flag);
}
void add_edge(int u,int v,int w)
{
    e[tot].to=v;
    e[tot].w=w;
    e[tot].next=head1[u];
    head1[u]=tot;
    e2[tot].to=u;//建反圖
    e2[tot].w=w;
    e2[tot].next=head2[v];
    head2[v]=tot;
    tot++;
}
void spfa(int t)//反圖預處理dis
{
    for(int i=1;i<=n;i++)dis[i]=maxn;
    dis[t]=0;
    queue<int> q;q.push(t);
    flag[t]=1;
    while(!q.empty())
    {
        int v=q.front();
        q.pop();flag[v]=0;
        for(int i=head2[v];~i;i=e2[i].next)
        if(dis[e2[i].to]>dis[v]+e2[i].w)
        {
            dis[e2[i].to]=dis[v]+e2[i].w;
            if(!flag[e2[i].to])
            {
                q.push(e2[i].to);
                flag[e2[i].to]=1;
            }
        }
    }
}
int a_star(int s,int t,int k)
{
    if(s==t) return 0;
    if(dis[s]==maxn) return -1;
    priority_queue<node> q;
    int cnt=0;
    node tmp,to;
    tmp.from=s;
    tmp.g=0;
    tmp.f=tmp.g+dis[tmp.from];
    q.push(tmp);
    while(!q.empty())
    {
        tmp=q.top();
        q.pop();
        if(tmp.from==t) cnt++;
        if(cnt==k) return tmp.g;
        for(int i=head1[tmp.from];i;i=e[i].next)
        {
            to.from=e[i].to;
            to.g=tmp.g+e[i].w;
            to.f=to.g+dis[to.from];
            q.push(to);
        }
    }
    return -1;
}
int main()
{
    int x,y,z,s,t,k;
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>x>>y>>z;
        add_edge(x,y,z);
    }
    cin>>s>>t>>k;//輸入起點,終點,k短路
    spfa(t);
    int ans=a_star(s,t,k);
    cout<<ans;
    return 0;
}


5、支配集、覆蓋集、獨立集。

定義

支配集,就是支配所有的點。

點覆蓋,就是覆蓋所有的邊。

獨立集,就是集合內所有點互相獨立。

演算法

  • 貪心

首先是深度優先遍歷,得到遍歷序列。程式碼如下:

int p[maxn];
bool select[maxn];
int newpos[maxn];
int now;
int n,m;
void DFS(int x)
{
    newpos[now++]=x;
    int k;
    for(k=head[x];k!=-1;k=edge[k].next)
    {
        if(!select[edge[k].to])
        {
            select[edge[k].to]=true;
            p[edge[k].to]=x;
            DFS(edge[k].to);
        }
    }
}

對於最小支配集,貪心函式如下:

int greedy()
{
    bool s[maxn];
    bool set[maxn]={0};
    int ans=0;
    int i;
    for(i=n-1;i>=0;i--)
    {
        int t=newpos[i];
        if(!s[t])
        {
            if(!set[p[t]])
            {
                set[p[t]]=true;
                ans++;
            }
            s[t]=true;
            s[p[t]]=true;
            s[p[p[t]]]=true;
        }
    }
    return ans;
}

對於最小點覆蓋,貪心函式如下:

int greedy()
{
    bool s[maxn]={0};
    bool set[maxn]={0};
    int ans=0;
    int i;
    for(i=n-1;i>=1;i--)
    {
        int t=newpos[i];
        if(!s[t]&&s[p[t]])
        {
            set[p[t]]=true;
            ans++;
            s[t]=true;
            s[p[t]]=true;
        }
    }
    return ans;
}

對於最大獨立集,貪心函式如下:

int greedy()
{
    bool s[maxn]={0};
    bool set[maxn]={0};
    int ans=0;
    int i;
    for(i=n-1;i>=0;i--)
    {
        int t=newpos[i];
        if(!s[t])
        {
            set[t]=true;
            ans++;
            s[t]=true;
            s[p[t]]=true;
        }
    }
    return ans;
}
  • dp

最小支配集:

void DP(int u,int p)
{
    dp[u][2]=0;
    dp[u][0]=1;
    bool s=false;
    int sum=0,inc=INF;
    int k;
    for(k=head[u];k!=-1;k=edge[k].next)
    {
        int to=edge[k].to;
        if(to==p)continue;
        DP(to,u);
        dp[u][0]+=min(dp[to][0],min(dp[u][1],dp[u][2]));
        if(dp[to][0]<=dp[to][1])
        {
            sum+=dp[to][0];
            s=true;
        }
        else
        {
            sum+=dp[to][1];
            inc=min(inc,dp[to][0]-dp[to][1]);
        }
        if(dp[to][1]!=INF&&dp[u][2]!=INF)dp[u][2]+=dp[to][1];
        else dp[u][2]=INF;
    }
    if(inc==INF&&!s)dp[u][1]=INF;
    else 
    {
        dp[u][1]=sum;
        if(!s)dp[u][1]+=inc;
    }
}

最小點覆蓋:

void DP(int u,int p)
{
    dp[u][0]=1;
    dp[u][1]=0;
    int k,to;
    for(k=head[u];k!=-1;k=edge[k].next)
    {
        to=edge[k].to;
        if(to==p)continue;
        DP(to,u);
        dp[u][0]+=min(dp[to][0],dp[to][1]);
        dp[u][1]+=dp[to][0];
    }
}

最大獨立集:

void DP(int u,int p)
{
    dp[u][0]=1;
    dp[u][1]=0;
    int k,to;
    for(k=head[u];k!=-1;k=edge[k].next)
    {
        to=edge[k].to;
        if(to==p)continue;
        DP(to,u);
        dp[u][0]+=dp[u][1];
        dp[u][1]+=max(dp[to][0],dp[to][1]);
    }
}

6、無向圖的割點、橋和雙連通分支的基本概念

參考kuangbin模板

其他概念模板

  • 2-SAT
  • 旅行揹包問題

匈牙利演算法

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define MAXL 100000
#define MAX 500
#define INF 1000000000
inline int read()
{
    int x=0,t=1;char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
int n1,n2;
struct Line
{
    int v,next,w;
}e[MAXL];
int h[MAX],cnt;
inline void Add(int u,int v,int w)
{
    e[cnt]=(Line){v,h[u],w};h[u]=cnt++;
    e[cnt]=(Line){u,h[v],0};h[v]=cnt++;
}
int n,S,T;
int level[MAX];
bool BFS()
{
    memset(level,0,sizeof(level));
    queue<int> Q;
    Q.push(S);level[S]=1;
    while(!Q.empty())
    {
        int u=Q.front();Q.pop();
        for(int i=h[u];i!=-1;i=e[i].next)
        {
            int v=e[i].v;
            if(e[i].w&&!level[v])
                level[v]=level[u]+1,Q.push(v);
        }
    }
    return level[T];
}
int cur[MAX];
int DFS(int u,int flow)
{
    if(u==T||!flow)return flow;
    int ret=0;
    for(int &i=cur[u];i!=-1;i=e[i].next)
    {
        int v=e[i].v;
        if(e[i].w&&level[v]==level[u]+1)
        {
            int d=DFS(v,min(flow,e[i].w));
            ret+=d;flow-=d;
            e[i].w-=d;e[i^1].w+=d;
        }
    }
    if(!ret)level[u]=0;
    return ret;
}
int Dinic()
{
    int ret=0;
    while(BFS())
    {
        for(int i=S;i<=T;++i)cur[i]=h[i];
        ret+=DFS(S,INF);
    }
    return ret;
}
int main()
{
    freopen("flyer.in","r",stdin);
    freopen("flyer.out","w",stdout);
    n=read();n1=read();n2=n-n1;
    S=0;T=n+1;
    memset(h,-1,sizeof(h));
    for(int i=1;i<=n1;++i)Add(S,i,1);
    for(int i=n1+1;i<=n;++i)Add(i,T,1);
    int u,v;
    while(scanf("%d%d",&u,&v)!=EOF)
        Add(u,v,1);
    printf("%d\n",Dinic());
    return 0;
}

網路流

1、最大流

  • sap
int head[MAXN];
int gap[MAXN],dep[MAXN],pre[MAXN],cur[MAXN];
void init()
{
    tol = 0;
    memset(head,-1,sizeof(head));
}
//加邊,單向圖三個引數,雙向圖四個引數
void addedge(int u,int v,int w,int rw=0)
{
    edge[tol].to =v;edge[tol].cap = w;edge[tol].next = head[u];
    edge[tol].flow= 0;head[u] = tol++;
    edge[tol].to =u;edge[tol].cap = rw;edge[tol].next = head[v];
    edge[tol].flow= 0;head[v]=tol++;
}
//輸入引數:起點、終點、點的總數
//點的編號沒有影響,只要輸入點的總數
int sap(int start,int end,int N)
{
    memset(gap,0,sizeof(gap));
    memset(dep,0,sizeof(dep));
    memcpy(cur,head,sizeof(head));
    int u = start;
    pre[u] = -1;
    gap[0] = N;
    int ans = 0;
    while(dep[start] < N)
    {
        if(u == end)
        {
            int Min = INF;
            for(int i = pre[u];i != -1; i = pre[edge[i^1].to])
            if(Min > edge[i].cap - edge[i].flow)
            Min = edge[i].cap - edge[i].flow;
            for(int i = pre[u];i != -1; i = pre[edge[i^1].to])
            {
                edge[i].flow += Min;
                edge[i^1].flow -= Min;
            }
            u = start;
            ans += Min;
            continue;
        }
        bool flag = false;
        int v;
        for(int i = cur[u]; i != -1;i = edge[i].next)
        {
            v = edge[i].to;
            if(edge[i].cap - edge[i].flow && dep[v]+1 == dep[u])
            {
            flag = true;
            cur[u] = pre[v] = i;
            break;
            }
        }
        if(flag)
        {
            u = v;
            continue;
        }
        int Min = N;
        for(int i = head[u]; i != -1;i = edge[i].next)
        if(edge[i].cap - edge[i].flow && dep[edge[i].to] < Min)
        {
            Min = dep[edge[i].to];
            cur[u] = i;
        }
        gap[dep[u]]--;
        if(!gap[dep[u]])return ans;
        dep[u] = Min+1;
        gap[dep[u]]++;
        if(u != start) u = edge[pre[u]^1].to;
    }
    return ans;
}

  • 上下界網路流

    #include <algorithm>
    #include <cstring>
    #include <cstdio>
    #include <queue>
    
    using namespace std;
    const int INF = 0x3f3f3f3f;
    const int MAXN = 110;
    
    namespace ISAP {
        const int MAXV = MAXN;
        const int MAXE = ( MAXV*MAXV/2 + MAXV*2 )*3;
        
        struct Edge {
            int u, v, c, f;
            Edge(){}
            Edge( int u, int v, int c, int f ):
                u(u),v(v),c(c),f(f){}
        }edge[MAXE<<1];
        int n, m, s, t, ss, tt;
        int head[MAXV], nxt[MAXE<<1], eid[MAXE<<1], eidx;
        void init( int n2, int ss2, int tt2 ) { // 初始化,設定附加源和附加匯
            n = n2; ss = ss2; tt = tt2;
            m = eidx = 0;
            memset( head, -1, sizeof(head) );
        }
        int adde( int u, int v, int c ) { // 新增一條只有上界的邊
            int rtn = m;
            eid[eidx] = m; nxt[eidx] = head[u]; head[u] = eidx++;
            edge[m++] = Edge(u,v,c,0);
            eid[eidx] = m; nxt[eidx] = head[v]; head[v] = eidx++;
            edge[m++] = Edge(v,u,0,0);
            return rtn;
        }
        int adde2( int u, int v, int b, int c ) { // 新增一條有上下界的邊,返回邊的下標
            int rtn = adde(u,v,c-b);
            adde(ss,v,b);
            adde(u,tt,b);
            return rtn;
        }
        // 以下ISAP板子
        int prev[MAXV], dist[MAXV], num[MAXV], cur[MAXV], res[MAXV];
        queue<int> bfsq;
        void bfs() {
            for( int i = 1; i <= n; ++i ) dist[i] = n;
            dist[t] = 0; bfsq.push(t);
            while( !bfsq.empty() ) {
                int u = bfsq.front(); bfsq.pop();
                for( int i = head[u]; ~i; i = nxt[i] ) {
                    Edge &e = edge[eid[i]];
                    if( dist[e.v] == n ) {
                        dist[e.v] = dist[u] + 1;
                        bfsq.push(e.v);
                    }
                }
            }
        }
        void augment() {
            int u = t, flow = res[t];
            while( u != s ) {
                int i = prev[u];
                edge[i].f += flow;
                edge[i^1].f -= flow;
                u = edge[i].u;
            }
        }
        bool advance( int &u ) {
            for( int i = cur[u]; ~i; i = nxt[i] ) {
                Edge &e = edge[eid[i]];
                if( e.c > e.f && dist[e.v] + 1 == dist[u] ) {
                    prev[e.v] = cur[u] = i;
                    res[e.v] = min( res[u], e.c - e.f );
                    u = e.v;
                    return true;
                }
            }
            return false;
        }
        bool retreat( int &u ) {
            if( --num[dist[u]] == 0 ) return false;
            int newd = n;
            for( int i = head[u]; ~i; i = nxt[i] ) {
                Edge &e = edge[eid[i]];
                if( e.c > e.f ) newd = min( newd, dist[e.v] + 1 );
            }
            ++num[ dist[u] = newd ];
            cur[u] = head[u];
            if( u != s ) u = edge[prev[u]].u;
            return true;
        }
        int solve( int s2, int t2 ) { // 以s2為源,t2為匯跑最大流
            s = s2; t = t2;
            bfs();
            for( int i = 1; i <= n; ++i )
                cur[i] = head[i], ++num[dist[i]];
            int u = s, flow = 0;
            res[s] = INF;
            while( dist[s] < n ) {
                if( u == t ) {
                    augment();
                    flow += res[t];
                    u = s;
                }
                if( !advance(u) )
                    if( !retreat(u) )
                        break;
            }
            return flow;
        }
    }
    
    int n, s, t, ss, tt; // 點的個數,源,匯,附加源,附加匯
    
    namespace Solve {
        using ISAP::head;
        using ISAP::nxt;
        using ISAP::eid;
        using ISAP::Edge;
        using ISAP::edge;
        bool first;
        void dfs( int u ) { // dfs輸出方案
            // printf( "Debug: u = %d\n", u );
            if( !first ) putchar(' ');
            first = false;
            printf( "%d", u );
            for( int i = head[u]; ~i; i = nxt[i] ) {
                Edge &e = edge[eid[i]];
                if( e.v <= n && e.f > 0 ) { // 任選一條邊走下去
                    // printf( "going eid = %d, from %d to %d, flow_left = %d\n", eid[i], e.u, e.v, e.f );
                    --e.f;
                    dfs(e.v);
                    return;
                }
            }
        }
        void addbound() { // 把每條邊流量加上下界,恢復成原圖的樣子,方便輸出方案
            using ISAP::m;
            for( int i = 0; i < m; ++i ) {
                Edge &e = edge[eid[i]];
                if( e.u <= n && e.v <= n && e.c > 0 )
                    ++e.f;
            }
        }
    }
    
    namespace Debug { // 除錯用QwQ
        void print_flow() {
            using ISAP::edge;
            using ISAP::Edge;
            using ISAP::eid;
            using ISAP::m;
            for( int i = 0; i < m; ++i ) {
                Edge &e = edge[eid[i]];
                if( e.u <= n && e.v <= n && e.c > 0 )
                    printf( "eid = %d, from %d to %d, flow = %d\n", eid[i], e.u, e.v, e.f );
            }
        }
        void print_flow2() {
            using ISAP::edge;
            using ISAP::Edge;
            using ISAP::eid;
            using ISAP::m;
            for( int i = 0; i < m; ++i ) {
                Edge &e = edge[eid[i]];
                if( e.f > 0 )
                    printf( "eid = %d, from %d to %d, flow = %d\n", eid[i], e.u, e.v, e.f );
            }
        }
    }
    
    int main() {
        while( scanf( "%d", &n ) == 1 ) {
            s = n+1, t = n+2, ss = n+3, tt = n+4;
            ISAP::init(tt,ss,tt);
            for( int i = 1; i <= n; ++i ) {
                int mi; scanf( "%d", &mi );
                while( mi-- ) {
                    int v; scanf( "%d", &v );
                    ISAP::adde2(i,v,1,INF);
                }
                ISAP::adde2(s,i,0,INF);
                ISAP::adde2(i,t,0,INF);
            }
            int flow1 = ISAP::solve(ss,tt);
            // printf( "flow1 = %d\n", flow1 );
            // Debug::print_flow();
            // Debug::print_flow2();
            int tsedge = ISAP::adde2(t,s,0,INF); // 儲存弧<t,s>的資訊,除錯用QwQ
            int ans = ISAP::solve(ss,tt);
            // printf( "t_s flow = %d\n", ISAP::edge[tsedge].f );
            // Debug::print_flow();
            // Debug::print_flow2();
            printf( "%d\n", ans );
            Solve::addbound(); // 把每條圖中的邊流量加上下界,恢復成原圖的樣子,方便輸出方案
            while( ans ) {
                using namespace Solve;
                for( int i = head[s]; ~i; i = nxt[i] ) {
                    Edge &e = edge[eid[i]];
                    if( e.v <= n && e.f > 0 ) { // 任選一個點dfs,輸出方案
                        first = true;
                        --e.f;
                        --ans;
                        dfs(e.v);
                            putchar('\n');
                    }
                }
            }
        }
        return 0;
    }
    

2、最小費用最大流

原理就是一直增廣 使用spfa(因為有負邊權)找到的s->t的一條可行最短路,直到不存在最短路了。可以證明這樣形成的最大流費用是最小的。

最大費用修改spfa或者直接建負邊權即可。

#include<bits/stdc++.h>
/*
支援重邊,所以邊數自己定,最大費用改成負權即可。

*/
using namespace std;

const int MAXN=1e4+10;
const int MAXM=2e5+10;
const int INF=0x3f3f3f3f;
struct Edge{
    int to,next,cap,flow,cost;
}edge[MAXM];

int tol,N;
int head[MAXN],pre[MAXN],dis[MAXN];
bool vis[MAXN];

void init(int n){
    N=n;
    tol=0;
    memset(head,-1,sizeof head);
}
void addedge(int u,int v,int cap,int cost){
    Edge &e=edge[tol];
    e.to=v;
    e.cap=cap;
    e.cost=cost;
    e.flow=0;
    e.next=head[u];
    head[u]=tol++;
    Edge &ee=edge[tol];
    ee.to=u;
    ee.cap=0;
    ee.cost=-cost;
    ee.flow=0;
    ee.next=head[v];
    head[v]=tol++;
}

bool spfa(int s,int t){
    //初始化
    queue<int> q;
    for(int i=0;i<=N;i++)
        dis[i]=INF,vis[i]=false,pre[i]=-1;
    dis[s]=0;vis[s]=true;q.push(s);
    //鬆弛,沒有判斷負環,判負環用cnt[N];
    while(!q.empty()){
        int u=q.front();q.pop();vis[u]=false;
        for(int i=head[u];~i;i=edge[i].next){
            Edge &e=edge[i];int v=e.to;
            if(e.cap>e.flow && dis[v]>dis[u]+e.cost){
                dis[v]=dis[u]+e.cost;
                pre[v]=i;
                if(!vis[v]){
                    vis[v]=true;
                    q.push(v);
                }
            }
        }
    }
    if(pre[t]==-1)return false;
    else return true;
}

void mcmf(int s,int t,int &cost,int &flow){
    flow=0;cost=0;
    //找一條費用最小的可行流。增廣之。
    while(spfa(s,t)){
        int Min=INF;
        for(int i=pre[t];~i;i=pre[edge[i^1].to])if(Min>edge[i].cap-edge[i].flow){
            Min=edge[i].cap-edge[i].flow;
        }
        //這裡i 是邊(u,v)的下標,讓edge[i^1].to=u
        for(int i=pre[t];~i;i=pre[edge[i^1].to]){
            //debug
            //cout<<"pre="<<edge[i^1].to<<endl;
            edge[i].flow+=Min;
            edge[i^1].flow-=Min;
            cost+=edge[i].cost*Min;
        }
        flow+=Min;
    }
}