学习笔记:最小割

上讲习题

AcWing 2237

这个题可以记录每一个猪舍上次打来的时间,那么那个时候所有的猪都可以转移到该猪舍卖给这个买家。于是建一条 l a s t [ u ] last[u] last[u] i i i的边,容量为正无穷。如果这是第一次打开,那么原来就有 a [ i d ] a[id] a[id]头猪,从 s s s向该时间流 a [ i d ] a[id] a[id]就行了。每一个时间可以向 t t t连容量为 b [ i ] b[i] b[i]的边,表示买走了 b [ i ] b[i] b[i]头猪。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=20204,KK=1004,INF=1e9;
int h[NN],ne[MM],e[MM],c[MM],d[NN],head[NN],x[KK],last[KK],idx=-1,s,t;
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    memset(d,-1,sizeof(d));
    d[s]=0;
    q.push(s);
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int minn)
{
    
    
    if(u==t)
        return minn;
    int res=0;
    for(int i=head[u];~i&&res<minn;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],minn-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&m,&n);
    t=n+1;
    memset(h,-1,sizeof(h));
    for(int i=1;i<=m;i++)
        scanf("%d",&x[i]);
    for(int i=1;i<=n;i++)
    {
    
    
        int a,b;
        scanf("%d",&a);
        while(a--)
        {
    
    
            int id;
            scanf("%d",&id);
            if(!last[id])
                add(s,i,x[id]);
            else
                add(last[id],i,INF);
            last[id]=i;
        }
        scanf("%d",&b);
        add(i,t,b);
    }
    printf("%d",dinic());
    return 0;
}

概念

最小割,图论中的一个问题。
把一个图中所有点分成两部分,保证 s ∈ S s∈S sS t ∈ T t∈T tT,这就是一个割。
最小割就是跨过两个割集的边权之和最小的一个割。
最大流最小割定理:最大流等于最小割。证明:有一些水要流到汇点,但是中间割开了,中间割开的管子流量是小于等于管子的容量的。所有管子的流量本来应当是流到汇点的,但是全流出去了,则这些流量等于网络流中的一个流。割是管子的容量,所以 c u t ( S , T ) ≥ f cut(S,T)\geq f cut(S,T)f,于是 m i n c u t ( S , T ) ≥ m a x f mincut(S,T)\geq maxf mincut(S,T)maxf。最大流中,没有增广路,则可以把能到的点放到 S S S,不能到的点放在 T T T,中间割边若不是满流,则能到 T T T的某一个点,矛盾,所以一定有存在一个可行流等于一个割。上文可得 m a x f ≥ f = c u t ( S , T ) ≥ m i n c u t ( S , T ) maxf\geq f=cut(S,T)\geq mincut(S,T) maxff=cut(S,T)mincut(S,T),则 m a x f ≥ m i n c u t ( S , T ) maxf\geq mincut(S,T) maxfmincut(S,T),又有 m i n c u t ( S , T ) ≥ m a x f mincut(S,T)\geq maxf mincut(S,T)maxf,则 m i n c u t ( S , T ) = m a x f mincut(S,T)=maxf mincut(S,T)=maxf,得证。

算法

dinic算法

方法

和最大流一样,见我的上一篇文章
时间复杂度 O ( n 2 m ) O(n^2m) O(n2m)

例题

AcWing 2173

直接套板子

#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=200004;
int n,m,s,t,h[NN],ne[MM],e[MM],c[MM],idx=-1,head[NN],d[NN];
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    c[idx]=w;
    ne[idx]=h[u];
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    memset(d,-1,sizeof(d));
    q.push(s);
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,1e9))
            res+=sum;
    return res;
}
int main()
{
    
    
    scanf("%d%d%d%d",&n,&m,&s,&t);
    memset(h,-1,sizeof(h));
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w);
    }
    printf("%d",dinic());
    return 0;
}

最大权闭合图

概念

最大权闭合图,即指有一些点之间有关系,要么都选要么都不选。每个点都有点权,可以为负数,求最大的收益。

方法

可以先把所有收入累加起来,支出向 t t t连一条容量为支出的绝对值的边, s s s连一条容量为收入的边,两个点相关联就连一条正无穷的边,避免把它作为割边。这样求一条最小割,相当于把右边拽到左边来,用了右边的支出,或者把左边的拽到右边去,少得了左边的收入。最后用总的收入减去最小割即可。

