网络流
网络流
没有赘述概念和理解,想了解可以看参考资料和oi-wiki
没什么营养的板子
细节!
Dinic
-
建边
tot=1 -
反边初始容量为零
add(y,x,0) -
中间任意时刻能流的量为零了,直接跳 如
&&c、&&res -
d数组分层用,如果不能往后面流了,直接封死if(!flow) d[u]=-1 -
弧优化
dinic
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 205,M = 5005;
int n,m,s,t;
int head[N],tot=1;// 初始 tot=0,后面定反边用
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
namespace FLOW
{
int T;
int d[N],now[N];
inline LL dfs(int u,LL res)
{
if(u==T) return res;
LL flow=0;
for(int i=now[u];i&&res;i=e[i].u)//res==0 时没有流量了,直接跳
{
now[u]=i;//弧优化
int v=e[i].v,c=min((LL)e[i].w,res);
if(d[v]==d[u]+1&&c)//c==0 时没流量了,直接跳
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;//反边加容量,后面可以反悔
}
}
if(!flow) d[u]=-1;//不能继续增广 ,路死了
return flow;
}
inline LL dinic(int s,int t)
{
T=t;//注意
LL flow=0;
while(1)
{
queue<int> q;
memcpy(now,head,sizeof(head));
memset(d,-1,sizeof(d));//分层,初始为零
q.push(s); d[s]=0;
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e18);
}
}
} using namespace FLOW;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,0);//注意,反边初始容量为 0
}
printf("%lld\n",dinic(s,t));
return 0;
}
EK
EK
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1200+5,M = 120000+5;
int n,m,s,t;
int head[N],tot=1;//从一开始
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
LL f[N];
int pre[N];
LL EK(int s,int t)
{
LL flow=0;
while(1)
{
queue<int> q;
memset(f,-1,sizeof(f));
f[s]=1e18; q.push(s);//初始流量极大值
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(e[i].w&&f[v]==-1)
{
f[v]=min(f[u],(LL)e[i].w); pre[v]=i;//记录路径
q.push(v);
}
}
}
if(f[t]==-1) return flow;
flow+=f[t];
for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=f[t],e[pre[u]^1].w+=f[t];//回溯
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d%d%d",&n,&m,&s,&t);
for(int i=1;i<=m;i++)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
add(x,y,z); add(y,x,0);
}
printf("%lld\n",EK(s,t));
return 0;
}
SSP
-
spfa 出队删标记
-
反边容量初始为 0,费用为负。
-
边数不是
N<<1!!!
ssp
#include<bits/stdc++.h>
using namespace std;
#define P pair<int,int>
const int N = 405,M = 15005;
int n,m;
int head[N],tot=1;
struct E {int u,v,w,c;} e[M<<1];//边数为M
inline void add(int u,int v,int w,int c) {e[++tot]={head[u],v,w,c}; head[u]=tot;}
int d[N],f[N],pre[N];
bool vs[N];
P ssp(int s,int t)
{
int flow=0,cost=0;
while(1)
{
queue<int> q;
memset(d,0x3f,sizeof(d));
memset(vs,0,sizeof(vs));
q.push(s); d[s]=0; f[s]=1e9;//初始流量极大值
while(!q.empty())
{
int u=q.front(); q.pop(); vs[u]=0;//出队删标记
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v,dis=d[u]+e[i].c;
if(e[i].w&&dis<d[v])
{
d[v]=dis; pre[v]=i; f[v]=min(f[u],e[i].w); //记录路径
if(!vs[v]) vs[v]=1,q.push(v);
}
}
}
if(d[t]>1e9) return {flow,cost};
flow+=f[t]; cost+=f[t]*d[t];
for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=f[t],e[pre[u]^1].w+=f[t];//回溯
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y,z,w; scanf("%d%d%d%d",&x,&y,&z,&w);
add(x,y,z,w); add(y,x,0,-w);
}
P ans=ssp(1,n);
printf("%d %d\n",ans.first,ans.second);
return 0;
}
题
开门大吉 (集合划分问题)

