1.1.3.4 最小割之建图实战、费用流基本概念

1.1.3.4 最小割之建图实战、费用流基本概念

最小割之建图实战

381. 有线电视网络

Problem

给定一张 n 个点 m 条边的无向图,求最少去掉多少个点,可以使图不连通。

如果不管去掉多少个点,都无法使原图不连通,则直接返回 n。

Solution

最小割模型的通用分析方式:

  1. 通过原图构造一个流网络
  2. 证明流网络的割集合和原题要求的集合一一对应
  3. 可以设置一个“简单割”的定义辅助自己证明

若无向图不连通,则图中至少必有两个点不连通,因此可以枚举最终不连通的点,我们取源点 S 和汇点 T,(注意:源点与汇点是枚举出来的,不是固定的)

题目转化为在剩余 n−2

个节点中最少去掉多少个,可以使 S 和 T 不连通。

注意:S 和 T 不能直接相连,否则不可能达到要求。 (比如三角形那种图)

在每次枚举的结果中取最小值就是本题的答案。
建图

考虑把删点转化为删边,与最小割联系起来

通过拆点我们可以把点限制变成边限制,将每个点拆成 入点 -> 出点 (这里我们把一个点的入点到出点的边叫做点内部的边,从其他点连过来的边叫做点外部的边)

这样把找割点的操作就变成了找割边的操作了。

因为我们只能去掉点,而不能去掉边,所以我们要使流网络的割边出现在点的内部,即拆点构成的边,而不能出现在点外部。

Tips:我们可以把所有不想让他出现在割边集里的边的容量设置为正无穷

因此我们可以将内部边的容量设为1,外部边的容量设为正无穷。

下面证明一下这种建图的正确性:

这里设定简单割为割边只包含内部边,不包含外部边。

如果一开始源点到汇点之间是没有边的话(如果有边就必不能令其孤立了,要把两个点都删掉),则从源点到汇点的路径必然会经过点的内部,即必然会经过容量为有限的边,这样从源点到汇点流过的可行流一定是有限值,最大流就一定是有限值,根据最小割=最大流,则最小割也是有限值,这样的最小割就必定是个简单割,即只包含点内部的边。最小的简单割一定是最小割。

这样的,所有简单割的集合就可以对应于本题中你想要删掉点的集合,因此从源点到汇点做一遍最小割,就是最小的需要删掉的点数。

证明简单割与要删掉的点的点割集存在一一对应的关系:

简单割 => 点割集

因为我们通过简单割求出的割边都是点内部的边,当我们把简单割里的边全删掉后,源点和汇点则不会联通了,这些构成“内部边”的点的集合就是点割集。

下面用反证法证明上面构造出来的点割集一定是符合题意的要删掉的点:

假设上面构造出来的点割集不符合题意,即把上面所有的点删掉,在原图里依然存在从源点到达汇点的路径,说明在原图中,存在一条不经过我们构造出来的点割集里的点的路径即不经过“点内部的边”,依然能从源点到达汇点,对应到流网络里则是存在一条从源点到汇点的不经过割边的路径,则说明源点与汇点在一个集合里,说明这不是一个割,与前提矛盾。因此反证得证。

点割集 => 简单割

这里的点割集指的是“极小点割集”

构造简单割的方法:

从源点开始dfs一遍,若经过点割集里的点,则停下不往前搜,若不是则往前搜,每次把搜到的点打个标记,这样标记了的点就是S集合,没有标记的点就是T集合,构成一个简单割C[S,T]

因此我们可以证出简单割与割点集存在一一对应的关系。
考虑数量关系

由于我们建边的时候把入点到出点的边的容量设为1,则得到的简单割的割边和就是选到的点的数量,则可以得到
割点集的点的数量 = 简单割的割的容量和 ,因此
\(最小割点集 = 最小割\)

总结

枚举源点与汇点,

源点枚举的是点的出点,汇点枚举的是点的入点

跑一遍dinic,在每次枚举的时候取更小的值即可。

但如果仅仅这样的话,我们发现网络流中的最小割与原问题的解不是一一对应的。

这是因为最小割中一定不包括源点和汇点,