例题

AcWing 961

这个题就是典型的最大权闭合图,按刚才的方法做最小割就行了。

#include<bits/stdc++.h>
using namespace std;
const int NN=55004,MM=(NN+100000)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    t=n+m+1;
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        add(m+i,t,x);
    }
    int sum=0;
    for(int i=1;i<=m;i++)
    {
    
    
        int a,b,x;
        scanf("%d%d%d",&a,&b,&x);
        add(s,i,x);
        add(i,m+a,INF);
        add(i,m+b,INF);
        sum+=x;
    }
    printf("%d",sum-dinic());
    return 0;
}

最大密度子图

概念

选出图 G G G的一个子图,设边子图的边集为 E E E,点集为 V V V,要求 ∣ E ∣ ∣ V ∣ \cfrac {|E|}{|V|} VE最大的一个子图。

方法

法1

要求 ∣ E ∣ ∣ V ∣ \cfrac {|E|}{|V|} VE最大,设密度为 g g g,则要求 G G G的一个子图 h ( g ) = ∣ E ∣ − ∣ V ∣ × g h(g)=|E|-|V|\times g h(g)=EV×g最大。我们又要求选一条边就必须选两个点,则每一条边化成一个点,连接他所连接的两个点,容量为正无穷。每少用一条边密度就会变成 ∣ E ∣ − 1 − ∣ V ∣ × g |E|-1-|V|\times g E1V×g,由源点连一条容量为 1 1 1的边。用一个点就会变成 ∣ E ∣ − ( ∣ V ∣ + 1 ) × g |E|-(|V|+1)\times g E(V+1)×g,向汇点连一条容量为 g g g的边,可以拿最大权闭合图做。最终答案的值是边数和减去最小割。如果这个值大于 0 0 0,那么就说明我们的密度还可以更大, l = m i d l=mid l=mid。如果这个最大值等于 0 0 0,则说明有密度太大可能一个都没有用或者密度刚好,有一个子图刚好是该密度, r = m i d r=mid r=mid。边界: r = m r=m r=m就够了,最多一个点就连接了 m m m条边(虽然不可能,但这是极限值),其实 l l l最小也只能是 1 n \cfrac{1}{n} n1,至少 n n n个点都用同一条边(也不可能,但是是极限)。时间复杂度 O ( l o g n × ( n + m ) 3 ) O(logn\times (n+m)^3) O(logn×(n+m)3)。如果有边权,就把 s s s到左边的点的权设为 w w w,如果有点权,就把右边的点到 t t t的边权设为 p v × g p_v\times g pv×g即可。

法2

