(终于学会了网络流好开心) 网络流是一类问题的统称,实际上很多问题都可以转成网络流来整,所以在此总结一下。
方法常见的是 F o r d − F u l k e r s o n Ford-Fulkerson Ford−Fulkerson方法,这个之所以叫方法,是因为这个有很多不同版本的实现,最常见的实现是 d i n i c dinic dinic。这个方法的核心是不停的寻找増广路,直到没有增广路为止。
算法的实现是不直接像FF方法所说的以来就找增广路,而是先用bfs将整个图分层,然后再在一层图中的点不算做增广路的路径,这样子的话,避免了找很多额外的边,这样子的话,可以证明复杂度是 O ( E V 2 ) O(EV^2) O(EV2)的,但是实际上根本就跑不到这个复杂度,而且还有一些优化,导致在随机图中可以过 1 e 5 1e5 1e5的数据都没有问题。
而且我们在做的时候,还可以有一个叫做当前弧优化的大法,方法是将肯定不可能的边跳过,这样就大大增快了速度。
下面提供两个版本的 d i n i c dinic dinic,分别是前缀星以及邻接表的储存方式。(是根据《挑战算法程序竞赛》的模板改来的)
前缀星:
#include
using namespace std;
const int N = 10005;
const int M = 100005;
const int inf = 1000000000;
struct E{int to,cap,nex;}e[M*2];int head[N],ecnt;
int dep[N];
int iter[N];
int n,m,s,t;
void adde(int fr,int to,int cap)
{
e[ecnt] = (E){to,cap,head[fr]};
head[fr] = ecnt++;
e[ecnt] = (E){fr,0,head[to]};
head[to] = ecnt++;
}
void bfs(int s)
{
memset(dep,-1,sizeof dep);
queue <int> q;
dep[s] = 0;
q.push(s);
while (!q.empty())
{
int cur = q.front();q.pop();
for (int j=head[cur];j!=-1;j=e[j].nex)
{
if (e[j].cap >0 && dep[e[j].to] < 0)
{
dep[e[j].to] = dep[cur] + 1;
q.push(e[j].to);
}
}
}
}
int dfs(int o,int t,int flow)
{
if (o==t) return flow;
for (int &j=iter[o];j!=-1;j=e[j].nex)
{
if (e[j].cap > 0 && dep[e[j].to] == dep[o] + 1)
{
int tmpflow = dfs(e[j].to,t,min(flow,e[j].cap));
if (tmpflow > 0)
{
e[j].cap -= tmpflow;
e[j^1].cap += tmpflow;
return tmpflow;
}
}
}
return 0;
}
int dinic(int s,int t)
{
int ansflow = 0;
for (;;)
{
bfs(s);
if (dep[t] < 0) return ansflow;
for (int i=1;i<=n;i++) iter[i]=head[i];
int f;
while ( (f=dfs(s,t,inf)) > 0)
{
ansflow += f;
}
}
}
void init()
{
ecnt=0;
memset(head,-1,sizeof head);
}
邻接表:
#include
#define pb push_back
using namespace std;
const int N = 10005;
const int inf = 1000000000;
struct E{int to,cap,rev;};
vector <E> G[N];
int dep[N];
int iter[N];
int n,m,s,t,e;
void adde(int fr,int to,int cap)
{
G[fr].pb((E){to,cap,G[to].size()});
G[to].pb((E){fr,0,G[fr].size()-1});
}
void bfs(int s)
{
memset(dep,-1,sizeof dep);
queue <int> q;
dep[s] = 0;
q.push(s);
while (!q.empty())
{
int cur = q.front();q.pop();
for (int i=0;i<G[cur].size();i++)
{
E &e = G[cur][i];
if (e.cap >0 && dep[e.to] < 0)
{
dep[e.to] = dep[cur] + 1;
q.push(e.to);
}
}
}
}
int dfs(int o,int t,int flow)
{
if (o==t) return flow;
for (int &i=iter[o];i<G[o].size();i++)
{
E &e = G[o][i];
if (e.cap > 0 && dep[e.to] == dep[o] + 1)
{
int tmpflow = dfs(e.to,t,min(flow,e.cap));
if (tmpflow > 0)
{
e.cap -= tmpflow;
G[e.to][e.rev].cap += tmpflow;
return tmpflow;
}
}
}
return 0;
}
int dinic(int s,int t)
{
int ansflow = 0;
for (;;)
{
bfs(s);
if (dep[t] < 0) return ansflow;
memset(iter,0,sizeof iter);
int f;
while ( (f=dfs(s,t,inf)) > 0)
{
ansflow += f;
}
}
}
常见的费用流有两种,一种是求流量为 F F F的费用流,另外一种是求最大流最小费用流。如何做呢?我们考虑一下我们是怎么做最大流的,我们是将增广路按照距离来 b f s bfs bfs分层,那么这个我们也可以模仿此,但是每次我们怎么走呢?我们按照费用的最小来走,这样子的话,就很明显了,但是要注意,不要乱写 d i j k s t r a l dijkstral dijkstral,要写 B e l l m a n − F o r d Bellman-Ford Bellman−Ford,因为路上的边权可能是负的。当然也可以写 d i j k s t r a l dijkstral dijkstral,但是要用一下势函数 h h h,借助类似差分的思想将边权变为正。下面是最小费用最大流的代码( S P F A SPFA SPFA):
#include
using namespace std;
const int N = 5002;
const int M = 500005;
const int inf = 100000;
struct E
{
int to,cap,cost,flow,next;
}e[2*M];int head[N] , ecnt;
int pre[N];
int dis[N];
bool vis[N];
int n,m,S,T;
void Clear()
{
ecnt = 0;
memset(head,-1,sizeof head);
}
void adde(int fr,int to,int cap,int cost)
{
e[ecnt]=(E){to,cap,cost,0,head[fr]};
head[fr] = ecnt++;
e[ecnt]=(E){fr,0,-cost,0,head[to]};
head[to] = ecnt++;
}
bool SPFA(int s,int t)
{
memset(vis,0,sizeof vis);
memset(dis,127,sizeof dis);
memset(pre,-1,sizeof pre);
queue <int> q;
q.push(s);dis[s] = 0;vis[s]=1;
while (!q.empty())
{
int cur = q.front();q.pop();vis[cur] = false;
for (int j=head[cur];j!=-1;j=e[j].next)
{
int to = e[j].to;
if (dis[to] > dis[cur] + e[j].cost && e[j].cap > e[j].flow )
{
dis[to] = dis[cur] + e[j].cost;
pre[to] = j;
if (!vis[to])
{
q.push(to);
vis[to] = true;
}
}
}
}
return pre[t] != -1;
}
void MCMF (int s,int t,int &maxflow,int &mincost)
{
maxflow = mincost = 0;
while (SPFA(s,t))
{
int MIN = inf;
for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
{
MIN = min(MIN,e[j].cap - e[j].flow);
}
for (int j=pre[t]; j!=-1;j=pre[e[j^1].to])
{
e[j].flow += MIN;
e[j^1].flow -= MIN;
mincost += MIN * e[j].cost;
}
maxflow += MIN;
}
}
还有一大坑点:建图的时候要从0号边开始,不然他的反边就不是 i ⊕ 1 i ⊕ 1 i⊕1
一个点有 A , B A,B A,B两种选择,分别有不同的收益。以及一个点如果与某些点同时选择一样,就会带来额外收益。
这种每个点两种选择的,像极了最小割中源点集以及汇点集的区分。那么先假设所有收益都能获得,然后用割的方式来决定怎么选。答案就等于 所有收益减去割 。因为要最大化答案,所以要最小化割,所以求一个最小割就行了。具体的。
比如割完之后源点集表示选择 A A A的,汇点集表示选择 B B B的。那么在源点集的点要割掉选 B B B的代价。所以连边 S → i S \rightarrow i S→i连 A i A_i Ai, i → T i\rightarrow T i→T连 B i B_i Bi。
然后考虑同时选择这些点的收益。比如同时选 择 A 择A 择A点的收益。我们新建一个点表示这个收益。那么,如果这个收益不被割掉,那么这几个点都选了 A A A。所以连边 i → n e w n o d e i\rightarrow newnode i→newnode连收益, i → 同 时 要 选 的 点 连 i n f i\rightarrow 同时要选的点连inf i→同时要选的点连inf。意义就是说,如果有至少有一个点在 T T T集中,假设这个点为 l l l。那么表示说 l → T l\rightarrow T l→T的边没被割掉。所以 i → n e w n o d e → l → T i\rightarrow newnode\rightarrow l\rightarrow T i→newnode→l→T还有通路,所以就会割掉这个收益。
小 N N N 手上有一个 M × N M×N M×N 的方格图。控制一个点要 A i j ( > 0 ) A_{ij} (> 0) Aij(>0)的代价;如果一个点被控制了,或它上下左右存在的点都被控制了,就算这个点被选择了,可以得到 B i j ( > 0 ) B_{ij} (> 0) Bij(>0)的回报。现在请你帮小 N N N 选一个最优的方案,使得回报减代价最大。
首先,我们明白,如果要获得一个点的收益,那么要么选它周围的四个点,要么选它。因为代价非负。
所以说,根据之前,我们还是有一个点选或者不选对应源点或者汇点集。我们有注意到这实际上黑白染色之后还是一个二分图。那么对于一个点,首先就二分图那样与源点/汇点连选的代价。然后怎么连回报?根据之前所说,只用新建一个点,分别向那五个点连边即可,意义与之前相同。
每天有几种选择:1)买餐巾,花费 a a a。2)把餐巾拿去洗,花费 b b b,要等上 c c c天才能洗好。然后每天要用一定数量的餐巾 n e e d [ i ] need[i] need[i],餐巾用了之后必须拿去洗才能再用,刚买的餐巾不用洗。问满足每天所需数量的前提下的最小代价。
对于每一天,我们建两个节点表示能用的纸巾与不能用的纸巾,记为 c a n i 与 c a n t i can_i与cant_i cani与canti。新建源点与汇点表示买以及每天所需。什么意思?就是说,比如对于某一天中能用的纸巾,我们要么把它拿去用,要么就留到下一天。那么对应了 c a n i → T < n e e d [ i ] , 0 > can_i\rightarrow T <need[i],0> cani→T<need[i],0>以及 c a n i → c a n i + 1 < i n f , 0 > can_i\rightarrow can_i+1<inf,0> cani→cani+1<inf,0>。对于不能用的,我们可以拿去洗,也可以留到下一天处理,注意,洗了之后就能用了,所以连边: c a n t i → c a n i + c < i n f , b > , c a n t i → c a n t i + 1 < i n f , 0 > cant_i\rightarrow can_i+c<inf,b>,cant_i\rightarrow cant_i+1<inf,0> canti→cani+c<inf,b>,canti→canti+1<inf,0>。然后还可以买,那么直接 S → c a n 1 < i n f , c > S\rightarrow can_1<inf,c> S→can1<inf,c>。但是这样每天那个用的纸巾就凭空消失了。所以从源点向 c a n t i cant_i canti连 < n e e d [ i ] , 0 > <need[i],0> <need[i],0>表示今天用的。
给定一张有向图,每条边都有一个容量 C i C_i Ci和一个扩容费用 W i W_i Wi。这里扩容费用是指将容量扩大 1 1 1所需的费用。
求:1) 在不扩容的情况下,1到N的最大流;2) 将1到N的最大流增加K所需的最小扩容费用。
跑出最大流之后,在残量网络上还有些非负边,可以直接用。所以我们对于原图中的边,将其费用设为0,然后新增一条边表示给这个边扩容,那么是 < i n f , w i > <inf,w_i> <inf,wi>,然后这样不一定能保证至多 k k k次,所以在连个 k k k大小的边随便限制一下就好了。
给定一张不合法的网络流图,每条边都有一个容量 C i C_i Ci和流量 f i f_i fi。可能会有流量大于容量的情况以及不满足流量平衡。现在你每次可以以1的代价将某条边容量(或流量)增加或减少1,问变成一个合法的网络流图的最小代价。
首先,像上下界网络流一样,根据出流量以及入流量来补流。
然后对于某一条边 ( c i , f i ) (c_i,f_i) (ci,fi),如果 c i > f i c_i > f_i ci>fi,那么答案要加上 c i − f i c_i - f_i ci−fi,然后根据是增加容量还是减少流量来建边。
给定一张有向图,每条边都有一个容量 C i C_i Ci和一个扩容费用 W i W_i Wi。这里扩容费用是指将容量扩大 1 1 1所需的费用。
求将使用K代价最多可以扩流多少。
分析
同1。
bzoj1449球队收益
题面不好打,就写了题号。
我们假设所有队伍在接下来的比赛中都 g g gg gg,这样会得到一个收益。然后用经典的费用流的费用与流量的平方成正比的方式做就好了。
首先我们知道至多有 n − 1 n-1 n−1个。然后如果每个点对都跑一次的话,就是 n 2 ∗ O ( 最 小 割 ) n^2 * O(最小割) n2∗O(最小割),肯定不行。考虑怎么乱搞。先随便选择一个源点 S S S一个汇点 T T T跑一下最小割,设为 F F F。然后对于一个源点集中的点 i i i以及汇点集中的点 j j j, F F F可能是 i , j i,j i,j的最小割。然后很明显,就像点分治那样,我们统计了 S → T S\rightarrow T S→T的割,然后分治源点集与汇点集。然而我不会证明。可以通过点分治的方式意会一下正确性。
这有一个结论:
先跑一边最大流,然后在残量网络上缩点。对于一条边 u → v u\rightarrow v u→v,如果 u , v u,v u,v不在一个联通分量,就可能被割掉。如果还满足 u u u与 S S S一个分量, v v v与 T T T一个分量。那么就必须割掉。当然,如果这条边在这次网络流中都没被割掉,那么肯定不会被割掉。