<h1>最小费用最大流</h1>
<h2>定义</h2>
我想你一定学会了最大流算法。
模板题链接
我们发现在同一个网络中,可能会有多个总流量相同的最大流 $f$,我们可以在计算流量的基础上,给网络中的弧增加一个单位流量的费用(简称费用),在确保流量最大的前提下总费用最小,这样的最大流叫做最小费用最大流。
在上面这个网络中,弧上用逗号分隔的两个数分别为弧的容量和单位流量费用。例如,一条流量为 $2$、经过 $S \to 1 \to 4 \to T$ 的流的费用是 $(3+5+4)*2 = 24$。
<h2>做法</h2>
首先每条边多了一个属性“费用”,对于反向边怎么加呢?
我们可以这样:
inline void add(int u,int v,int cap,int cost){ node[u].first = New(&node[u],&node[v],cap,cost); node[v].first = New(&node[v],&node[u],0,-cost); node[u].first->rev = node[v].first; node[v].first->rev = node[u].first; } // 看不懂指针的朋友:这里就是反向边的消费是正向边的相反数的意思,
接下来我们按照以下流程来实现:
- 网络初始流量为 $0$
- 在当前的残量网络上求出从 $S$ 到 $T$ 的最短增广路,以每条弧的单位费用为边权。如果不存在则算法结束
- 统计答案,更改流量,重复步骤 $2$
在绝大多数情况下我们使用 SPFA 算法来求解第 $2$ 步。
<h2>代码</h2>
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define Re register #define LL long long #define U unsigned #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 5000+5; const int MAXM = 50000+5; inline char nc(){ #define SIZE 100000 static char buf[SIZE],p1 = buf+SIZE,pend = buf+SIZE; if(p1 == pend){ p1 = buf;pend = buf+fread(buf,1,SIZE,stdin); if(p1 == pend) return -1; } return *p1++; } inline void read(int &x){ x = 0;int flag = 1; char ch = nc(); while(!isdigit(ch)){ if(ch == '-') flag = -1; ch = nc(); } while(isdigit(ch)){ x = (x<<1) + (x<<3) + (ch^'0'); ch = nc(); } x *= flag; } struct Node{ struct Edge first,pre; int dist,h;bool vis; }node[MAXN]; struct Edge{ Node s,t; Edge next,rev; int flow,cap,cost; }pool[(MAXM<<1)+3],*frog = pool; Edge New(Node s,Node *t,int cap,int cost){ Edge *ret = ++frog; *ret = (Edge){s,t,s->first,NULL,0,cap,cost}; return ret; } inline void add(int u,int v,int cap,int cost){ node[u].first = New(&node[u],&node[v],cap,cost); node[v].first = New(&node[v],&node[u],0,-cost); node[u].first->rev = node[v].first; node[v].first->rev = node[u].first; } #define P std::pair<int,Node *> #define MP std::make_pair int N,M,S,T; int maxFlow,minCost; bool spfa(Node s,Node t){ FOR(i,1,N) node[i].vis = false,node[i].dist = INT_MAX,node[i].pre = NULL; s->vis = true;s->dist = 0; std::queue<Node *> q;q.push(s); while(!q.empty()){ Node *v = q.front();q.pop(); v->vis = false; for(Edge *e = v->first;e;e = e->next){ if(e->flow < e->cap){ if(e->t->dist > v->dist + e->cost){ e->t->dist = v->dist + e->cost; e->t->pre = e; if(!e->t->vis) q.push(e->t),e->t->vis = true; } } } } return t->pre != NULL; } inline void work(){ while(spfa(&node[S],&node[T])){ int flow = INT_MAX; for(Node *v = &node[T];v != &node[S];v = v->pre->s){ flow = std::min(flow,v->pre->cap-v->pre->flow); } maxFlow += flow; for(Node *v = &node[T];v != &node[S];v = v->pre->s){ v->pre->flow += flow; v->pre->rev->flow -= flow; minCost += v->pre->cost * flow; } } } int main(){ //freopen("./LG/testdata.in","r",stdin); read(N);read(M);read(S);read(T); // DEBUG(N); while(M--){ int u,v,cap,cost; read(u);read(v);read(cap);read(cost); add(u,v,cap,cost); } work(); printf("%d %dn",maxFlow,minCost); return 0; }
<h1>模型扩展</h1>
<h2>贴纸与玩偶</h2>
其中 $m \leq 100,n \leq 50$。
这个题目就是费用流经典模型了:有两种不同的属性的物品,需要找出两个不同的属性的物品进行配对并规定配对对答案的贡献。并且每种物品数量有限,要求答案最大或最小。
对于上述题目来说:源点连向每个贴纸一条流量为 $a_i$,费用为 $0$ 的弧,每个玩偶连向汇点一条流量为 $b_i$,费用为 $0$ 的弧,每个贴纸连向每个玩偶流量为 $\text{INF}$,费用为 $h_{i,j}$ 的弧,按需跑最小/大费用最大流即可。
<h3>代码</h3>
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define Re register #define LL long long #define U unsigned #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 1000+5; const int MAXM = 10000+5; struct Node{ struct Edge first,pre; int dist;bool inq; }node[MAXN]; struct Edge{ Node s,t; Edge next,rev; int cap,flow,cost; }pool[MAXM<<1],*frog = pool; Edge New(Node s,Node *t,int cap,int cost){ Edge *ret = ++frog; *ret = (Edge){s,t,s->first,NULL,cap,0,cost}; return ret; } int N,M,S,T; inline void add(int u,int v,int cap,int cost){ node[u].first = New(&node[u],&node[v],cap,cost); node[v].first = New(&node[v],&node[u],0,-cost); node[u].first->rev = node[v].first; node[v].first->rev = node[u].first; } bool spfa(Node s,Node t){ FOR(i,0,N){ node[i].dist = INT_MAX; node[i].inq = false; node[i].pre = NULL; } std::queue<Node *> q; q.push(s);s->inq = true;s->dist = 0; while(!q.empty()){ Node *v = q.front();q.pop(); v->inq = false; for(Edge *e = v->first;e;e = e->next){ if(e->flow < e->cap && e->t->dist > v->dist + e->cost){ e->t->dist = v->dist + e->cost; e->t->pre = e; if(!e->t->inq){ q.push(e->t);e->t->inq = true; } } } } return t->pre != NULL; } inline int mincost(){ int ret = 0; while(spfa(&node[S],&node[T])){ int flow = INT_MAX; for(Node *v = &node[T];v != &node[S];v = v->pre->s){ flow = std::min(flow,v->pre->cap-v->pre->flow); } for(Node *v = &node[T];v != &node[S];v = v->pre->s){ v->pre->flow += flow; v->pre->rev->flow -= flow; ret += v->pre->cost*flow; } } return ret; } int a[MAXN],b[MAXN]; int hMAXN; int n,m; int work(int flag){ CLR(node,0);CLR(pool,0);frog = pool; S = 0,T = n+m+1; N = n+m+1; FOR(i,1,n) add(S,i,a[i],0); FOR(i,1,m) add(n+i,T,b[i],0); FOR(i,1,n){ FOR(j,1,m){ add(i,n+j,INT_MAX,hi*flag); } } return mincost()*flag; } int main(){ scanf("%d%d",&n,&m); FOR(i,1,n) scanf("%d",a+i); FOR(i,1,m) scanf("%d",b+i); FOR(i,1,n) FOR(j,1,m) scanf("%d",&hi); printf("%dn",work(1)); printf("%dn",work(-1)); return 0; }
<h2>蒜头君的理发店</h2>
这一题直接看不是很好想,我们不如考虑每一个理发师对答案的贡献。
考虑一个理发师接待了 $m$ 位顾客,编号从 $1$ 到 $m$,给第 $i$ 个顾客理发需要花费 $w_i$ 的时间,那么选定一种排列方式 $p$,则该排列方式对应的答案就是 $ \sum_{i=1}^n w_{p_i} * (n-i+1) $。
那么对于每一个理发师我们可以看做是 $m$ 个状态表示,可以拆点第 $i$ 个点表示正在理倒数第 $i$ 个顾客,如果指向顾客 $j$ 表示这个顾客被这个理发师当做了倒数第 $i$ 个顾客(仅对于该理发师独立而言)。
这样建图方法就出来了:我们建立超级源点 $S$ 连向每一个理发师的每一个状态,流量为 $1$ ,费用为 $0$。每个顾客连向超级汇点 $T$ 一条流量为 $1$ 费用为 $0$ 的边。理发师 $i$ 的第 $k$ 个状态连向顾客 $j$ 一条流量为 $1$ 费用为 $k \times t_{i,j}$ 的弧。
跑一边最小费用最大流就可以求得最小总花费,进而求得最小平均花费。
<h3>代码</h3>
#include <algorithm> #include <iostream> #include <cstring> #include <climits> #include <cstdio> #include <vector> #include <cstdlib> #include <ctime> #include <cmath> #include <queue> #include <stack> #include <map> #include <set> #define Re register #define LL long long #define U unsigned #define FOR(i,a,b) for(Re int i = a;i <= b;++i) #define ROF(i,a,b) for(Re int i = a;i >= b;--i) #define SFOR(i,a,b,c) for(Re int i = a;i <= b;i+=c) #define SROF(i,a,b,c) for(Re int i = a;i >= b;i-=c) #define CLR(i,a) memset(i,a,sizeof(i)) #define BR printf("--------------------n") #define DEBUG(x) std::cerr << #x << '=' << x << std::endl const int MAXN = 1000+5; const int MAXM = 1000000+5; struct Node{ struct Edge first,pre; int dist;bool inq; }node[MAXN]; struct Edge{ Node s,t; Edge next,rev; int flow,cap,cost; }pool[MAXM<<1],*frog = pool; Edge New(Node s,Node *t,int cap,int cost){ Edge *ret = ++frog; *ret = (Edge){s,t,s->first,NULL,0,cap,cost}; return ret; } inline void add(int u,int v,int cap,int cost){ node[u].first = New(&node[u],&node[v],cap,cost); node[v].first = New(&node[v],&node[u],0,-cost); node[u].first->rev = node[v].first; node[v].first->rev = node[u].first; } int N,S,T; inline bool spfa(Node s,Node t){ FOR(i,0,N){ node[i].dist = INT_MAX; node[i].inq = false; node[i].pre = NULL; } std::queue<Node *> q; q.push(s);s->dist = 0;s->inq = true; while(!q.empty()){ Node *v = q.front();q.pop(); v->inq = false; for(Edge *e = v->first;e;e = e->next){ //DEBUG(e->t->dist);DEBUG(v->dist);DEBUG(e->cost); if(e->flow < e->cap && e->t->dist > v->dist + e->cost){ e->t->dist = v->dist + e->cost; e->t->pre = e; if(!e->t->inq){ e->t->inq = true; q.push(e->t); } } } } return t->pre != NULL; } inline int mincost(){ int ret=0; while(spfa(&node[S],&node[T])){ int flow = INT_MAX; for(Node *v = &node[T];v != &node[S];v = v->pre->s){ flow = std::min(flow,v->pre->cap - v->pre->flow); } for(Node *v = &node[T];v != &node[S];v = v->pre->s){ v->pre->flow += flow; v->pre->rev->flow -= flow; ret += flow*v->pre->cost; } } return ret; } int n,m; int tMAXN; inline int getp(int x,int y){ return (x-1)*m+y; } int main(){ scanf("%d%d",&n,&m); FOR(i,1,m) FOR(j,1,n) scanf("%d",&ti); FOR(i,1,n){ FOR(j,1,m){ FOR(k,1,m){ add(getp(i,j),getp(n,m)+k,1,tk*j); } } } S = 0;T = getp(n,m)+m+1; N = T; FOR(i,1,getp(n,m)) add(S,i,1,0); FOR(i,1,m) add(getp(n,m)+i,T,1,0); printf("%.2fn",1.0*mincost() / m); return 0; }
<h2>环游世界</h2>
题目显然可以转化为两个人,限制不能有一条边被走了两次,都到达终点的总距离最小的方案。
然后可以转换为求流量为 $2$ 时最小的费用流。
对于原图的双向边在新图中插入一条双向弧,容量为 $1$,费用为边上的距离,源点连向起点一条费用为 $0$ ,容量为 $2$ 的弧,终点连向汇点一条费用为 $0$,容量为 $2$ 的边。
考虑如何处理“容量为 $2$” 这个限制,发现这个图的特殊性质保证每次增广最多能使流量加一,所以最多增广两次即可。
2 comments
orz %%%吴雨航
orz %%%吴雨航