因为选择了一条边就要选所连的两个点,设 d v d_v dv表示 v v v的入度, o u t ( V ) out(V) out(V)表示点集 V V V与外部有关联的边,则有: ∣ E ∣ = ∑ v ∈ V ( d v ) − ∣ o u t ( V ) ∣ 2 |E|=\cfrac{\displaystyle\sum_{v\in V}(d_v)-|out(V)|}{2} E=2vV(dv)out(V)因为是双向边,度数和边数都是两倍的,所以要除以2。于是为了变成一个方便求最小割的模型,把原式 h ( g ) = ∣ E ∣ − ∣ V ∣ × g h(g)=|E|-|V|\times g h(g)=EV×g变成: − h ( g ) = ∣ V ∣ × g − ∣ E ∣ -h(g)=|V|\times g-|E| h(g)=V×gE = ∑ v ∈ V ( g ) − ∑ v ∈ V ( d v ) − ∣ o u t ( V ) ∣ 2 =\displaystyle\sum_{v\in V}(g)-\cfrac{\displaystyle\sum_{v\in V}(d_v)-|out(V)|}{2} =vV(g)2vV(dv)out(V) = ∑ v ∈ V ( 2 g − d v ) − ∣ o u t ( V ) ∣ 2 =\cfrac{\displaystyle\sum_{v\in V}(2g-d_v)-|out(V)|}{2} =2vV(2gdv)out(V)
观察我们的式子,可以发现每选择一个点 v v v就有 2 g − d v 2g-d_v 2gdv的代价,按法1的思路,可以向t连一条容量为 2 g − d v 2g-d_v 2gdv的边。但是这样可能会出现负数,但是我们解决最大流的方法不能处理负数,所以就可以给每个点一个增加的值 U U U U U U最小 m m m就够了,因为极限情况 g = 0 g=0 g=0 U ≥ d v U\geq d_v Udv即可, d v d_v dv最大为m。但是加上了 U U U有一个问题,原图 G G G流量不守恒了!怎么办呢?可以从 s s s向每一个点流一条容量为 U U U的边,但是这样的话我们在跑最小割时如果不选一个点就没有少选边的代价了。于是我们去掉边当的点,两个点之间用 1 1 1表示,说明你把我们隔开了,那你就损失了一条边的密度。最后我们又回到二分,我们的原要求是大于 0 0 0即可。但是这次给每一个点一个额外的 U U U的流量,则一共多给了 U × n U\times n U×n,所以如果刚好就应该等于 U × n U\times n U×n,密度小了就是大于 U × n U\times n U×n,密度大了就是小于 U × n U\times n U×n。这样建图时间复杂度 O ( l o g n × n 2 ( n + m ) ) O(logn\times n^2(n+m)) O(logn×n2(n+m))。如果有边权,就把两点之间的权值设为 w w w,如果有点权,那么原式就变成了: = ∑ v ∈ V ( 2 g − d v ) + ∣ o u t ( V ) ∣ 2 − ∑ v ∈ V p v =\cfrac{\displaystyle\sum_{v\in V}(2g-d_v)+|out(V)|}{2}-\displaystyle\sum_{v\in V}p_v =2vV(2gdv)+out(V)vVpv = ∑ v ∈ V ( 2 g − 2 p v − d v ) + ∣ o u t ( V ) ∣ 2 =\cfrac{\displaystyle\sum_{v\in V}(2g-2p_v-d_v)+|out(V)|}{2} =2vV(2g2pvdv)+out(V)于是,每一个点连向t的边权就是 2 g − 2 p v − d v 2g-2p_v-d_v 2g2pvdv U U U就不能设为 m m m,要设为 max ⁡ ( 2 ∗ p v + d v ) \max(2*p_v+d_v) max(2pv+dv),因为极限情况是 g = 0 g=0 g=0,只要 U ≥ 2 p v + d v U\geq2p_v+d_v U2pv+dv即可。

例题

AcWing 2324

这个题就是一个最大密度子图。但题目中要求输出方案怎么办?这里有一个技巧,从 s s s出发,把能到的点标记,标记的就是集合 S S S的点,否则就是集合 T T T的点。所有集合 S S S的点就是要裁的员工。

