题解
网络流经典问题,希望大家不要死记模型,要会自己建模
本文会详细讲解网络流的基础建模方法
先来看一下闭合图是什么
放一张论文的图片:
意思就是说,某些决策具有依赖性,这些依赖关系构成了一个DAG(如果是树就可以树形DP)
就可以使用最大权闭合图模型
最大权闭合图模型的原理是什么呢?
让我们先回到最小割的定义:把S集合与T集合割开的最小代价
如图:(割的方式一共有四种,红色的割线是最小割)
假设现在点与点之间没有任何关联
我们会发现,每一种割的方式都会对应一个选点的集合
而对于一个点u,我们如果想把它选入S集合,我们就得割掉u—>T的边,并付出代价
如果不把它分入S集合,我们就要割掉S—>u的边,并付出代价
如果我们想找到最小的代价来选出S集合,那么最小割一定对应了最小的代价
这才是最基本的模型
再回到最大权闭合图
这个选集合的过程中,如果某些点之间有依赖关系(比如说选A必选B),我们就可以加一条从A向B容量为INF的边
此时我们发现依然有四种割集,但是其中一种割集中含有边INF,所以这种割集会在计算答案时被排除。
而被排除的这种割集恰好对应了不满足依赖关系的选点方案
(关于这种构造方式的可行性、最优性证明在胡伯涛论文中有)
所以我们就有了一个解决最大权闭合图的朴素方法
由于P可能为负,所以就需要把所有的边容量加一个值,保证所有的边容量均为正数,最后算答案的时候减掉
对于这道题而言,把边也看成点,边权可以带来利润,就可以减少选出S集合的代价。
用这种建图方式就可以过了(虽然复杂度明显不对)
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100015
#define M 600005
const int INF=0x3f3f3f3f;
int fir[N],to[M],nxt[M],cap[M],cnt;
void adde(int a,int b,int c1,int c2)
{
to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;cap[cnt]=c1;
to[++cnt]=a;nxt[cnt]=fir[b];fir[b]=cnt;cap[cnt]=c2;
}
int S,T,d[N],vd[N],flow;
int sap(int u,int aug)
{
if(u==T) return aug;
int tmp,ret=0,mind=T-1;
for(int v,p=fir[u];p;p=nxt[p]){
v=to[p];
if(cap[p]>0){
if(d[u]==d[v]+1){
tmp=sap(v,min(cap[p],aug));
cap[p]-=tmp;aug-=tmp;
cap[p^1]+=tmp;ret+=tmp;
if(aug==0||d[S]>=T) return ret;
}
mind=min(mind,d[v]);
}
}
if(ret==0){
vd[d[u]]--;
if(vd[d[u]]==0)
d[S]=T;
d[u]=mind+1;
vd[d[u]]++;
}
return ret;
}
void f()
{
memset(d,0,sizeof(d));
memset(vd,0,sizeof(vd));
vd[0]=T;flow=0;
while(d[S]<T)
flow+=sap(S,INF);
}
#define mov 101
int main()
{
cnt=1;
int n,m,i,u,v,x;
scanf("%d%d",&n,&m);
S=m+n+1;T=m+n+2;
for(i=1;i<=n;i++){
scanf("%d",&x);
adde(S,i+m,mov,0);
adde(i+m,T,x+mov,0);
}
for(i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&x);
adde(i,u+m,INF,0);
adde(i,v+m,INF,0);
adde(S,i,mov,0);
adde(i,T,-x+mov,0);
}
f();
printf("%d",mov*(n+m)-flow);
}
如果想要在保持连通性的情况下建立模型
我们可以把S连边指向有利润的点,容量为利润,需要代价的点向T连边,容量为代价
这样的模型会更加优美简单
如论文的建模:
虽然时间复杂度还是明显不对,但有所优化。((n+m)^2*(n+m))
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 100015
#define M 400005
const int INF=0x3f3f3f3f;
int fir[N],to[M],nxt[M],cap[M],cnt;
void adde(int a,int b,int c1,int c2)
{
to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;cap[cnt]=c1;
to[++cnt]=a;nxt[cnt]=fir[b];fir[b]=cnt;cap[cnt]=c2;
}
int S,T,d[N],vd[N],flow;
int sap(int u,int aug)
{
if(u==T) return aug;
int tmp,ret=0,mind=T-1;
for(int v,p=fir[u];p;p=nxt[p]){
v=to[p];
if(cap[p]>0){
if(d[u]==d[v]+1){
tmp=sap(v,min(cap[p],aug));
cap[p]-=tmp;aug-=tmp;
cap[p^1]+=tmp;ret+=tmp;
if(aug==0||d[S]>=T) return ret;
}
mind=min(mind,d[v]);
}
}
if(ret==0){
vd[d[u]]--;
if(vd[d[u]]==0)
d[S]=T;
d[u]=mind+1;
vd[d[u]]++;
}
return ret;
}
void f()
{
memset(d,0,sizeof(d));
memset(vd,0,sizeof(vd));
vd[0]=T;flow=0;
while(d[S]<T)
flow+=sap(S,INF);
}
int main()
{
cnt=1;
int n,m,i,u,v,x,sum=0;
scanf("%d%d",&n,&m);
S=m+n+1;T=m+n+2;
for(i=1;i<=n;i++){
scanf("%d",&x);
adde(i+m,T,x,0);
}
for(i=1;i<=m;i++){
scanf("%d%d%d",&u,&v,&x);
adde(i,u+m,INF,0);
adde(i,v+m,INF,0);
adde(S,i,x,0);
sum+=x;
}
f();
printf("%d",sum-flow);
}
下面介绍一个更优的建模方法(不用边转点)
由于每条边只有两个端点是它的传递闭包
所以我们的答案就可以化为
选出一个子图G‘={V',E'}
最大化:(其中pv规定为负数)
接下来就是一个技巧,把边权转到对点权的求和上
(其中dv为与点v相邻的边的权值和)
为什么呢?
我们把V'集合中的点的相邻边的权值和加起来,会发现集合内部的边会算两次,红色的边会算一次
而红色边的边权就是V'与V'的补集的割的大小(由于我们想要最大化该式子的值,所以取最小割)
继续化式子:
乘以2不影响式子(最后再除掉2)
乘个-1,变成最小化:
我们想把后面那一坨式子放到求最小割的时候去做
就可以利用我们之前的选点模型,把每个点都连边向T,容量为-dv-2*pv
再把S到v和v到T的边的容量加一个较大值,保证边权非负,最后再减掉
虽然时间复杂度还是不对,但是这已经是本题的时间复杂度下限。(n^2*(n+m))
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 5015
#define M 200005
const int INF=0x3f3f3f3f;
int fir[N],to[M],nxt[M],cap[M],cnt;
void adde(int a,int b,int c1,int c2)
{
to[++cnt]=b;nxt[cnt]=fir[a];fir[a]=cnt;cap[cnt]=c1;
to[++cnt]=a;nxt[cnt]=fir[b];fir[b]=cnt;cap[cnt]=c2;
}
int S,T,d[N],vd[N],flow;
int sap(int u,int aug)
{
if(u==T) return aug;
int tmp,ret=0,mind=T-1;
for(int v,p=fir[u];p;p=nxt[p]){
v=to[p];
if(cap[p]>0){
if(d[u]==d[v]+1){
tmp=sap(v,min(cap[p],aug));
cap[p]-=tmp;aug-=tmp;
cap[p^1]+=tmp;ret+=tmp;
if(aug==0||d[S]>=T) return ret;
}
mind=min(mind,d[v]);
}
}
if(ret==0){
vd[d[u]]--;
if(vd[d[u]]==0)
d[S]=T;
d[u]=mind+1;
vd[d[u]]++;
}
return ret;
}
void f()
{
memset(d,0,sizeof(d));
memset(vd,0,sizeof(vd));
vd[0]=T;flow=0;
while(d[S]<T)
flow+=sap(S,INF);
}
int p[N],du[N];
struct node{int u,v,c;}e[M];
int main()
{
cnt=1;
int n,m,i,U=0;
scanf("%d%d",&n,&m);
S=n+1;T=n+2;
for(i=1;i<=n;i++){
scanf("%d",&p[i]);
U+=2*p[i];
}
for(i=1;i<=m;i++){
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].c);
U+=e[i].c;
du[e[i].u]+=e[i].c;du[e[i].v]+=e[i].c;
adde(e[i].u,e[i].v,e[i].c,e[i].c);
}
for(i=1;i<=n;i++){
adde(S,i,U,0);
adde(i,T,U+2*p[i]-du[i],0);
}
f();
printf("%d",(U*n-flow)/2);
}
这种方法在胡伯涛论文中有所提及