我是初学者,如果我哪里说的有问题还请指正,谢谢!
先回顾一下基本的网络流问题。
最大流
Ford-Fulkerson 方法指出,在「残量网络」(对原网络 G G G 的每条边增加反平行边) G f G_f Gf 上不停增流,每次寻找一条「增广路径」(从 S S S 到 T T T 的一条简单路径)并对增广路上所有边修改流量,这个方法能够正确地得出网络流。
Edmonds-Karp 算法通过广度优先算法寻找增广路,可以证明其时间复杂度为 Θ ( V E 2 ) \Theta(VE^2) Θ(VE2)。
Dinic 算法通过分层以及多路增广等手段寻找增广路,可以证明其时间复杂度为 Θ ( V 2 E ) \Theta(V^2E) Θ(V2E)。
费用流
Minimum-Cost-Maximum-Flow 算法在 Edmonds-Karp 算法的基础上将广度优先搜索改成 Shortest-Path-Faster-Algorithm 寻找增广路。
接下来我们都默认上述说法是正确的(重点:最短路,简单路径),这样能够帮助我们分析问题。
贪心模拟费用流,就是对题目刻画费用流模型,并找到增广的一些规律,进而使用更加快速的方法进行增广,达到优化时间复杂度的目的。
一般而言,一个模型的增广是没有普遍规律的,但这种题目的模型一般较特殊,因此我们可以通过一些手法进行分析。要注意,分析的时候不能只依赖于费用流模型,结合原题的性质能够更大帮助你的分析。
下面将会对几道例题进行分析。
「NOI2019」序列
我们称「选对」为选择了一对数,「选等对」为选择了一对下标相同的数,「选不等对」为选择了一对下标不同的数。
一定可以通过 K K K 次选对得到答案,其中最多 K − L K-L K−L 次选不等对。证明:这样操作得到的答案肯定合法,且每个答案都能从这样的操作过来。
这样我们就将题目变成了做操作的问题,容易使用费用流求解:
点: S , L 1 , 2 , ⋯ , n , R 1 , 2 , ⋯ , n , A , B , C , T S, L_{1,2,\cdots,n}, R_{1,2,\cdots,n}, A, B, C, T S,L1,2,⋯,n,R1,2,⋯,n,A,B,C,T,共 2 n + 4 2n+4 2n+4 个点。
边: ( S , L i , 1 , − a i ) , ( L i , A , 1 , 0 ) , ( A , B , K − L , 0 ) , ( L i , R i , 1 , 0 ) , ( B , R i , 1 , 0 ) , ( R i , C , 1 , − b i ) , ( C , T , K , 0 ) (S,L_i,1,-a_i), (L_i,A,1,0), (A,B,K-L,0), (L_i,R_i,1,0), (B,R_i,1,0), (R_i,C,1,-b_i), (C,T,K,0) (S,Li,1,−ai),(Li,A,1,0),(A,B,K−L,0),(Li,Ri,1,0),(B,Ri,1,0),(Ri,C,1,−bi),(C,T,K,0),共 5 n + 2 5n+2 5n+2 条边。
跑最小费用最大流。
下面为了叙述方便,我们称 s t ( i ) st(i) st(i) 为一个二元组,表示为 s t ( i ) = ( [ a i 被选 ] , [ b i 被选 ] ) st(i)=([a_i \text{ 被选}], [b_i \text{ 被选}]) st(i)=([ai 被选],[bi 被选])。
称「退流」为包含反平行边的增广路,「正向增广路」为不包含反平行边的增广路,「被退流」指一条边被包含在一次退流中。
一种解释
接下来考虑寻找这张图中增广的规律,首先发现这些性质:
- 不考虑反平行边,则可能的增广路就两种: S − L i − R i − T ( s t ( i ) = ( 0 , 0 ) , c o s t = a i + b i ) S-L_i-R_i-T (st(i)=(0,0),cost=a_i+b_i) S−Li−Ri−T(st(i)=(0,0),cost=ai+bi) 和 ( S − L i − R j − T ) ( i ≠ j , s t ( i ) = s t ( j ) = ( 0 , 0 ) , c o s t = a i + b j ) (S-L_i-R_j-T) (i\not= j, st(i)=st(j)=(0,0),cost=a_i+b_j) (S−Li−Rj−T)(i=j,st(i)=st(j)=(0,0),cost=ai+bj)。
- 最后的状态可以只使用上面两种增广路得到。(意即不考虑反平行边也可能找到答案)
这没啥问题吧,如果不能这怎么可能是答案呢。。 - 如果
s
t
(
i
)
=
(
1
,
1
)
st(i)=(1,1)
st(i)=(1,1),我们将不再考虑
i
i
i,也即删除
S
−
L
i
−
R
i
−
T
S-L_i-R_i-T
S−Li−Ri−T 这三条边。
我们已经找到了最优的方式,也就是不使用 A − B A-B A−B 的流量,理应删除。这说明了网路中不会有 i i i 满足 s t ( i ) = ( 1 , 1 ) st(i)=(1,1) st(i)=(1,1) 且用了 A − B A-B A−B 的流量,这大大简化了我们分类讨论的过程。
接下来,我们分类讨论,逐一判断。
先考虑此次退流同时影响了两个配对。我们先考虑
S
S
S 走到
L
2
(
s
t
=
(
0
,
1
)
)
L_2(st=(0,1))
L2(st=(0,1))。
(1) 接下来走
S
−
L
2
−
R
2
−
B
S-L_2-R_2-B
S−L2−R2−B,由于要同时影响两个,所以
S
−
L
2
−
R
2
−
B
−
A
−
L
1
−
R
1
S-L_2-R_2-B-A-L_1-R_1
S−L2−R2−B−A−L1−R1,此时只能走到
T
T
T,这也说明了退流不会同时影响三个配对。
(2) 接下来走
S
−
L
2
−
A
−
L
1
−
R
1
S-L_2-A-L_1-R_1
S−L2−A−L1−R1,只能走向
T
T
T,不能同时两个,舍。
然后考虑从 S S S 走到 L 3 L_3 L3,此时走 S − L 3 − A − L 1 − R 1 S-L_3-A-L_1-R_1 S−L3−A−L1−R1,只能走到 T T T,舍。
所以同时影响两个的只会有 S − L i − R i − B − A − L j − R j − T ( s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 1 , 0 ) , c o s t = a i + b j ) S-L_i-R_i-B-A-L_j-R_j-T (st(i)=(0,1),st(j)=(1,0), cost=a_i+b_j) S−Li−Ri−B−A−Lj−Rj−T(st(i)=(0,1),st(j)=(1,0),cost=ai+bj)。
所以现在我们可以只画出一个
我们走
S
−
L
2
S-L_2
S−L2,
(1)
S
−
L
2
−
A
S-L_2-A
S−L2−A,一种是
S
−
L
2
−
A
−
B
−
R
3
−
T
S-L_2-A-B-R_3-T
S−L2−A−B−R3−T,这种上面提到过,合并了;一种是
S
−
L
2
−
A
−
L
1
−
R
1
−
T
S-L_2-A-L_1-R_1-T
S−L2−A−L1−R1−T,这种会
(2)
S
−
L
2
−
R
2
S-L_2-R_2
S−L2−R2,只能往上走
B
B
B,接下来选择回到
A
A
A,走
S
−
L
2
−
R
2
−
B
−
A
−
L
1
−
R
1
−
T
S-L_2-R_2-B-A-L_1-R_1-T
S−L2−R2−B−A−L1−R1−T。和上面有两条时提到的相同,我们直接合并。
(3)
S
−
L
2
−
R
2
−
B
−
R
1
−
T
S-L_2-R_2-B-R_1-T
S−L2−R2−B−R1−T,此时
L
1
,
R
1
L_1,R_1
L1,R1 使用了
A
−
B
A-B
A−B,舍。
然后尝试走 S − L 3 − A − L 1 − R 1 − T S-L_3-A-L_1-R_1-T S−L3−A−L1−R1−T。
另一种解释
我们不关心中间的边是如何流过的,这是因为中间的边都没有费用。
因此不妨假定每时刻流的都是最优策略,意即每时刻都将网络重构,使得被选的数不变,且让 A − B A-B A−B 剩余流量尽量多。
我们同样也将 s t ( i ) = ( 1 , 1 ) st(i)=(1,1) st(i)=(1,1) 的 i i i 删除。
加上这两个限制之后,我们就不需要枚举具体路径。
- S − L i − R i − T ( s t ( i ) = ( 0 , 0 ) , c o s t = a i + b i ) S-L_i-R_i-T (st(i)=(0,0),cost=a_i+b_i) S−Li−Ri−T(st(i)=(0,0),cost=ai+bi)
- S − L i − R j − T ( s t ( i ) = s t ( j ) = ( 0 , 0 ) , c o s t = a i + b j , + + f l o w ) S-L_i-R_j-T(st(i)=st(j)=(0,0),cost=a_i+b_j,++flow) S−Li−Rj−T(st(i)=st(j)=(0,0),cost=ai+bj,++flow)
这些肯定有。
然后可以枚举两者的状态了,如果 s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 1 , 0 ) st(i)=(0,1),st(j)=(1,0) st(i)=(0,1),st(j)=(1,0),则重构后 A-B 流量 -1
- S − L i − R i − B − A − L j − R j − T ( s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 1 , 0 ) , c o s t = a i + b j , − − f l o w ) S-L_i-R_i-B-A-L_j-R_j-T (st(i)=(0,1),st(j)=(1,0), cost=a_i+b_j,--flow) S−Li−Ri−B−A−Lj−Rj−T(st(i)=(0,1),st(j)=(1,0),cost=ai+bj,−−flow)
其它的不会对 A-B 流量造成影响
- S − L i − A − L j − R j − T ( s t ( i ) = ( 0 , 0 ) , s t ( j ) = ( 1 , 0 ) , c o s t = a i + b j ) S-L_i-A-L_j-R_j-T (st(i)=(0,0),st(j)=(1,0),cost=a_i+b_j) S−Li−A−Lj−Rj−T(st(i)=(0,0),st(j)=(1,0),cost=ai+bj)
- S − L i − R i − B − R j − T ( s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 0 , 0 ) , c o s t = a i + b j ) S-L_i-R_i-B-R_j-T (st(i)=(0,1),st(j)=(0,0),cost=a_i+b_j) S−Li−Ri−B−Rj−T(st(i)=(0,1),st(j)=(0,0),cost=ai+bj)
结果
综上所述,我们只有五种增广路:
- S − L i − R i − T ( s t ( i ) = ( 0 , 0 ) , c o s t = a i + b i ) S-L_i-R_i-T (st(i)=(0,0),cost=a_i+b_i) S−Li−Ri−T(st(i)=(0,0),cost=ai+bi)
- S − L i − R j − T ( s t ( i ) = s t ( j ) = ( 0 , 0 ) , c o s t = a i + b j , + + f l o w ) S-L_i-R_j-T(st(i)=st(j)=(0,0),cost=a_i+b_j,++flow) S−Li−Rj−T(st(i)=st(j)=(0,0),cost=ai+bj,++flow)
- S − L i − R i − B − A − L j − R j − T ( s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 1 , 0 ) , c o s t = a i + b j , − − f l o w ) S-L_i-R_i-B-A-L_j-R_j-T (st(i)=(0,1),st(j)=(1,0), cost=a_i+b_j,--flow) S−Li−Ri−B−A−Lj−Rj−T(st(i)=(0,1),st(j)=(1,0),cost=ai+bj,−−flow)
- S − L i − A − L j − R j − T ( s t ( i ) = ( 0 , 0 ) , s t ( j ) = ( 1 , 0 ) , c o s t = a i + b j ) S-L_i-A-L_j-R_j-T (st(i)=(0,0),st(j)=(1,0),cost=a_i+b_j) S−Li−A−Lj−Rj−T(st(i)=(0,0),st(j)=(1,0),cost=ai+bj)
- S − L i − R i − B − R j − T ( s t ( i ) = ( 0 , 1 ) , s t ( j ) = ( 0 , 0 ) , c o s t = a i + b j ) S-L_i-R_i-B-R_j-T (st(i)=(0,1),st(j)=(0,0),cost=a_i+b_j) S−Li−Ri−B−Rj−T(st(i)=(0,1),st(j)=(0,0),cost=ai+bj)
可以发现,我们在分析的时候没有局限于模型,我们在原题的基础上思考了一些贪心手段,比如当 S − L i − A − B − R i − j S-L_i-A-B-R_i-j S−Li−A−B−Ri−j 这种出现时我们直接舍弃了,这样能让我们处理这种问题更加便捷。
至于上面五种操作,开五个堆来维护就行了,时间复杂度 Θ ( n log n ) \Theta(n\log n) Θ(nlogn)。
代码(我写的比较慢):
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int read() {
int ret = 0; char c = getchar();
while (c < '0' || c > '9') c = getchar();
while (c >= '0' && c <= '9') ret = ret * 10 + c - '0', c = getchar();
return ret;
}
const int MAXN = 500005;
const ll INF = LLONG_MAX >> 1;
int Data, N, K, L, a[MAXN], b[MAXN], s[MAXN]; ll ans;
struct cmpa { bool operator()(int x, int y) { return a[x] < a[y]; } };
struct cmpb { bool operator()(int x, int y) { return b[x] < b[y]; } };
struct cmpc { bool operator()(int x, int y) { return a[x] + b[x] < a[y] + b[y]; } };
priority_queue<int, vector<int>, cmpa> A0, S2;
priority_queue<int, vector<int>, cmpb> B0, S1;
priority_queue<int, vector<int>, cmpc> S0;
int main() {
Data = read();
while (Data--) {
N = read(), K = read(), L = read(); int i, x = K, cnt = K - L, id, ret;
while (!S0.empty()) S0.pop(); while (!A0.empty()) A0.pop(); while (!B0.empty()) B0.pop(); while (!S1.empty()) S1.pop(); while (!S2.empty()) S2.pop();
for (i = 1; i <= N; ++i) a[i] = read();
for (i = 1; i <= N; ++i) b[i] = read(), S0.push(i), A0.push(i), B0.push(i), s[i] = 0;
ans = 0;
while (x--) {
id = ret = 0;
if (!S0.empty() && ret < a[S0.top()] + b[S0.top()]) id = 1, ret = a[S0.top()] + b[S0.top()];
if (cnt > 0 && !A0.empty() && !B0.empty() && ret < a[A0.top()] + b[B0.top()]) id = 2, ret = a[A0.top()] + b[B0.top()];
if (!S2.empty() && !S1.empty() && ret < a[S2.top()] + b[S1.top()]) id = 3, ret = a[S2.top()] + b[S1.top()];
if (!S2.empty() && !B0.empty() && ret < a[S2.top()] + b[B0.top()]) id = 4, ret = a[S2.top()] + b[B0.top()];
if (!A0.empty() && !S1.empty() && ret < a[A0.top()] + b[S1.top()]) id = 5, ret = a[A0.top()] + b[S1.top()];
ans += ret;
if (id == 1) s[S0.top()] = 3, s[S0.top()] = 3;
else if (id == 2) s[A0.top()] = 1, s[B0.top()] = 2, --cnt, S1.push(A0.top()), S2.push(B0.top());
else if (id == 3) s[S2.top()] = 3, s[S1.top()] = 3, ++cnt;
else if (id == 4) s[S2.top()] = 3, s[B0.top()] = 2, S2.push(B0.top());
else s[A0.top()] = 1, s[S1.top()] = 3, S1.push(A0.top());
while (!A0.empty() && s[A0.top()] != 0) A0.pop();
while (!S2.empty() && s[S2.top()] != 2) S2.pop();
while (!B0.empty() && s[B0.top()] != 0) B0.pop();
while (!S1.empty() && s[S1.top()] != 1) S1.pop();
while (!S0.empty() && s[S0.top()] != 0) S0.pop();
}
printf("%lld\n", ans);
}
return 0;
}
咕?
「APIO/CTSC 2007」数据备份
特点:退流方式比较简单
「XJOI」Apple Business
特点:图的独特保证了时间复杂度的正确