#include<bits/stdc++.h>
using namespace std;
const int NN=104,MM=(NN*2+1000)*2;
int h[NN],ne[MM],e[MM],d[NN],head[NN],du[NN],idx,s,t,ans,n,m;
bool st[NN];
double c[MM];
pair<int,int>edge[1004];
void add(int u,int v,double w1,double w2)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w1;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    c[idx]=w2;
    h[v]=idx;
}
void build(double mid)
{
    
    
    memset(h,-1,sizeof(h));
    idx=-1;
    for(int i=1;i<=m;i++)
        add(edge[i].first,edge[i].second,1,1);
    for(int i=1;i<=n;i++)
    {
    
    
        add(s,i,m,0);
        add(i,t,m+mid*2-du[i],0);
    }
}
bool bfs()
{
    
    
    queue<int>q;
    memset(d,-1,sizeof(d));
    q.push(s);
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
double find(int u,double sum)
{
    
    
    if(u==t)
        return sum;
    double res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            double temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
double dinic()
{
    
    
    double res=0,sum;
    while(bfs())
        while(sum=find(s,1e9))
            res+=sum;
    return res;
}
void dfs(int u)
{
    
    
    st[u]=true;
    if(u!=s)
        ans++;
    for(int i=h[u];~i;i=ne[i])
    {
    
    
        int v=e[i];
        if(!st[v]&&c[i])
            dfs(v);
    }
}
int main()
{
    
    
    scanf("%d%d",&n,&m);
    t=n+1;
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v;
        scanf("%d%d",&u,&v);
        edge[i]={
    
    u,v};
        du[u]++;
        du[v]++;
    }
    double l=0,r=m;
    while(r-l>1e-8)
    {
    
    
        double mid=l+(r-l)/2;
        build(mid);
        int res=dinic();
        assert(res>=0);
        if(m*n-res>0)
            l=mid;
        else
            r=mid;
    }
    build(l);
    dinic();
    dfs(s);
    if(!ans)
    {
    
    
        printf("1\n1");
        return 0;
    }
    printf("%d\n",ans);
    for(int i=1;i<=n;i++)
        if(st[i])
            printf("%d\n",i);
    return 0;
}

AcWing 961

这个题目其实就是上文最大权闭合图的题,它还可以用最大密度子图做。可以发现每个点都连接了两个中转站,很像最大密度子图一个边选了就必须选两个点。这个就是带边权和点权的最大密度子图。但是因为这个题和密度并没有关系,所以我们不用二分密度,只用模仿最大密度子图的建图方式建图就行了,省去了二分的步骤。

#include<bits/stdc++.h>
using namespace std;
const int NN=5004,MM=(NN*2+50000)*2;
int h[NN],ne[MM],e[MM],c[MM],d[NN],head[NN],du[NN],p[NN],idx=-1,s,t;
void add(int u,int v,int w1,int w2)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w1;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    c[idx]=w2;
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    memset(d,-1,sizeof(d));
    q.push(s);
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,1e9))
            res+=sum;
    return res;
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    t=n+1;
    for(int i=1;i<=n;i++)
    {
    
    
        scanf("%d",&p[i]);
        p[i]*=-1;
    }
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v,w;
        scanf("%d%d%d",&u,&v,&w);
        add(u,v,w,w);
        du[u]+=w;
        du[v]+=w;
    }
    int U=0;
    for(int i=1;i<=n;i++)
        U=max(U,2*p[i]+du[i]);
    for(int i=1;i<=n;i++)
    {
    
    
        add(s,i,U,0);
        add(i,t,U-du[i]-2*p[i],0);
    }
    printf("%d",(U*n-dinic())/2);
    return 0;
}

最小点权覆盖集

概念

每个点有一个点权,可以花费该点的点权来覆盖所有周围的边。问最小花费多少覆盖全部的边。

方法

这种模型如果想用网络流解决就必须是二分图,否则只能dfs。把二分图左边和右边的点分别向汇点流容量为 p i p_i pi的边,这样就可以把左边拽到集合 T T T表明这条边用左边的点覆盖,右边拽到集合 S S S表示这条边用右边的点覆盖。为了保证一定会从点向源汇的边割开,也就是不想从中间割开,那么就把中间的边设为正无穷,这样最小割就不会割中间的边了。然后要注意先把所有负权点选了,把覆盖的边删掉再建图,因为用的是最大流解的最小割,不支持负权边,然而选负权一定最优。

例题

AcWing 2325

这个题就是让你用尽量便宜的点破坏所有的边。被某个点覆盖了的边就是本题被破坏了的边,问题就变成了用尽量少的点权覆盖全部的边,直接按刚刚最小点权覆盖集的方法做即可。

#include<bits/stdc++.h>
using namespace std;
const int NN=204,MM=(NN*2+5004)*2,INF=1e9;
int h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
bool st[NN];
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
void dfs(int u)
{
    
    
    st[u]=true;
    for(int i=head[u];~i;i=ne[i])
        if(c[i]&&!st[e[i]])
            dfs(e[i]);
}
int main()
{
    
    
    int n,m;
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    t=2*n+1;
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        add(i,t,x);
    }
    for(int i=1;i<=n;i++)
    {
    
    
        int x;
        scanf("%d",&x);
        add(s,i+n,x);
    }
    for(int i=1;i<=m;i++)
    {
    
    
        int u,v;
        scanf("%d%d",&u,&v);
        add(u+n,v,INF);
    }
    int ans=dinic(),cnt=0;
    vector<int>point;
    printf("%d\n",ans);
    dfs(s);
    for(int i=0;i<=idx;i+=2)
    {
    
    
        int a=e[i^1],b=e[i];
        if(st[a]&&!st[b])
        {
    
    
            if(a==s)
            {
    
    
                cnt++;
                point.push_back(b);
            }
            if(b==t)
            {
    
    
                cnt++;
                point.push_back(a);
            }
        }
    }
    printf("%d\n",cnt);
    for(int i=0;i<point.size();i++)
        printf("%d %c\n",point[i]>n?point[i]-n:point[i],point[i]>n?'-':'+');
    return 0;
}