做了几道类似的题,感觉关键在于决策的并联、串联和限制。
在不考虑限制的条件下,“串联”的决策需要在一条路径上做出一个选择,而“并联”表示决策具有独立性,每条路径都单独做出决策。
在这种由几条长链连接源点和汇点的图中,显然最小割就是最优决策。
如果再加入限制呢?
也就是规定一些边不能同时被选(或者是同时被选有额外代价),那么我们希望图中得到的效果就是:如果两条边同时被选,那么图仍没有被割成两个不相交的点集(割不动)。
所以我们可以通过在不同的长链间添加“横插边”来提供种限制,至于容量直接设置成无限大,怎么割也割不完,所以不能同时选,并且不会影响其他的条件。
注意加的限制需要考虑方向,有时候需要加双向边,也就是正反都会有代价,实际上就是正反容量初始都不为零,这样怎么 割都可以,就是双向边了。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 100005,M = 1000005;
const double del = 1e-18,inf = 1e9;
int n,m,p,TT,q,c[N];
int head[N],tot=1;
struct E {int u,v; double w;} e[M<<1];
inline void add(int u,int v,double w) {e[++tot]={head[u],v,w}; head[u]=tot; /*printf("%d %d %lf\n",u,v,w)*/;}
void init()
{
memset(head,0,sizeof(head)); tot=1;
}
int T,now[N],d[N];
double dfs(int u,double res)
{
if(u==T) return res;
double flow=0;
for(int i=now[u];i&&res>del;i=e[i].u)
{
now[u]=i;
int v=e[i].v; double c=min(res,e[i].w);
if(d[v]==d[u]+1&&c>del)
{
double k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(flow<del) d[u]=-1;
return flow;
}
double dinic(int s,int t)
{
T=t;
double flow=0;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w>del) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
if(flow>=1e9) return flow;
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d",&TT);
while(TT--)
{
init();
scanf("%d%d%d%d",&n,&m,&p,&q); int s=0,t=n*m+1;
for(int i=1;i<=p;i++) scanf("%d",&c[i]);
for(int j=1;j<=m;j++)
{
for(int i=1;i<=n;i++)
{
double sum=0,tt=1;
for(int k=1;k<=p;k++)
{
double x; scanf("%lf",&x);
tt*=x; sum=sum+tt*c[k];
}
if(j==m) add((i-1)*m+j,t,sum),add(t,(i-1)*m+j,0);
else add((i-1)*m+j,(i-1)*m+j+1,sum),add((i-1)*m+j+1,(i-1)*m+j,0);
}
}
for(int i=1;i<=n;i++)
{
add(s,(i-1)*m+1,inf); add((i-1)*m+1,s,0);
}
for(int i=1;i<=q;i++)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
for(int j=1;j<=m;j++)
{
if(j+z<=m&&j+z>=1) add((y-1)*m+j,(x-1)*m+j+z,inf),add((x-1)*m+j+z,(y-1)*m+j,0);
else if(j+z<1) continue;//注意有负数
else add((y-1)*m+j,t,inf),add(t,(y-1)*m+j,0);
}
}
double ans=dinic(s,t);
if(ans>=1e9) printf("-1\n");
else printf("%lf\n",ans);
}
return 0;
}
切糕(最小割离散变量)
集合划分的应用,本质一样。
不知道为什么起了个新名字,板板板,典典典。
发现在每个 \((x,y)\) 的 \(z\) 个决策中选一个,每个 \((x,y)\) 之间互不影响(暂时不考虑额外限制)。
所以每个 \((x,y)\) 的 \(z\) 个决策连一条长链连接源点和汇点,一共 \(xy\) 条链,然后再加入限制,挺典。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 50,inf = 1e9;
int p,q,r,D;
int a[N][N][N],cnt,mp[N][N][N];
int head[N*N*N],tot=1;
struct E {int u,v,w;} e[N*N*N<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot; /* printf("%d %d\n",u,v);*/}
int xx[4]={0,1,-1,0},yy[4]={1,0,0,-1};
int T,now[N*N*N],d[N*N*N];
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(res,e[i].w);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(flow==0) d[u]=-1;
return flow;
}
inline int dinic(int s,int t)
{
int flow=0; T=t;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(e[i].w&&d[v]==-1) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d%d%d",&p,&q,&r,&D);
for(int k=1;k<=r;k++)
for(int i=1;i<=p;i++)
for(int j=1;j<=q;j++) scanf("%d",&a[k][i][j]),mp[k][i][j]=++cnt;
int s=0,t=cnt+1;
for(int i=1;i<=p;i++)
for(int j=1;j<=q;j++)
{
add(s,mp[1][i][j],inf); add(mp[1][i][j],s,0);
for(int k=1;k<r;k++) add(mp[k][i][j],mp[k+1][i][j],a[k][i][j]),add(mp[k+1][i][j],mp[k][i][j],0);
add(mp[r][i][j],t,a[r][i][j]),add(t,mp[r][i][j],0);
}
for(int i=1;i<=p;i++)
for(int j=1;j<=q;j++)
for(int h=0;h<4;h++)
{
int x=i+xx[h],y=j+yy[h];
if(x<1||x>p||y<1||y>q) continue;
for(int k=D+1;k<=r;k++) add(mp[k][i][j],mp[k-D][x][y],inf),add(mp[k-D][x][y],mp[k][i][j],0);
}
printf("%d\n",dinic(s,t));
return 0;
}
货币
题面