但是只要原问题的解最后余下两个点以上,这种方案已经包括在最小割中了。
所以我们分别考虑最后只余下一个点和零个点的情况,

一个点的话,图还连通,不符题意。
零个点的话,符合题意,需要割掉的点数为n。
#include<bits/stdc++.h>
using namespace std;
const int MX_N=5100,MX_M=51000;
const int INF=0x3f3f3f3f;
struct node{
    int next,to,w;
}edge[MX_M<<1];
int head[MX_N]={0},edge_cnt=0;
inline void Add(int x,int y,int w){
    node &i=edge[edge_cnt];
    i.to=y,i.w=w,i.next=head[x];
    head[x]=edge_cnt++;
}
inline void add(int x,int y,int w){
    Add(x,y,w),Add(y,x,0);
}
int cur[MX_N]={0},dist[MX_N]={0};
int s=0,t=MX_N-1;
bool bfs(){
    for(int i=0;i<MX_N;i++)  cur[i]=head[i],dist[i]=-1;
    queue<int > qu;qu.push(s);
    dist[s]=0;
    while(!qu.empty()){
        int now=qu.front();qu.pop();
        for(int i=head[now];~i;i=edge[i].next){
            int to=edge[i].to,w=edge[i].w;
            if(dist[to]==-1&&w){
                qu.push(to);
                dist[to]=dist[now]+1;
            }
        }
    }
    return dist[t]!=-1;
}
int dfs(int now,int flow){
    if(now==t)  return flow;
    int left=flow;
    for(int &i=cur[now];~i;i=edge[i].next){
        int to=edge[i].to,w=edge[i].w;
        if(dist[to]==dist[now]+1&&w){
            int cur_flow=dfs(to,min(left,w));
            left-=cur_flow;
            edge[i].w-=cur_flow;
            edge[i^1].w+=cur_flow;
            if(left==0)  break;
        }
    }
    if(left==flow)  dist[now]=-1;
    return flow-left;
}
int dinic(){
    int sum=0;
    while(bfs())  sum+=dfs(s,INF);
    return sum;
}
inline void init(){
    edge_cnt=0;memset(head,-1,sizeof(head));
}
inline void build(){
    for(int i=0;i<edge_cnt;i+=2){
        edge[i].w+=edge[i^1].w;
        edge[i^1].w=0;
    }
}
signed main()
{
    char c;
    int n,m;
    while(scanf("%d%d",&n,&m)==2) {
        init();
        for(int i=1;i<=m;i++) {
            int x,y;
            cin>>c;
            scanf("%d,%d",&x,&y);x++,y++;
            cin>>c;
            add(x+n,y,INF);
            add(y+n,x,INF);
        }
        for(int i=1;i<=n;i++)  add(i,i+n,1);
        int minn=n;
        for(int S=1;S<=n;S++){
            for(int T=S+1;T<=n;T++){
                if(S==T)  continue ;
                s=S+n,t=T;
                minn=min(minn,dinic());
                build();
            }
        }
        printf("%d\n",minn);
    }
    return 0;
}

2199. 骑士共存问题

#include<bits/stdc++.h>
using namespace std;
const long long MX_N=41000,MX_M=510000;
struct node{
    long long to,next,w;
}edge[MX_M<<1];
long long head[MX_N]={0},edge_cnt=0;
inline void Add(long long x,long long y,long long w){
    node &i=edge[edge_cnt];
    i.w=w,i.to=y,i.next=head[x];
    head[x]=edge_cnt++;
}
inline void add(long long x,long long y,long long w){
    Add(x,y,w),Add(y,x,0);
}
long long s=0,t=MX_N-1;
long long cur[MX_N]={0},dist[MX_N]={0};
bool bfs(){
    for(long long i=0;i<MX_N;i++)  cur[i]=head[i],dist[i]=-1;
    queue<long long > qu;qu.push(s);dist[s]=0;
    while(!qu.empty()){
        long long now=qu.front();qu.pop();
        for(long long i=head[now];i!=-1;i=edge[i].next){
            long long to=edge[i].to;
            if(dist[to]==-1&&edge[i].w){
                dist[to]=dist[now]+1;
                qu.push(to);
            }
        }
    }
    return dist[t]!=-1;
}
long long dfs(long long now,long long flow){
    if(now==t)  return flow;
    long long left=flow;
    for(long long &i=cur[now];i!=-1;i=edge[i].next){
        long long to=edge[i].to,w=edge[i].w;
        if(dist[to]==dist[now]+1&&w){
            long long cur_flow=dfs(to,min(left,w));
            left-=cur_flow;
            edge[i].w-=cur_flow;
            edge[i^1].w+=cur_flow;
            if(left==0)  break;
        }
    }
    if(flow==left)  dist[now]=-1;
    return flow-left;
}
long long dinic(){
    long long sum=0;
    while(bfs()){
        sum+=dfs(s,0x3f3f3f3f);
    }
    return sum;
}
long long n,m;
long long mapn[210][210]={0};
inline long long has(long long x,long long y){
    return (x-1)*n+y;
}
inline bool check(long long x,long long y){
    return x>=1&&x<=n&&y>=1&&y<=n&&mapn[x][y]==0;
}
long long sn=0;
long long fx[9]={0,1,1,-1,-1,2,2,-2,-2},fy[9]={0,2,-2,2,-2,1,-1,1,-1}; 