最大点权独立集

概念

这个题就是每一条边最多只能用一个点,每个点有权值,要求得到的点权之和最大。

方法

同样要求是二分图。这个题和最小点权覆盖集是逆运算。用总点权减去最小点权覆盖集即可。

引理:设一个点独立集为 V 1 V1 V1,其补集 V 2 = V − V 1 V2=V-V1 V2=VV1一定是一个点覆盖集。

证明:设 V 2 V2 V2不是点覆盖集,假设是 u → v u\to v uv这一条边在 V 2 V2 V2两个点都没有选择,那么在补集 V 1 V1 V1中一定两个点都选了,并不是点独立集。证明一个点覆盖集为 V 1 V1 V1,其补集 V 2 = V − V 1 V2=V-V1 V2=VV1一定是一个点独立集同理。于是,每一个点覆盖集和一个点独立集是互为补集的,则和都是所有点的总权值。那么为了让点独立集得到最大权,肯定要作为补集的点覆盖集尽量小,则一定是最小点权覆盖集。

例题

AcWing 2326

这个题不难发现,相邻的两个点只能选择一个点,因为奇数秒是由相邻的格子在偶数秒过来的,所以能得到宝石的一定是偶数秒,那么周围的格子都会清空。则如果选择了一个格子周围的四格都不能选了。而且这种题可以把相邻的点放在不同的集合,保证是二分图。于是这个题就是最大权独立集。

#include<bits/stdc++.h>
using namespace std;
const int NN=10004,MM=NN*8,INF=1e9,dx[4]={
    
    0,0,-1,1},dy[4]={
    
    -1,1,0,0};
int n,m,h[NN],e[MM],ne[MM],c[MM],head[NN],d[NN],idx=-1,s,t;
int get(int x,int y)
{
    
    
    return (x-1)*m+y;
}
void add(int u,int v,int w)
{
    
    
    e[++idx]=v;
    ne[idx]=h[u];
    c[idx]=w;
    h[u]=idx;
    e[++idx]=u;
    ne[idx]=h[v];
    h[v]=idx;
}
bool bfs()
{
    
    
    queue<int>q;
    q.push(s);
    memset(d,-1,sizeof(d));
    d[s]=0;
    head[s]=h[s];
    while(q.size())
    {
    
    
        int f=q.front();
        q.pop();
        for(int i=h[f];~i;i=ne[i])
        {
    
    
            int v=e[i];
            if(d[v]==-1&&c[i])
            {
    
    
                d[v]=d[f]+1;
                head[v]=h[v];
                if(v==t)
                    return true;
                q.push(v);
            }
        }
    }
    return false;
}
int find(int u,int sum)
{
    
    
    if(u==t)
        return sum;
    int res=0;
    for(int i=head[u];~i&&res<sum;i=ne[i])
    {
    
    
        head[u]=i;
        int v=e[i];
        if(d[v]==d[u]+1&&c[i])
        {
    
    
            int temp=find(v,min(c[i],sum-res));
            if(!temp)
                d[v]=-1;
            c[i]-=temp;
            c[i^1]+=temp;
            res+=temp;
        }
    }
    return res;
}
int dinic()
{
    
    
    int res=0,sum;
    while(bfs())
        while(sum=find(s,INF))
            res+=sum;
    return res;
}
int main()
{
    
    
    int res=0;
    scanf("%d%d",&n,&m);
    memset(h,-1,sizeof(h));
    t=n*m+1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
        {
    
    
            int x;
            scanf("%d",&x);
            res+=x;
            if(i+j&1)
            {
    
    
                add(s,get(i,j),x);
                for(int k=0;k<4;k++)
                {
    
    
                    int nx=i+dx[k],ny=j+dy[k];
                    if(nx<=n&&ny<=m&&nx&&ny)
                        add(get(i,j),get(nx,ny),INF);
                }
            }
            else
                add(get(i,j),t,x);
        }
    printf("%d",res-dinic());
    return 0;
}

习题

AcWing 2279

AcWing 2280

AcWing 381

AcWing 2176

AcWing 2199

解析和代码在下一篇博客——费用流给出

猜你喜欢

转载自blog.csdn.net/weixin_44043668/article/details/108685833