浅谈一类技巧:贪心模拟费用流

我是初学者,如果我哪里说的有问题还请指正,谢谢!


先回顾一下基本的网络流问题。

最大流

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 KL 次选不等对。证明:这样操作得到的答案肯定合法,且每个答案都能从这样的操作过来。

这样我们就将题目变成了做操作的问题,容易使用费用流求解:

点: 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,KL,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 被选])

称「退流」为包含反平行边的增广路,「正向增广路」为不包含反平行边的增广路,「被退流」指一条边被包含在一次退流中。

一种解释

接下来考虑寻找这张图中增广的规律,首先发现这些性质:

  1. 不考虑反平行边,则可能的增广路就两种: 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) SLiRiT(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) (SLiRjT)(i=j,st(i)=st(j)=(0,0),cost=ai+bj)
  2. 最后的状态可以只使用上面两种增广路得到。(意即不考虑反平行边也可能找到答案)
    这没啥问题吧,如果不能这怎么可能是答案呢。。
  3. 如果 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 SLiRiT 这三条边
    我们已经找到了最优的方式,也就是不使用 A − B A-B AB 的流量,理应删除。这说明了网路中不会有 i i i 满足 s t ( i ) = ( 1 , 1 ) st(i)=(1,1) st(i)=(1,1) 且用了 A − B A-B AB 的流量,这大大简化了我们分类讨论的过程。

接下来,我们分类讨论,逐一判断。

先考虑此次退流同时影响了两个配对。我们先考虑 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 SL2R2B,由于要同时影响两个,所以 S − L 2 − R 2 − B − A − L 1 − R 1 S-L_2-R_2-B-A-L_1-R_1 SL2R2BAL1R1,此时只能走到 T T T,这也说明了退流不会同时影响三个配对。
(2) 接下来走 S − L 2 − A − L 1 − R 1 S-L_2-A-L_1-R_1 SL2AL1R1,只能走向 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 SL3AL1R1,只能走到 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) SLiRiBALjRjT(st(i)=(0,1),st(j)=(1,0),cost=ai+bj)

所以现在我们可以只画出一个

我们走 S − L 2 S-L_2 SL2
(1) S − L 2 − A S-L_2-A SL2A,一种是 S − L 2 − A − B − R 3 − T S-L_2-A-B-R_3-T SL2ABR3T,这种上面提到过,合并了;一种是 S − L 2 − A − L 1 − R 1 − T S-L_2-A-L_1-R_1-T SL2AL1R1T,这种会
(2) S − L 2 − R 2 S-L_2-R_2 SL2R2,只能往上走 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 SL2R2BAL1R1T。和上面有两条时提到的相同,我们直接合并。
(3) S − L 2 − R 2 − B − R 1 − T S-L_2-R_2-B-R_1-T SL2R2BR1T,此时 L 1 , R 1 L_1,R_1 L1,R1 使用了 A − B A-B AB,舍。

然后尝试走 S − L 3 − A − L 1 − R 1 − T S-L_3-A-L_1-R_1-T SL3AL1R1T

另一种解释

我们不关心中间的边是如何流过的,这是因为中间的边都没有费用。

因此不妨假定每时刻流的都是最优策略,意即每时刻都将网络重构,使得被选的数不变,且让 A − B A-B AB 剩余流量尽量多。

我们同样也将 s t ( i ) = ( 1 , 1 ) st(i)=(1,1) st(i)=(1,1) i i i 删除。

加上这两个限制之后,我们就不需要枚举具体路径。

  1. 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) SLiRiT(st(i)=(0,0),cost=ai+bi)
  2. 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) SLiRjT(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

  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) SLiRiBALjRjT(st(i)=(0,1),st(j)=(1,0),cost=ai+bj,flow)

其它的不会对 A-B 流量造成影响

  1. 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) SLiALjRjT(st(i)=(0,0),st(j)=(1,0),cost=ai+bj)
  2. 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) SLiRiBRjT(st(i)=(0,1),st(j)=(0,0),cost=ai+bj)
结果

综上所述,我们只有五种增广路:

  1. 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) SLiRiT(st(i)=(0,0),cost=ai+bi)
  2. 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) SLiRjT(st(i)=st(j)=(0,0),cost=ai+bj,++flow)
  3. 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) SLiRiBALjRjT(st(i)=(0,1),st(j)=(1,0),cost=ai+bj,flow)
  4. 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) SLiALjRjT(st(i)=(0,0),st(j)=(1,0),cost=ai+bj)
  5. 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) SLiRiBRjT(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 SLiABRij 这种出现时我们直接舍弃了,这样能让我们处理这种问题更加便捷。

至于上面五种操作,开五个堆来维护就行了,时间复杂度 Θ ( 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

特点:图的独特保证了时间复杂度的正确

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值