signed main(){
    memset(head,-1,sizeof(head));
//============================================================
    scanf("%lld%lld",&n,&m);
    for(long long i=1;i<=m;i++){
        long long x,y;scanf("%lld%lld",&x,&y);
        mapn[x][y]=1;
    }
    for(long long i=1;i<=n;i++){
        for(long long j=1;j<=n;j++){
            if(mapn[i][j])  continue;
            if((i+j)%2==0){
                add(s,has(i,j),1);
                for(long long k=1;k<=8;k++){
                    long long dx=i+fx[k],dy=j+fy[k];
                    if(check(dx,dy)){
                        add(has(i,j),has(dx,dy),0x3f3f3f3f);
                    }
                }
            }
            else{
                add(has(i,j),t,1);
            }
        }
    }
    printf("%lld",n*n-m-dinic());
 //===========================================================
    return 0;
}

费用流基本概念

费用流问题就是要求在所有最大流之中,找到费用最大/最小的问题。

下面重点讨论最小费用最大流

一定要保证流网络没有负权回路(如果有,用消圈算法)

原理

先给出大概的做法:
在残留网络上沿着最短路(边权即费用)增广,直到得到最大流(无法再增广),那么,假如图中没有负圈,这样的最大流的费用是最小的。

下面证明正确性:
假如存在具有相同流量\(f\) 费用更小的流 \(f′\) ,考察 \(f′−f\),可知它由若干个圈组成,因为 \(f′−f\) 的费用为负,故在这些圈中至少存在一个负圈

也就是说,f 是最小费用流 ⇔ 残留网络中不存在负圈

基于上面的结论,可以利用归纳法证明上述做法的正确性。

  1. 对于流量为 \(0\) 的流 \(f_0\) ,残留网络即为原图,故只要不存在负圈,那么 \(f_0\) 就是最小费用流。

  2. 下证:若流量为 \(i\) 的流 \(f_i\) 为最小费用流,则其通过 \(s\)\(t\) 的最短路增广得到的 \(f_{i+1}\) 是流量为 \(i+1\) 的最小费用流。
    假设存在 \(f'_{i+1}\) 满足 \(f'_{i+1}\) < \(f_{i+1}\) ,而 \(f_{i+1} -f_i\) 对应的路径又是 \(s→t\) 的最短路,所以 \(f_{i+1} -f_i\) 对应的路径一定存在负圈,与 \(f_i\) 为最小费用流矛盾。

那么,如果图中存在负圈,如何解决呢?

此时我们需要对问题进行转化,从 \(t→s\) 连一条容量\(∞\)费用\(-∞\) 的虚边得到无源汇流。注意到如果存在增广路,则一定可以通过这条路径以及虚边使得费用更小,反之,如果不存在增广路,则在原图相应地得到了最小费用最大流。

下面只需求无源汇流的最小费用。

详见
各种Hack / 基于 Capacity Scaling 的弱多项式复杂度最小费用流算法

posted @ 2024-03-11 18:07  是菜菜呀  阅读(24)  评论(0编辑  收藏  举报