网络流
————重在建模
网络,有向图,源点s,汇点t
最大流
Edmonds−karp(EK)增广路算法
(O(nm^2)) $ 10^3 — 10^4 $
不断用BFS寻找增广路并不断更新最大流量值,直到网络上不存在增广路为止
在BFS寻找一条增广路时,我们只需要考虑剩余流量不为0的边,然后找到一条从S到T的路径,同时计算出路径上各边剩余容量值的最小值dis,则网络的最大流量就可以增加dis(经过的正向边容量值全部减去dis,反向边全部加上dis)
注意tot=1;
tot是从2开始,因为1^1=0,而没有0这条边,2的反边是3,4的反边是5
建边的时候见单向边,然后再建一条 w=0 的反边
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int N=510005;
int n,m,s,t;
int tot=1,hd[N];
struct edge{
int to,nxt;
LL w;
}e[N];
inline void add(int x,int y,LL z) {
e[++tot].to=y;e[tot].w=z;e[tot].nxt=hd[x];hd[x]=tot;
}
bool vis[N];
int pre[N];
LL dis[N],ans;
inline bool bfs() {
for(int i=1;i<=n;i++) vis[i]=0;
queue<int>q;
q.push(s);
vis[s]=1;
dis[s]=99999999999;
while(!q.empty()) {
int x=q.front();q.pop();
for(int i=hd[x];i;i=e[i].nxt) {
int y=e[i].to;
if(vis[y]) continue;
if(e[i].w<=0) continue;//!!!
dis[y]=min(dis[x],e[i].w);
pre[y]=i;
q.push(y);
vis[y]=1;
if(y==t) return 1;
}
}
return 0;
}
inline void update() {
int x=t;
ans+=dis[t];
while(x!=s) {
int v=pre[x];
e[v].w-=dis[t];
e[v^1].w+=dis[t];
x=e[v^1].to;
}
}
int flag[2505][2505];
int main() {
scanf("%d%d%d%d",&n,&m,&s,&t);
int x,y;LL z;
for(int i=1;i<=m;i++) {
scanf("%d%d%lld",&x,&y,&z);
if(!flag[x][y]) {
add(x,y,z);
add(y,x,0);
flag[x][y]=tot;
}
else e[flag[x][y]-1].w+=z;
}
while(bfs()) update();
printf("%lld
",ans);
return 0;
}
Dinic
(O(n^2m)) —— $ 10^4 — 10^5 $
食用这篇blog更易理解
基于对EK的优化,EK每次bfs只能找到一条增广路,Dinic就通过dfs优化它(见下面)
BFS 出图的层次,用 d[] 数组表示它的层次,即S到x最少需要经过的边数
在DFS中,从S开始,每次我们向下一层次随便找一个点,直到到达T,然后再一层一层回溯回去,继续找这一层的另外的点再往下搜索,这样就满足了我们同时求出多条增广路的需求
当前弧优化
其实就是走过的边dfs返回后不再走。。。
具体来说,就是 开一个now[] ,一开始now[]=head[],在dfs中,把now[x]赋值为 i ,也就是下一条边,这样回溯回来的时候会直接走下一条边,而不是重复搜之前搜的
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
typedef long long LL;
const int N=510005;
const int inf=0x3f3f3f3f;
int n,m,s,t;
int hd[N],nxt[N],to[N],tot=1;
LL ans,w[N];
inline void add(int x,int y,LL z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
LL c[N];
int now[N];
inline bool bfs() {
for(int i=1;i<=n;i++) c[i]=inf;
queue<int>q;
q.push(s);
c[s]=0;
now[s]=hd[s];
while(!q.empty()) {
int x=q.front();q.pop();
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(c[y]<inf||w[i]<=0) continue;
c[y]=c[x]+1;
q.push(y);
now[y]=hd[y];
if(y==t) return 1;
}
}
return 0;
}
inline LL dfs(int x,LL rest) {
if(x==t) return rest;
LL sum=0,k;
for(int i=now[x];i&&rest;i=nxt[i]) {//rest需>0
now[x]=i;//当前弧优化
int y=to[i];
if(w[i]<=0||c[y]!=c[x]+1) continue;//
k=dfs(y,min(rest,w[i]));
if(!k) c[y]=inf;
w[i]-=k;w[i^1]+=k;
sum+=k;rest-=k;
}
return sum;
}
int main() {
scanf("%d%d%d%d",&n,&m,&s,&t);
LL z;
for(int i=1,x,y;i<=m;i++) {
scanf("%d%d%lld",&x,&y,&z);
add(x,y,z);
add(y,x,0);
}
while(bfs()) ans+=dfs(s,inf);
printf("%lld
",ans);
return 0;
}
更牛B的算法
题:
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f;
inline int read() {
char ch=getchar();
while(!isalpha(ch)) ch=getchar();
return ch=='Y';
}
int n,m;
bool mp[55][55];
int hd[N],from[N],to[N],nxt[N],w[N],tot=1;
inline void add(int x,int y,int z) {
to[++tot]=y;from[tot]=x;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
void add_edge(int x,int y,int z) {
add(x,y,z),add(y,x,0);
}
int s,t,ans;
int c[N],now[N];
bool bfs() {
for(int i=1;i<N;i++) c[i]=inf;
queue<int>q;
q.push(s);
c[s]=0;
now[s]=hd[s];
while(!q.empty()) {
int x=q.front();q.pop();
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(w[i]==0||c[y]<inf) continue;
c[y]=c[x]+1;
q.push(y);
now[y]=hd[y];
if(y==t) return 1;
}
}
return 0;
}
int dfs(int x,int rest) {
if(x==t) return rest;
int sum=0,k;
for(int i=now[x];i&&rest;i=nxt[i]) {
now[x]=i;
int y=to[i];
if(w[i]==0||c[y]!=c[x]+1) continue;
k=dfs(y,min(rest,w[i]));
if(!k) c[y]=inf;
w[i]-=k;w[i^1]+=k;
sum+=k;rest-=k;
}
return sum;
}
int main(){
scanf("%d%d",&m,&n);
int x,y;
while(1) {
scanf("%d%d",&x,&y);
if(x==-1&&y==-1) break;
add_edge(x,y,1);
}
s=0,t=n+1;
for(int i=1;i<=m;i++)
add_edge(s,i,1);
for(int i=m+1;i<=n;i++)
add_edge(i,t,1);
while(bfs()) ans+=dfs(s,inf);
printf("%d
",ans);
for(int i=2;i<=tot;i+=2) {
if(to[i]==s||to[i^1]==s) continue;
if(to[i]==t||to[i^1]==t) continue;
if(w[i^1]) printf("%d %d
",from[i],to[i]);
}
return 0;
}
最小割
网络图上每条边有边权,移除边权和最小的一组边使得源点和汇点不连通
最小割=最大流
Description
•有m个男生和n个女生,每个人有一个智商值,其中有一些男女生存在“交往过密”的现象(一个人有可能和多个异性“交往过密”)
•现在要从中选出若干人组成一个班,现在你是教育处主任,你不希望看到这个班中有“交往过密”现象,同时你非常关心升学率,希望最大化这个班的智商值之和,求这个最大值
•n,m≤10000,智商值非负
Solution
男女间连正无穷,s到男连男智商,女到t连女智商,跑最小割
技巧
边点转化——拆点/拆边
Poj 1966
最小费用最大流
•费用流:边上还有一些费用,要求在流量最大的前提下,让每条边的流量*费用之和最大/最小
bfs改成spfa,+1改成+val[i]
注意dfs 入时vis[x]=1;回溯时vis[x]=0
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <cmath>
#include <queue>
using namespace std;
inline int read() {
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,m,s,t,ans=0,cost=0;
const int N = 510005;
const int inf = 0x3f3f3f3f;
int hd[N],to[N],nxt[N],w[N],val[N],tot=1;
inline void add(int x,int y,int z,int f) {
to[++tot]=y;w[tot]=z;val[tot]=f;nxt[tot]=hd[x];hd[x]=tot;
}
bool vis[N];
int now[N],c[N];
inline bool spfa() {
for(int i=1;i<=N;i++) c[i]=inf;
queue<int>q;
c[s]=0;
q.push(s);
now[s]=hd[s];
while(!q.empty()) {
int x=q.front();q.pop();
vis[x]=0;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(w[i]<=0) continue;
if(c[y]>c[x]+val[i]) {
c[y]=c[x]+val[i];
now[y]=hd[y];
if(!vis[y]) vis[y]=1,q.push(y);
}
}
}
return (c[t]!=inf);
}
inline int dfs(int x,int rest) {
if(x==t) return rest;
int sum=0,k;
vis[x]=1;
for(int i=now[x];i&&rest;i=nxt[i]) {
now[x]=i;
int y=to[i];
if(c[y]!=c[x]+val[i] || w[i]<=0 || vis[y]) continue;
k=dfs(y,min(rest,w[i]));
if(!k) c[y]=inf;
w[i]-=k;w[i^1]+=k;
sum+=k; rest-=k;
cost+=k*val[i];
}
vis[x]=0;//回溯
return sum;
}
int main() {
n=read();m=read();
s=read();t=read();
for(int i=1,x,y,z,f;i<=m;i++)
x=read(),y=read(),z=read(),f=read(),add(x,y,z,f),add(y,x,0,-f);
while(spfa())
ans+=dfs(s,inf);
printf("%d %d
",ans,cost);
return 0;
}
problems
晨跑
题意就是最小费用最大流,边和点的限制都是只能用一次,边可以用流量限制,点呢?应用上面提到的有向图拆点技巧,拆成入点和出点,中间连一条限制为 1(费用为0) 的边,以边代点
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#include <queue>
using namespace std;
inline int read() {
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) {x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,m,s,t,ans=0,cost=0;
const int N = 510005;
const int inf = 0x3f3f3f3f;
int hd[N],to[N],nxt[N],w[N],val[N],tot=1;
inline void add(int x,int y,int z,int f) {
to[++tot]=y;w[tot]=z;val[tot]=f;nxt[tot]=hd[x];hd[x]=tot;
}
bool vis[N];
int now[N],c[N];
inline bool spfa() {
for(int i=1;i<=N;i++) c[i]=inf;
queue<int>q;
c[s]=0;
q.push(s);
now[s]=hd[s];
while(!q.empty()) {
int x=q.front();q.pop();
vis[x]=0;
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(w[i]==0) continue;
if(c[y]>c[x]+val[i]) {
c[y]=c[x]+val[i];
now[y]=hd[y];
if(!vis[y]) vis[y]=1,q.push(y);
}
}
}
return (c[t]!=inf);
}
inline int dfs(int x,int rest) {
if(x==t) return rest;
int sum=0,k;
vis[x]=1;
for(int i=now[x];i&&rest;i=nxt[i]) {
now[x]=i;
int y=to[i];
if(c[y]!=c[x]+val[i] || w[i]==0 || vis[y]) continue;
k=dfs(y,min(rest,w[i]));
if(!k) c[y]=inf;
w[i]-=k;w[i^1]+=k;
sum+=k; rest-=k;
cost+=k*val[i];
}
vis[x]=0;
return sum;
}
int main() {
n=read();m=read();
s=1+n;t=n;
for(int i=1,x,y,z;i<=m;i++) {
x=read(),y=read(),z=read();
add(x+n,y,1,z);//
add(y,x+n,0,-z);//
}
for(int i=1;i<=n;i++)
add(i,i+n,1,0),add(i+n,i,0,0);//
while(spfa())
ans+=dfs(s,inf);
printf("%d %d
",ans,cost);
return 0;
}
方格取数
方法 1,dp,我担心的,就是一个点会被两条路径重复经过,注意到如果这样的话那么两条 路径走到这个点所用步数相同,那么可以 $dp[i][j][k][l] $对这两条路径同时 dp,同时一步一步 走。
方法 2,经过一个点,要么取一次,要么不取,这“仅取一次”是个限制 注意到一条路径可以等效为“一条流量” 一个点有两种入边,一种是不取数,只经过它,那么费用为 0,不需要流量限制(因为可以 是两条路径的交点,不仅经过一次) 另一种是取数,但是这种途径只能使用一次,因此流量限制为 1,费用为数 ,为了使其仅有两条路径,我把源点到左上角的点的流量连成 2 可以发现,这个建图思路是对“走路径”模拟得到的。 按照这个思路,很多带有“限制”的东西都能被费用流所“模拟”
跳舞
一个显然的想法是,男女可以看做左右部点,再让他们分别建立一个辅助点表示我要跟不喜欢 的人去跳,这个辅助点要连流量为 k 的边,表示 k 的限制,对于不喜欢的男女,男辅助点要连女 辅助点,喜欢的男女,让男主点连女主点,其实类似二分图匹配 但是这样的思路是混乱的,我们要求最多能让所有人集体跳多少轮,如果跑最大流,就会让 一些人多跳,但是另一些人少跳 你发现了没有,对于这种“整体限制”的不好做的问题,很好的思路就是二分答案 :我二分能跳多少轮,然后用刚才建的网络去判断是不是能跳 mid 轮。 对于每个人,我不让他多跳(源、汇点分别向男女连流量限制为 mid 的边)也不能让她少跳 (让总流量最大,判断是不是 mid*n),这种二分+判断满流的方法也是常用的
#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f;
inline int read() {
char ch=getchar();
while(!isalpha(ch)) ch=getchar();
return ch=='Y';
}
int n,k;
bool mp[55][55];
int hd[N],to[N],nxt[N],w[N],tot=1;
inline void add(int x,int y,int z) {
to[++tot]=y;w[tot]=z;nxt[tot]=hd[x];hd[x]=tot;
}
void add_edge(int x,int y,int z) {
add(x,y,z),add(y,x,0);
}
void clear() {
tot=1;
memset(hd,0,sizeof(hd));
}
int s,t;
int c[N],now[N];
bool bfs() {
for(int i=1;i<N;i++) c[i]=inf;
queue<int>q;
q.push(s);
c[s]=0;
now[s]=hd[s];
while(!q.empty()) {
int x=q.front();q.pop();
for(int i=hd[x];i;i=nxt[i]) {
int y=to[i];
if(w[i]==0||c[y]<inf) continue;
c[y]=c[x]+1;
q.push(y);
now[y]=hd[y];
if(y==t) return 1;
}
}
return 0;
}
int dfs(int x,int rest) {
if(x==t) return rest;
int sum=0,k;
for(int i=now[x];i&&rest;i=nxt[i]) {
now[x]=i;
int y=to[i];
if(w[i]==0||c[y]!=c[x]+1) continue;
k=dfs(y,min(rest,w[i]));
if(!k) c[y]=inf;
w[i]-=k;w[i^1]+=k;
sum+=k;rest-=k;
}
return sum;
}
bool check(int x) {
clear();
int res=0;
for(int i=1;i<=n;i++)
add_edge(s,i,x),add_edge(i+n,t,x),
add_edge(i,i+n*2,k),add_edge(i+n*3,i+n,k);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(mp[i][j]) add_edge(i,j+n,1);
else add_edge(i+n*2,j+n*3,1);
while(bfs()) res+=dfs(s,inf);
return res==n*x;
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
mp[i][j]=read();
s=0,t=n*4+1;
int l=0,r=n,ans;
while(l<=r) {
int mid=l+r>>1;
if(check(mid)) ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d
",ans);
return 0;
}
https://www.luogu.com.cn/blog/Multifuctional/fu-zai-ping-heng-wen-ti