模拟费用流小记
模拟费用流小记
介绍
费用流作为一种应用面非常广的算法,其可怜的 \(O(nmf)\) 时间复杂度,让我们面对 \(10^5\) 及以上数量级的问题感到心有余而力不足。
但是作为网络流高手是不能容忍这样的事情发生的。
怎么办呢?
不难发现,如果在算法选择正确实在想不到其他的做法的情况下,可以通过观察题目的特殊性质进行模拟,我们称之为,模拟费用流。
这里需要注意的是,模拟费用流并不是真正的对费用流进行模拟,其实是借用了费用流源汇点的思想。或者说,模拟费用流 \(\approx\) 数据结构+反悔贪心。
更加值得注意的是,模拟费用流并不是一个固定的算法,需要对于题目进行一对一的设计,因此并不一定有着像费用流一样的普适性。
A例题
你需要生产 \(k\) 张光盘。每张光盘都要经过两道工序:先在 A 工厂进行挤压,再送到 B 工厂涂上反光层。
你知道每天 A、B 工厂分别加工一张光盘的花费。你现在有 \(n\) 天时间,每天可以先送一张光盘到 A 工厂(或者不送),然后再送一张已经在 A 工厂加工过的光盘到 B 工厂(或者不送),每家工厂一天只能对一张光盘进行操作,同一张光盘在一天内生产出来是允许的。我们假定将未加工的或半成品的光盘保存起来不需要费用。
求生产出 \(k\) 张光盘的最小花费。
\(n,k\le5\times10^5\)
A题解
首先主动降低难度,考虑如果这道题的 \(n\le300\),是不是就是简单的费用流了。
具体而言,建立源点 \(S\) 和汇点 \(T\)、超级汇点 \(TT\)。
然后连接如下边:
- 连接 \(T\to TT\),容量是 \(k\),费用是 \(0\)。
- 将所有的 \(S\to i\),容量是 \(1\),费用是 \(a_i\)。
- 将所有的 \(i\to T\),容量是 \(1\),费用是 \(b_i\)。
- 将所有的 \(i\to i + 1\),容量是 \(INF\),费用是 \(0\)。
然后跑最小费用最大流即可。
但是这个时候的 \(n\) 已经到 \(5\times10^5\) 的数量级了,再用费用流就有点异想天开了。
所以这个时候需要想想这张图的特殊性质了。
一个显然的性质是,在原图中我们将一个流量看做一张光盘,而一张光盘显然需要选择一个点对 \((i,j),i\le j\),让这张光盘在 \(i\) 时刻从源点流入图中,在 \(j\) 时刻流出。
再转化下题意,设 \(x_i\) 表示在 \(i\to i + 1\) 这条边的流量,那么有:
- 选择一个光盘在 \(i\) 时刻流入图中,等价于 \(x_i\gets x_i+1\),同时费用增加 \(a_i\)。
- 选择一个光盘在 \(j\) 时刻流出图中,等价于 \(x_i\gets x_i-1\),同时费用增加 \(b_i\)。
- 同时需要保证任意时刻 \(x_i\ge 0\)。
再再再转化下题意,相当于维护一个括号序列,设(的权值是 \(1\),)的权值是 \(-1\),\(S_i\) 表示从 \(1\to i\) 的权值之和。
那么你需要保证任意时刻都有 \(S_i\ge 0\)。
每次的操作有两种:
- 选择一个点对 \((i,j),i\le j\),在 \(i\) 位置填入
(,在 \(j\) 位置填入),花费是 \(a_i+b_j\)。没有任何限制。 - 选择一个点对 \((i,j),i<j\),在 \(i\) 位置填入
),在 \(j\) 位置填入(,花费是 \(b_i+a_j\)。进行这个操作之前,你需要保证 \(\forall k\in[i,j-1],S_k>0\)。 - 需要留意的是,一个 \(a_i\) 或者 \(b_i\) 如果被选择,之后就不能再被选择了。
问你进行 \(k\) 次的最小花费。
不难发现这玩应可以线段树维护。
具体而言,对于线段树的节点 \([l,r]\),我们需要维护以下信息:
- \(mina\):在 \([l,r]\) 中,\(a\) 的最小值点。
- \(minb\):在 \([l,r]\) 中,\(b\) 的最小值点。
- \(la\):满足 \(\forall k\in[l,la-1],S_k>0\) 的所有可能取值中,\(a\) 的最小值点。
- \(lb\):满足 \(\forall k\in[l,lb-1],S_k>0\) 的所有可能取值中,\(b\) 的最小值点。
- \(minn\):在 \([l,r]\) 中,最小的 \(S_i\) 值。
- \(ans1\):对于所有 \((i,j),i\le j\) 中,最小的 \(a_i+b_j\) 的点对 \((i,j)\)。
- \(ans2\):对于所有 \((i,j),i>j\) 中,在一定要求满足 \(\forall k\in[j,i-1],S_k>0\) 的情况下,最小的 \(a_j+b_i\) 的点对 \((i,j)\)。
- \(ans3\):对于所有 \((i,j),i>j\) 中,不要求一定满足 \(\forall k\in[j,i-1],S_k>0\) 的情况下,最小的 \(a_j+b_i\) 的点对 \((i,j)\)。
尝试维护这样的信息,细节看代码。
然后每次贪心找到最小的点对 \((i,j)\),进行操作即可。
时间复杂度降到了可观的 \(O(k\log n)\)。
后记:需要注意的是,这道题早在2017年就出现在了CF802O上。
而在不久的2018年,在ICPC 2018 World Final 上,这个算法就出现在了题目中(就是接下来的B例题),可惜的是,也许是因为当时科技不够普及,考场上没有切掉这道题的队伍。
在距离 2018 年遥远的今天,模拟费用流已经不再是遥不可及的科技了,更多意义上,这只是个高级数据结构+反悔贪心的题目,有可能会在NOI上出现,所以需要注意。
A代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int x = 0, f = 1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -1;ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * f;
}
const int maxn = 5e5 + 10, INF = 0x3f3f3f3f;
int n, k;
int a[maxn], b[maxn];
struct node1{
int first, second;
node1(int f = 0,int s = 0):first(f),second(s){}
friend bool operator <(node1 x,node1 y){
return a[x.first] + b[x.second] < a[y.first] + b[y.second];}
};
typedef node1 pii;
struct Segment_Tree{
struct node{
int mina, minb, la, lb, minn, tag; pii ans1, ans2,ans3;
node(int mina = 0,int minb = 0,int la = 0,int lb = 0,int minn = 0,int tag = 0,pii a1 = node1(0,0), pii a2 = node1(0,0),
pii a3 = node1(0,0)):mina(mina),minb(minb),la(la),lb(lb),minn(minn),tag(tag),ans1(a1),ans2(a2),ans3(a3){}
}d[maxn << 2];
node mergenode(node l,node r){
node ans = node();
ans.mina = a[l.mina] < a[r.mina] ? l.mina : r.mina;
ans.minb = b[l.minb] < b[r.minb] ? l.minb : r.minb;
ans.minn = min(l.minn,r.minn);
ans.ans1 = min(min(l.ans1,r.ans1),node1(l.mina,r.minb));
ans.ans3 = min(min(l.ans3,r.ans3),node1(r.mina,l.minb));
ans.ans2 = min(l.ans2,r.ans2);
if(l.minn > r.minn){
ans.ans2 = min(ans.ans2,min(node1(r.la,l.minb),l.ans3));
ans.la = a[l.mina] < a[r.la] ? l.mina : r.la; ans.lb = r.lb;
}
else if(l.minn < r.minn){
ans.ans2 = min(ans.ans2,min(node1(r.mina,l.lb),r.ans3));
ans.lb = b[r.minb] < b[l.lb] ? r.minb : l.lb; ans.la = l.la;
}
else{
ans.la = l.la;ans.lb = r.lb;
ans.ans2 = min(ans.ans2,node1(r.la,l.lb));
}
return ans;
}
void pushdown(int p){
if(d[p].tag){
d[p << 1].tag += d[p].tag;d[p << 1].minn += d[p].tag;
d[p << 1 | 1].tag += d[p].tag;d[p << 1 | 1].minn += d[p].tag;
d[p].tag = 0;
}
}
void build(int l = 0,int r = n,int p = 1){
if(l == r){d[p] = node(l,l,l,0,0,0,node1(l,l),node1(0,0),node1(l,l));return;}
int mid = l + r >> 1;
build(l,mid,p << 1);build(mid + 1,r,p << 1 | 1);
d[p] = mergenode(d[p << 1],d[p << 1 | 1]);
}
void update(int l,int r,int pos,int p){
if(l == r && l == pos){return;}
int mid = l + r >> 1;pushdown(p);
if(pos <= mid)update(l,mid,pos,p << 1);
else update(mid + 1,r,pos,p << 1 | 1);
d[p] = mergenode(d[p << 1],d[p << 1 | 1]);
}
void update(int l,int r,int s,int t,int p,int add){
if(s <= l && r <= t){d[p].tag += add;d[p].minn += add;return;}
int mid = l + r >> 1;pushdown(p);
if(s <= mid)update(l,mid,s,t,p << 1,add);
if(mid < t)update(mid + 1,r,s,t,p << 1 | 1,add);
d[p] = mergenode(d[p << 1],d[p << 1 | 1]);
}
void update(int pos){update(0,n,pos,1);}
void update(int s,int t,int add){update(0,n,s,t,1,add);}
}tree;
signed main(){
n = read(); k = read();
a[0] = b[0] = INF;
for(int i = 1;i <= n;i++)a[i] = read();
for(int i = 1;i <= n;i++)b[i] = read();
tree.build(0,n,1);int ans = 0;
while(k--){
int x, y, upd = 0;
if(tree.d[1].ans1 < tree.d[1].ans2){x = tree.d[1].ans1.first, y = tree.d[1].ans1.second;upd = 1;}
else {x = tree.d[1].ans2.first, y = tree.d[1].ans2.second;upd = -1;}
// printf("choose 1 : %lld %lld %lld\n",tree.d[1].ans1.first,tree.d[1].ans1.second,a[tree.d[1].ans1.first] + b[tree.d[1].ans1.second]);
// printf("choose 2 : %lld %lld %lld\n",tree.d[1].ans2.first,tree.d[1].ans2.second,a[tree.d[1].ans2.first] + b[tree.d[1].ans2.second]);
ans += a[x] + b[y];a[x] = b[y] = INF;
tree.update(x);tree.update(y);
if(x == y)continue;
tree.update(min(x, y),max(x, y) - 1,upd);
}
printf("%lld\n",ans);
return 0;
}
/*
8 4
3 8 7 9 9 4 6 8
2 5 9 4 3 8 9 1
*/
B例题
P6943 [ICPC2018 WF] Conquer The World
给定一个 \(n\) 点 \(n-1\) 边的无向图,每条边 \((u,v)\) 有边权 \(c\)。
现在第 \(i\) 个点有 \(x_i\) 的点权,每个点需要 \(y_i\) 的点权,所以你可以移动点权到不同的点上。移动一条边上的点 \(u\) 的 \(k\) 个单位点权到 \(v\) 要用 \(k \times c\) 的代价。
求满足所有点的需要的最小代价。
\(1 \le n \le 2.5 \times 10^5\),\(1 \le u,v \le n\),\(1 \le c \le 10^6\),\(0 \le \sum y_i\le \sum x_i \le 10^6\)。
B题解
这个有着显然的网络流建模方法:
首先将每条边连接双向的网络流边,容量正无穷,费用就是边权。
然后对于每个点,如果剩余点权为正,就向汇点连边,如果剩余点权为负,则从源点连边。
然后跑最小费用最大流即可。
但是现在的 \(n\le2.5\times10^5\),怎么办呢?
考虑一个性质,就是所有的路径一定不会交叉。
考虑反悔贪心,如果设现在所在节点是 \(x\),那么将深度最小的源点和汇点相匹配,尝试流一个流量,费用显然好算。
然后考虑如何反悔,很简单,对于一次匹配 \((i,j)\),就让 \(2\times dep_x-dep_i\) 和 \(2\times dep_x-dep_j\) 加入一个堆中,这样的话只要后来选择了其中一个点,那么这对匹配的贡献就减除了。
然后每次向上进行合并以及进行决策的时候用左偏堆维护即可,合并和查询都是 \(O(\log n)\) 的。
哦对了,因为每个需求点一定需要匹配,那么就让加入的 \(dep_x\) 再减去 \(INF\),这样一定是最小的,就一定能够匹配上。
B代码
#include<bits/stdc++.h>
#include<ext/pb_ds/priority_queue.hpp>
#define ll long long
using namespace std;
inline int read(){
int x = 0, f = 1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -1;ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * f;
}
const int maxn = 2.5e5 + 10;
const ll INF = 1e13;
int n;
ll x[maxn], y[maxn];
int tot, head[maxn];
struct edge{
int to, nexte;ll wei;
edge(int to = 0,int ne = 0,ll we = 0):to(to),nexte(ne),wei(we){}
}e[maxn << 1];
void add(int u,int v,ll w){e[++tot] = edge(v,head[u],w);head[u] = tot;}
ll ans, cnt, dep[maxn];
__gnu_pbds::priority_queue<ll,greater<ll> > A[maxn],B[maxn];
void dfs(int u,int f){
for(int i = x[u] + 1;i <= y[u];i++)
A[u].push(dep[u] - INF),cnt++;
for(int i = y[u] + 1;i <= x[u];i++)
B[u].push(dep[u]);
for(int i = head[u];i;i = e[i].nexte){
int v = e[i].to;ll w = e[i].wei;
if(v == f)continue;
dep[v] = dep[u] + w;dfs(v, u);
A[u].join(A[v]);B[u].join(B[v]);
}
while(!A[u].empty() && !B[u].empty() && A[u].top() + B[u].top() < 2LL * dep[u]){
ll xx = A[u].top(), yy = B[u].top(); A[u].pop(); B[u].pop();
ans += xx + yy - 2LL * dep[u];
A[u].push(2 * dep[u] - yy);B[u].push(2 * dep[u] - xx);
}
}
signed main(){
n = read();int u, v, w;
for(int i = 1;i < n;i++){
u = read();v = read(); w = read();
add(u, v, w);add(v, u, w);
}
for(int i = 1;i <= n;i++){x[i] = read();y[i] = read();}
dfs(1 ,1);
printf("%lld\n",ans + cnt * INF);
return 0;
}
中场总结
某种意义上来说,模拟费用流虽然在字面上带了一个子串“费用流”,但是其实本质上是数据结构+反悔贪心,从这几道题也能看出来。
C例题
cyrcyr 今天在种树,他在一条直线上挖了 \(n\) 个坑。这 \(n\) 个坑都可以种树,但为了保证每一棵树都有充足的养料,cyrcyr 不会在相邻的两个坑中种树。而且由于 cyrcyr 的树种不够,他至多会种 \(k\) 棵树。假设 cyrcyr 有某种神能力,能预知自己在某个坑种树的获利会是多少(可能为负),请你帮助他计算出他的最大获利。
C题解
某种意义上来说,这道题看出费用流的人多少有点魔怔了。
但是还是先说一下魔怔的做法(bushi)
我们尝试建立这样一个图:
- 建立源点 \(S\) 和汇点 \(T\)。
- 对所有 \(2\mid i\) 的点 \(i\),连接 \(S\to i\),费用是 \(0\),容量为 \(1\)。
- 对所有 \(2\nmid i\) 的点 \(i\),连接 \(i\to T\),费用是 \(0\),容量为 \(1\)。
- 对所有间隔 \(i\to i+1\),从奇数点连向偶数点,费用是获利,容量为 \(1\)。
然后在图中跑最小费用最大流即可。
然后再来说一说不魔怔的做法。
从费用流的视角出发,不难发现,最优解一定不会退回源边和汇边,被退回的只可能是中间的边,考虑怎么才能提供像费用流一样的反悔机制。
不难发现,出现反悔当且仅当在之前选了 \(x\),但是选择周围的 \(x-1,x+1\) 会更优。
所以每次选择一个点的时侯,加入 \(val_{x+1}+val_{x-1}-val_x\),以提供反悔的机会。
这个直接堆维护一下即可。
C代码
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
#include<queue>
#define ll long long
using namespace std;
int n, k;
const int maxn = 1e6 * 5 + 10;
int w[maxn],leftt[maxn],rightt[maxn];
bool book[maxn];
struct node{
int idx,w;
}tmp;
bool operator < (node a,node b){
return a.w < b.w;
}
priority_queue<node> que;
signed main(){
scanf("%d%d",&n,&k);
for(int i = 1;i <= n;i++){
scanf("%d",&w[i]);
tmp.idx = i;
tmp.w = w[i];
leftt[i] = i - 1;
rightt[i] = i + 1;
que.push(tmp);
}
leftt[n + 1] = n;
rightt[0] = 1;
long long ans = 0;
for(int i = 1;i <= k;i++){
while(!que.empty() && book[que.top().idx])que.pop();
tmp = que.top();que.pop();
if(tmp.w < 0)break;
ans += tmp.w;
book[leftt[tmp.idx]] = book[rightt[tmp.idx]] = 1;
tmp.w = w[leftt[tmp.idx]] + w[rightt[tmp.idx]] - w[tmp.idx];
w[tmp.idx] = tmp.w;//由于取点时可能只是局部最优解,而全局最优解可能取得是当前点的左右,因此提供反悔机制
//如果取了一个点j,则将w[left[j]] + w[right[j]] - w[j]加入到队列中,如果取了新加入的点,就相当于删除了j
//并取left[j],right[j],间接的解决了问题
leftt[tmp.idx] = leftt[leftt[tmp.idx]];
rightt[tmp.idx] = rightt[rightt[tmp.idx]];
rightt[leftt[tmp.idx]] = tmp.idx;
leftt[rightt[tmp.idx]] = tmp.idx;
que.push(tmp);
}
printf("%lld\n",ans);
return 0;
}
D例题
已知接下来N天的股票价格,每天你可以买进一股股票,卖出一股股票,或者什么也不做.N天之后你拥有的股票应为0,当然,希望这N天内能够赚足够多的钱。
D题解
不难发现,可以建出这张图:
- 建立源点 \(S\) 和汇点 \(T\)。
- 第 \(i\) 天连接 \(S\to i\),容量为 \(1\),费用为 \(-c_i\);连接 \(i\to T\),容量为 \(1\),费用为 \(c_i\)。
最后答案就是最大费用任意流。
考虑这玩应的策略。
不难发现,每天只有两个策略:
- 买入一张股票,在之后的某一天卖出。
- 更改之前某一张股票的卖出时间,在今天卖出。
这东西显然可以堆维护,细节看代码。
D代码
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int x = 0, f = 1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -1;ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * f;
}
typedef long long ll;
const int maxn = 3e5 + 10;
priority_queue<ll> que;
int n;
signed main(){
n = read();ll u, ans = 0;
for(int i = 1;i <= n;i++){
u = read();que.push(-u);//这个-u指的是买入股票花费
if(que.empty() || que.top() + u < 0)continue;
ans += u + que.top();que.pop();que.push(-u);
//这个-u指的是,更改一个股票的卖出时间,原来的卖出收益就没了,需要减去
}
printf("%lld\n",ans);
return 0;
}
E例题
CF703I Olympiad in Programming and Sports
有 \(n\) 个学生每人有两种技能,分别是 \(a,b\) 表示编程能力和运动能力。你需要将他们分为两个团队分别参加编程比赛和体育比赛,编程团队有 \(p\) 人,体育团队有 \(s\) 人,一个学生只能参加其中一项。每个团队的力量是其成员在对应领域能力总和,请问如何分配能使得两个团队的实力和最大?
需要给出构造方案。
- \(2\le n\le 3\times 10^3\) 且 \(p+s\le n\)。
- \(1\le a_i,b_i\le 3000\)。
E题解
虽然但是这个数据范围直接上费用流好像能过(
老规矩,先讲费用流怎么做。
按照如下方式建图:
- 建立源点 \(S\) 和汇点 \(T\)。
- 每个人 \(i\) 连接 \(S\to i\),费用 \(0\),容量 \(1\)。
- 每个人到编程团队(称为集合 \(A\))连接 \(i\to A\),费用 \(a_i\),容量 \(1\)。
- 每个人到体育团队(称为集合 \(B\))连接 \(i\to B\),费用 \(b_i\),容量 \(1\)。
- 连接 \(A\to T\),费用 \(0\),容量 \(p\)。
- 连接 \(B\to T\),费用 \(0\),容量 \(s\)。
直接跑最大费用最大流是可过的。
但是但是,如果我把 \(n\) 的范围开到 \(10^5\),阁下又当如何应对呢?
老规矩,考虑贪心策略。
如果我们先将前 \(p\) 大的 \(a_i\) 全都分配给 \(A\),那么我们能够对剩下的人进行的操作只有两个:
- 将一个人加入 \(B\)。
- 将一个人加入 \(A\),同时将 \(A\) 中的其中一个人调到 \(B\) 中。
每次选择最优的策略,进行 \(s\) 次即可。
这东西搞三个堆就能维护了。
E代码
#include<bits/stdc++.h>
using namespace std;
inline int read(){
int x = 0, f = 1;char ch = getchar();
while(ch < '0' || ch > '9'){if(ch == '-') f = -1;ch = getchar();}
while(ch >= '0' && ch <= '9'){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
return x * f;
}
const int maxn = 3e3 + 10;
struct node{
int val, id;
node(int v = 0,int i = 0):val(v),id(i){}
friend bool operator < (node a,node b){return a.val < b.val;}
};
priority_queue<node,vector<node>,less<node> > q1,q2,q3;
int n, x, y;
int a[maxn], b[maxn];
int opt[maxn];
signed main(){
n = read();x = read(); y = read();int ans = 0;
for(int i = 1;i <= n;i++)q1.push(node(a[i] = read(),i));
for(int i = 1;i <= n;i++)q2.push(node(b[i] = read(),i));
for(int i = 1;i <= n;i++)q3.push(node(b[i] - a[i],i));
for(int i = 1;i <= x;i++){
int pos = q1.top().id; opt[pos] = 1;
ans += q1.top().val; q1.pop();
}
while(!q1.empty())q1.pop();
for(int i = 1;i <= n;i++)if(opt[i] != 1)q1.push(node(a[i],i));
for(int i = 1;i <= y;i++){
int op = 0, j, k,val = 0;
while(!q2.empty() && opt[q2.top().id] != 0)q2.pop();
if(!q2.empty()){
val = q2.top().val;
j = q2.top().id; op = 1;
}
while(!q1.empty() && opt[q1.top().id] != 0)q1.pop();
while(!q3.empty() && opt[q3.top().id] != 1)q3.pop();
if(!q1.empty() && !q3.empty()){
if(q3.top().val + q1.top().val > val){
val = q3.top().val + q1.top().val;
j = q3.top().id; k = q1.top().id; op = 2;
}
}
if(op == 1){ans += val;q2.pop();opt[j] = 2;}
else{ans += val;opt[k] = 1;opt[j] = 2;q1.pop();q3.pop();}
}
printf("%d\n",ans);
for(int i = 1;i <= n;i++)if(opt[i] == 1)printf("%d ",i);puts("");
for(int i = 1;i <= n;i++)if(opt[i] == 2)printf("%d ",i);puts("");
return 0;
}

浙公网安备 33010602011771号