挺板的集合划分问题,转化问题就简单了。
把 \((1,i)\) 到 \((2,i)\) 的边也看成特殊限制,也就是如果同时走了 \((1,i-1)\) 和 \((2,i)\),那么会多 \(c_i\) 的代价。
每一个 \(i\) 可以选择在第一行走,也可以选择在第二行走,直接连 \(i\) 条长链,然后加入限制。
在 \(i\) 选了第一行,\(j\) 选了第二行,会多代价,连一条代价为 \(c_i\) 的横插边就好了。注意有一些双向边。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 505,M = 3005;
int n,m;
int head[N],tot=1;
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
int T,d[N],now[N];
LL dfs(int u,LL res)
{
if(u==T) return res;
LL flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min((LL)e[i].w,res);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
LL dinic(int s,int t)
{
LL flow=0; T=t;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e18);
}
}
int main()
{
freopen("currency.in","r",stdin);
freopen("currency.out","w",stdout);
scanf("%d%d",&n,&m); int s=0,t=n+1;
for(int i=1;i<n;i++)
{
int x; scanf("%d",&x); add(s,i,x); add(i,s,0);
}
for(int i=1;i<=n;i++)
{
int x; scanf("%d",&x);
if(i==1) add(i,t,x),add(t,i,0);
else if(i==n) add(s,i-1,x),add(i-1,s,0);
else
{
add(i-1,i,x); add(i,i-1,x);
}
}
for(int i=1;i<n;i++)
{
int x; scanf("%d",&x); add(i,t,x); add(t,i,0);
}
for(int i=1;i<=m;i++)
{
int x,y,z; scanf("%d%d%d",&x,&y,&z);
add(y,x,z); add(x,y,0);
}
printf("%lld\n",dinic(s,t));
return 0;
}
飞行员配对方案问题 (二分图最大匹配)
二分图最大匹配板子,考虑左右部分别连源点和汇点,容量都为一,中间的边容量也是一,,因为一个人只能连一个人。最后方案找满流的边。(注意边数很大)
code
#include<bits/stdc++.h>
using namespace std;
const int N = 105,M = 1e5+5;
int n,m;
int head[N],tot=1;
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
namespace FLOW
{
int T,now[N],d[N];
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(res,e[i].w);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
int dinic(int s,int t)
{
T=t;
int flow=0;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
} using namespace FLOW;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&m,&n); n=n-m;
for(int i=1;i<=m;i++) add(0,i,1),add(i,0,0) ;
for(int i=1;i<=n;i++) add(i+m,n+m+1,1),add(n+m+1,i+m,0);
int x,y;
while(scanf("%d%d",&x,&y)&&x!=-1&&y!=-1)
add(x,y,1),add(y,x,0);
int ans=dinic(0,n+m+1);
if(ans==0)
{
printf("No Solution!");
return 0;
}
printf("%d\n",ans);
for(int u=1;u<=m;u++)
{
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v; if(e[i^1].w==0||e[i].w||v==0||v==n+m+1) continue;
printf("%d %d\n",u,v);
}
}
return 0;
}
方格取数问题(二分图最大(权)独立集)
方格取数是二分图最大权匹配,网络流的做法就是统计总价值,然后用最小割刻画最小的冲突代价,然后相减。
其他的都是二分图最大匹配,就是权值为 \(1\).
具体的建图方式:
将所有点分成两部分,保证每一部分内的点之间没有冲突(二分图),分别用源点和汇点连边,容量为价值,中间冲突用无穷大的容量刻画。
这样是否删减的信息只在与源点和汇点的边上,中间的边只描述冲突,不参与统计(割不动)。
注意第一步要把所有点分成两部分,一般直接黑白交错(横纵坐标之和的奇偶性),可以满足”不相邻“和”日字“限制。
长脖子鹿放置出现了奇葩的”目字“限制,考虑直接按行号奇偶分,就做完了。
P2774
#include<bits/stdc++.h>
using namespace std;
const int N = 1e4+5,M = 1e5+5,inf = 1e9;
int n,m;
int head[N],tot=1,a[N],mp[105][105],num,ans;
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
int xx[4]={0,0,1,-1},yy[4]={1,-1,0,0};
int T,now[N],d[N];
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(e[i].w,res);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
int dinic(int s,int t)
{
T=t;int flow=0;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
mp[i][j]=++num;
scanf("%d",&a[num]); ans=ans+a[num];
}
}
int s=0,t=num+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if((i+j)&1)
{
add(s,mp[i][j],a[mp[i][j]]);
add(mp[i][j],s,0);
for(int k=0;k<4;k++)
{
int x=i+xx[k],y=j+yy[k];
if(x<1||x>n||y<1||y>m) continue;
add(mp[i][j],mp[x][y],inf);
add(mp[x][y],mp[i][j],0);
}
}
else
{
add(mp[i][j],t,a[mp[i][j]]);
add(t,mp[i][j],0);
}
}
}
printf("%d\n",ans-dinic(s,t));
return 0;
}
P5030
#include<bits/stdc++.h>
using namespace std;
const int N = 4e4+5,M = 4e5+5,inf = 1e9;
int n,m,S;
int head[N],tot=1,a[N],mp[205][205],num,ans;
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
int xx[8]={-3,-1,1,3,3,1,-1,-3},yy[8]={1,3,3,1,-1,-3,-3,-1};
int T,now[N],d[N];
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(e[i].w,res);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
int dinic(int s,int t)
{
T=t;int flow=0;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d%d",&n,&m,&S);
for(int i=1;i<=S;i++)
{
int x,y; scanf("%d%d",&x,&y);
mp[x][y]=-1;
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(mp[i][j]!=-1) mp[i][j]=++num,ans++;
}
}
int s=0,t=num+1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++) if(mp[i][j]!=-1)
{
if((i)&1)
{
add(s,mp[i][j],1);
add(mp[i][j],s,0);
for(int k=0;k<8;k++)
{
int x=i+xx[k],y=j+yy[k];
if(x<1||x>n||y<1||y>n||mp[x][y]==-1) continue;
add(mp[i][j],mp[x][y],inf);
add(mp[x][y],mp[i][j],0);
}
}
else
{
add(mp[i][j],t,1);
add(t,mp[i][j],0);
}
}
}
printf("%d\n",ans-dinic(s,t));
return 0;
}
圆桌问题(二分图多重匹配)
板,好像是扩展版的最大匹配。转化问题为每组匹配 \(r_i\) 个桌子,然后做完了。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 1000+5,M = 1e5+5;
int n,m,s,t;
int head[N],tot=1,sum;
struct E {int u,v,w;} e[M<<1];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
int T,now[N],d[N];
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(res,e[i].w);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
int dinic(int s,int t)
{
int flow=0; T=t;
while(1)
{
queue<int> q;
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&m,&n); s=0; t=n+m+1;
for(int i=1;i<=m;i++)
for(int j=m+1;j<=m+n;j++)
add(i,j,1), add(j,i,0);
for(int i=1;i<=m;i++)
{
int x; scanf("%d",&x); sum+=x;
add(s,i,x); add(i,s,0);
}
for(int i=m+1;i<=m+n;i++)
{
int x; scanf("%d",&x);
add(i,t,x); add(t,i,0);
}
int ans=dinic(s,t);
if(ans<sum) printf("0\n");
else
{
printf("1\n");
for(int u=1;u<=m;u++)
{
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(e[i].w==0&&e[i^1].w) printf("%d ",v-m);
}
printf("\n");
}
}
return 0;
}
餐巾计划问题 (建模)
- 流量守恒
- 最大流等于最小割
- 拆点
首先发现干净的餐巾不能作为脏的继续流,所以把每天的餐巾分成两部分,干净的和脏的,这样就方便进行转化操作了。
注意每天流进多少干净的餐巾,就会流出多少脏的餐巾。这就恰好对应了网络流的一个重要的性质:流量守恒。
于是,不如直接让源点向每天流出固定的脏餐巾,让汇点收入固定的干净的餐巾(这里其实是一个逆向的过程)。
这样既满足了流量守恒(源点流出的一定等于汇点流入的),又使图的最小割(满流的边)恰好为所需要的餐巾数。
……其实在这种定义之下,可以凭空出现一个和S,T不通的环流。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 4e3+5,M = 4e6+5,inf = 1e9;
int n,s,t;
int head[N],tot=1;
struct E {int u,v,w,c;} e[M<<1];
inline void add(int u,int v,int w,int c) {e[++tot]={head[u],v,w,c}; head[u]=tot;}
int pre[N];
long long d[N],f[N];
bool vs[N];
long long ssp(int s,int t)
{
long long cost=0;
while(1)
{
queue<int> q;
memset(d,0x3f,sizeof(d));
memset(vs,0,sizeof(vs));
d[s]=0; q.push(s); f[s]=inf;
while(!q.empty())
{
int u=q.front(); q.pop(); vs[u]=0;
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v,dis=d[u]+e[i].c;
if(dis<d[v]&&e[i].w)
{
d[v]=dis; f[v]=min(f[u],(long long)e[i].w); pre[v]=i;
if(!vs[v]) vs[v]=1,q.push(v);
}
}
}
if(d[t]>1e9) return cost;
cost+=d[t]*f[t];
for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=f[t],e[pre[u]^1].w+=f[t];
}
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d",&n); s=0; t=2*n+1;
for(int i=1;i<=n;i++)
{
int x; scanf("%d",&x);
if(i+1<=n)add(i,i+1,inf,0), add(i+1,i,0,0);
add(s,i,x,0); add(i,s,0,0);
add(i+n,t,x,0); add(t,i+n,0,0);
}
int p,d1,d2,v1,v2;
scanf("%d%d%d%d%d",&p,&d1,&v1,&d2,&v2);
for(int i=1;i<=n;i++)
{
add(s,i+n,inf,p); add(i+n,s,0,-p);
if(i+d1<=n) add(i,i+d1+n,inf,v1),add(i+d1+n,i,0,-v1);
if(i+d2<=n) add(i,i+d2+n,inf,v2),add(i+d2+n,i,0,-v2);
}
printf("%lld\n",ssp(s,t));
return 0;
}
太空飞行计划问题(最大权闭合子图问题)
最大权闭合子图问题:如果选了一个点,那么它的所有后继也都选。
发现本题就是这个,最大割不可做,考虑正难则反,找不选的,变成最小割。
把所有实验向器材连边(inf),源点连实验,器材连汇点。边权都是绝对值。
如果不割实验,那就要割它的所有后继,如果割实验,那么它就不对后继有限制。
输出方案就是所有没被割的,注意反向边也要跑,应该是进行反悔得到的?
感谢提供输入。
code
#include<bits/stdc++.h>
using namespace std;
const int N = 105,inf = 1e9;
int n,m,sum;
int head[N],tot=1;
struct E {int u,v,w;} e[N*N*N];
inline void add(int u,int v,int w) {e[++tot]={head[u],v,w}; head[u]=tot;}
char tools[10000];
vector<int> ans1,ans2;
namespace FLOW
{
int d[N],now[N],T;
int dfs(int u,int res)
{
if(u==T) return res;
int flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(res,e[i].w);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
int dinic(int s,int t)
{
T=t; int flow=0;
while(1)
{
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
queue<int> q; q.push(s); d[s]=0;
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e9);
}
}
bool vs[N];
void find(int u,int t)
{
if(u==t) return; vs[u]=1;
if(u) {if(u<=m) ans1.push_back(u); else ans2.push_back(u-m);}
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(!e[i].w||vs[v]) continue;
find(v,t);
}
}
} using namespace FLOW;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d%d",&m,&n);
int S = 0,T = m+n+1;
for(int i=1;i<=m;i++)
{
int c; scanf("%d",&c);
add(S,i,c); add(i,S,0); sum+=c;
memset(tools,0,sizeof tools);
cin.getline(tools,10000);
int ulen=0,tool;
while (sscanf(tools+ulen,"%d",&tool)==1)//之前已经用scanf读完了赞助商同意支付该实验的费用
{//tool是该实验所需仪器的其中一个
//这一行,你可以将读进来的编号进行储存、处理,如连边。
add(i,tool+m,inf); add(tool+m,i,0);
if (tool==0)
ulen++;
else {
while (tool) {
tool/=10;
ulen++;
}
}
ulen++;
}
}
for(int i=1;i<=n;i++)
{
int c; scanf("%d",&c);
add(i+m,T,c); add(T,i+m,0);
}
int tmp=dinic(S,T);
find(S,T);
for(int x:ans1) printf("%d ",x); putchar('\n');
for(int x:ans2) printf("%d ",x); putchar('\n');
printf("%d\n",sum-tmp);
return 0;
}
apers
先考虑 \(k=1\) 的情况,显然是拆点后最小割。
怎么做到多割几次呢?
一张图显然只能割一次,那就分层图,一层割一次。
注意一个点只能被割一次,所以从上一层的入点应该连向下一层出点,边权 inf。
最后所有层的汇点连到超级汇点。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1005,M = 1e7;
const LL inf = 1e16;
int n,m,k,s,t,id[6][N][2];
int head[N<<1],tot=1;
struct E {int u,v; LL w;} e[M<<1];
inline void add(int u,int v,LL w) {e[++tot]={head[u],v,w}; head[u]=tot;}
namespace FLOW
{
int d[N<<1],now[N<<1],T;
LL dfs(int u,LL res)
{
if(u==T) return res;
LL flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(e[i].w,res);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
LL dinic(int s,int t)
{
LL flow=0; T=t;
int time=100000;
while(time--)
{
queue<int> q;
memcpy(now,head,sizeof(head));
memset(d,-1,sizeof(d));
d[s]=0; q.push(s);
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e18);
}
return -1;
}
} using namespace FLOW;
int main()
{
freopen("apers.in","r",stdin);
freopen("apers.out","w",stdout);
scanf("%d%d%d%d%d",&n,&m,&k,&s,&t);
int num=0;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=k;j++)
{
id[j][i][0]=++num, id[j][i][1]=++num;
if(j>1) add(id[j-1][i][0],id[j][i][1],inf),add(id[j][i][1],id[j-1][i][0],0);
}
}
for(int i=1;i<=n;i++)
{
LL w; scanf("%lld",&w);
for(int j=1;j<=k;j++)
{
add(id[j][i][0],id[j][i][1],w);
add(id[j][i][1],id[j][i][0],0);
}
}
for(int i=1;i<=m;i++)
{
int x,y; scanf("%d%d",&x,&y);
for(int j=1;j<=k;j++)
{
add(id[j][x][1],id[j][y][0],inf);
add(id[j][y][0],id[j][x][1],0);
}
}
int S=id[1][s][0],T=++num;
for(int j=1;j<=k;j++) add(id[j][t][1],T,inf),add(T,id[j][t][1],0);
printf("%lld\n",dinic(S,T));
return 0;
}
狼抓兔子
同上,拆点都不用。
数据范围吓人。但是这是 dinic。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 1e6+5;
const LL inf = 1e18;
int n,m,id[1003][1003];
int head[N],tot=1;
struct E {int u,v; LL w;} e[N<<1];
inline void add(int u,int v,LL w) {e[++tot]={head[u],v,w}; head[u]=tot;}
inline int read()
{
int res=0; char x=getchar();
while(x<'0'||x>'9') x=getchar();
while(x<='9'&&x>='0') res=(res<<1)+(res<<3)+(x^48),x=getchar();
return res;
}
namespace FLOW
{
int d[N],now[N],T;
int dfs(int u,LL res)
{
if(u==T) return res;
LL flow=0;
for(int i=now[u];i&&res;i=e[i].u)
{
now[u]=i;
int v=e[i].v,c=min(e[i].w,res);
if(d[v]==d[u]+1&&c)
{
int k=dfs(v,c);
flow+=k; res-=k; e[i].w-=k; e[i^1].w+=k;
}
}
if(!flow) d[u]=-1;
return flow;
}
LL dinic(int s,int t)
{
T=t; LL flow=0;
while(1)
{
memset(d,-1,sizeof(d));
memcpy(now,head,sizeof(head));
queue<int> q; q.push(s); d[s]=0;
while(!q.empty())
{
int u=q.front(); q.pop();
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(d[v]==-1&&e[i].w) d[v]=d[u]+1,q.push(v);
}
}
if(d[t]==-1) return flow;
flow+=dfs(s,1e18);
}
}
} using namespace FLOW;
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
n=read(); m=read();
int num=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++) id[i][j]=++num;
for(int i=1;i<=n;i++)
{
for(int j=2;j<=m;j++)
{
int x=read();
add(id[i][j-1],id[i][j],x); add(id[i][j],id[i][j-1],x);
}
}
for(int i=2;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
int x=read();
add(id[i-1][j],id[i][j],x); add(id[i][j],id[i-1][j],x);
}
}
for(int i=2;i<=n;i++)
{
for(int j=2;j<=m;j++)
{
int x=read();
add(id[i-1][j-1],id[i][j],x); add(id[i][j],id[i-1][j-1],x);
}
}
int S=id[1][1],T=id[n][m];
printf("%lld\n",dinic(S,T));
return 0;
}
AGC031E
看到数据范围和奇妙限制想网络流(赛时 \(O(2^nm\log(m))\) 直接过)。
没办法直接刻画限制,所以想能不能转化一下限制。目标应该是将限制转化为对于单独的点或者单独的位置的。
所以发现限制能被表示为“从上到下/从左到右第 \(b_i+1\) 个点的横/纵坐标至少为 \(a_i \pm 1\)”。
这就是一个模型了,即使没见过应该也能看出来。
对“从上到下”第 \(k\) 个和"从左到右"第 \(k\) 个建两层点,中间把候选点拆成边然后连向两边可以连的位置。费用为权值,流量都是 \(1\)。
对于 \(k\) 直接枚举就好了,感觉倒是不太好想到。
注意建边和转化限制时的细节。
最后跑最大费用最大流,就是把边权负过来,最后取相反数。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define fi first
#define se second
const int N = 105,M = 505;
const LL inf = 1e16;
int n,m;
LL ans;
struct P{int x,y; LL z;} a[N];
int A[N],B[N],C[N],D[N];
int tot=1,head[M];
struct E {int u,v,w; LL c;} e[M*M];
inline void add(int u,int v,int w,LL c) {e[++tot]={head[u],v,w,c}; head[u]=tot;}
namespace FLOW
{
#define P pair<int,LL>
LL d[N]; int f[N],pre[N];
bool vs[N];
P ssp(int s,int t)
{
int flow=0; LL cost=0;
while(1)
{
queue<int> q;
memset(d,0x3f,sizeof(d));
memset(vs,0,sizeof(vs));
d[s]=0; q.push(s); f[s]=1e9;
while(!q.empty())
{
int u=q.front(); q.pop(); vs[u]=0;
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(e[i].w&&d[u]+e[i].c<d[v])
{
d[v]=d[u]+e[i].c; pre[v]=i; f[v]=min(f[u],e[i].w);
if(!vs[v]) vs[v]=1,q.push(v);
}
}
}
if(d[t]>=1e18) return make_pair(flow,cost);
flow+=f[t]; cost+=f[t]*d[t];
for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=f[t],e[pre[u]^1].w+=f[t];
}
}
} using namespace FLOW;
LL cal(int k)
{
for(int i=0;i<=2*n+2*k+1;i++) head[i]=0; tot=1;
int s=0,t=2*n+2*k+1;
for(int i=2*n+1;i<=2*n+k;i++) add(s,i,1,0),add(i,s,0,0);
for(int i=2*n+k+1;i<=2*n+2*k;i++) add(i,t,1,0),add(t,i,0,0);
for(int i=1;i<=n;i++)
{
add(i,i+n,1,-a[i].z),add(i+n,i,0,a[i].z);
for(int j=1;j<=k;j++)
{
if(a[i].x>=A[j]&&a[i].x<=B[k-j+1]) add(2*n+j,i,1,0),add(i,2*n+j,0,0);
if(a[i].y>=C[j]&&a[i].y<=D[k-j+1]) add(i+n,2*n+k+j,1,0),add(2*n+k+j,i+n,0,0);
}
}
return -ssp(s,t).se;
}
int main()
{
// freopen("in.in","r",stdin);
// freopen("out.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int x,y; LL z; scanf("%d%d%lld",&x,&y,&z);
a[i]={x,y,z};
}
scanf("%d",&m);
memset(B,0x3f,sizeof(B)); memset(D,0x3f,sizeof(D));
for(int i=1;i<=m;i++)
{
char c; scanf(" %c",&c); int x,y; scanf("%d%d",&x,&y); y++;
if(c=='L') A[y]=max(A[y],x);
if(c=='R') B[y]=min(B[y],x);
if(c=='D') C[y]=max(C[y],x);
if(c=='U') D[y]=min(D[y],x);
}
for(int i=1;i<=n;i++) A[i]=max(A[i],A[i-1]),B[i]=min(B[i],B[i-1]),C[i]=max(C[i],C[i-1]),D[i]=min(D[i],D[i-1]);
LL ans=0;
for(int i=1;i<=n;i++) ans=max(ans,cal(i));
printf("%lld\n",ans);
return 0;
}
CF1572D
发现又是奇妙限制,需要奇妙结论。
首先这是一个图的匹配问题,考虑能否找到性质使其变成二分图。
这是显然的,有偶数个 \(1\) 的数只会连向奇数个 \(1\) 的数。
但是直接二分图匹配复杂度就炸了。
于是考虑图本身具有什么性质。
显然的是每个点仅会连 \(n\) 条边。进一步思考,假如选了一条边,那么有 \(2n-1\) 条边以后不能被选。
而一共只选 \(k\) 条边,所以最多只有 \(k(2n-1)\) 条边是有用的,我们只关心这些边就好了。
所以答案的边集一定在边权最大的 \(k(2n-1)\) 条边里。
因为边条太多了,所以上桶排,先把合法的权值找出来,然后对着权值取边,注意可能最小的权值里的边不一定取完,要特判一下。
还是最大费用最大流。
发现没有限制 \(k\),直接在源点前面连一个源源点,容量为 \(k\)。很简单实用的小技巧。
code
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define fi first
#define se second
const int N = 1.2e6,M = 2e6+5,inf = 5e8;
int n,k,a[N],l[N],c[N];
int head[N],tot=1;
struct E {int u,v,w,c;} e[N*20];
inline void add(int u,int v,int w,int c) {e[++tot]={head[u],v,w,c}; head[u]=tot;}
int b[M],d[N];
bool vs[M];
unordered_map<int,bool> mp[N];
namespace FLOW
{
int d[N],f[N],pre[N];
bool vs[N];
inline pair<int,int> ssp(int s,int t)
{
int flow=0,cost=0;
while(1)
{
queue<int> q;
for(int i=0;i<=(1<<n)+2;i++) vs[i]=0;
for(int i=0;i<=(1<<n)+2;i++) d[i]=inf;
// memset(d,0x3f,sizeof(d));
d[s]=0; q.push(s); f[s]=1e9;
while(!q.empty())
{
int u=q.front(); q.pop(); vs[u]=0;
for(int i=head[u];i;i=e[i].u)
{
int v=e[i].v;
if(e[i].w&&d[u]+e[i].c<d[v])
{
d[v]=e[i].c+d[u]; pre[v]=i; f[v]=min(f[u],e[i].w);
if(!vs[v]) vs[v]=1,q.push(v);
}
}
}
if(d[t]>=5e8) return make_pair(flow,cost);
flow+=f[t]; cost+=f[t]*d[t];
for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=f[t],e[pre[u]^1].w+=f[t];
}
}
}
int main()
{
// freopen("pp.in","r",stdin);
// freopen("pp.out","w",stdout);
scanf("%d%d",&n,&k);
int s=0,t=(1<<n)+1;
for(int i=0;i<1<<n;i++) scanf("%d",&a[i]);
for(int i=0;i<1<<n;i++)
{
for(int j=0;j<n;j++) c[i]+=(i>>j&1);
if(!(c[i]&1))
{
for(int j=0;j<n;j++) b[a[i]+a[i^(1<<j)]]++;
}
}
int T=0,kkk=0,cnt=0;
for(int i=2000000;i>=1;i--)
{
if(b[i]) T+=b[i],vs[i]=1;
if(T>=2*n*k) {kkk=i; vs[i]=0; cnt=2*n*k-T+b[i]; break;}
}
T=0;
for(int i=0;i<(1<<n);i++) if(!(c[i]&1))
{
for(int j=0;j<n;j++)
{
if(vs[a[i]+a[i^(1<<j)]]||(a[i]+a[i^(1<<j)]==kkk&&cnt)) mp[i][i^(1<<j)]=1,cnt-=a[i]+a[i^(1<<j)]==kkk,d[i]=d[i^(1<<j)]=1,T++;
if(T>=2*n*k) break;
}
if(T>=2*n*k) break;
}
for(int i=0;i<1<<n;i++) if(d[i])
{
if(c[i]&1) add(i+1,t,1,0),add(t,i+1,0,0);
else
{
add(s,i+1,1,0),add(i+1,s,0,0);
for(int j=0;j<n;j++)
{
int h=i^(1<<j); if(mp[i].find(h)==mp[i].end()) continue;
add(i+1,h+1,1,-a[i]-a[h]); add(h+1,i+1,0,a[i]+a[h]);
}
}
}
add(s,t+1,0,0); add(t+1,s,k,0); s=t+1;
printf("%d\n",-FLOW::ssp(s,t).se);
return 0;
}
CF708D
真·网络流。
看完题解发现好简单,但是想到也太难了。
直接粘 粉兔 的题解了。
对于哪些点连超级汇点、哪些点连超级源点,如果流入多了,那就从超级源点再引过来一点,然后就能从原图流出去,流出就变多了。反之同理。
对于原来的源汇,我们想让它们在新图上流量守恒,所以连一条 \(n \to 1\) 的边,构造循环流,然后当成普通点就行了。
感觉只是理解了为什么它是对,没理解为什么其他的是错的。

浙公网安备 33010602011771号