模拟费用流学习笔记


算是很早之前就挖了的坑吧,只是一直没打算填,今天碰到题目了刚好回来填坑
所谓模拟费用流,就是根据建出来的模型的一些特殊性奇技淫巧来以更加优秀的复杂度实现增广。
可惜这个东西不同的题目有不同的方法,属于一种思路而非套路,只能靠写几道题目来巩固。


先来一道简单的热热身

小Q最近沉迷于《跳伞求生》游戏。他组建了一支由 \(n\) 名玩家(包括他自己)组成的战队,编号依次为 \(1\)\(n\) 。这个游
戏中,每局游戏开始时,所有玩家都会从飞机上跳伞,选择一个目的地降落,跳伞和降落的时间有早有晚。在某局
游戏降落前,他们在空中观察发现地面上一共有 \(m\) 间房子,编号依次为 \(1\)\(m\) 。其中每间房子恰好有一名敌人早于他
们到达。小Q战队的第 \(i\) 名玩家拥有 \(a_i\) 发子弹,地面上第 \(i\) 间房子里的敌人拥有$ b_i$ 发子弹,消灭他可以获得 \(c_i\) 点积
分。每名玩家必须且只能选择一间房子降落,然后去消灭里面的敌人。若第 \(i\) 名玩家选择了第 \(j\) 间房子,如果 \(a_i>b_ j\) ,那么他就可以消灭该敌人,获得 \(a_i-b_j+c_j\) 的团队奖励积分,否则他会被敌人消灭。为了防止团灭,小Q不允
许多名玩家选择同一间房子,因此如果某位玩家毫无利用价值,你可以选择让他退出游戏。因为房子之间的距离过
长,你可以认为每名玩家在降落之后不能再去消灭其它房间里的敌人。作为小Q战队的指挥,请制定一套最优的降
落方案,使得最后获得的团队奖励总积分最大

\(n,m\leq 1000\) 的时候要怎么做?
不难想到可以建一个费用流模型,其中所有玩家连向 \(S\),敌人全部连向 \(T\),玩家向打的过的敌人再连边。

但是当 \(n,m\leq1e5\) 的时候,费用流的效率就有点太低了。
通过观察,可以发现这个模型满足这样的特点:

对于一个玩家点 \(i\), 如果他比玩家点 \(j\) 更优,那么 \(j\) 连向的敌人 \(i\) 肯定也是连向的。

也就是说按照紫蛋子弹数量排序之后,后面加入的玩家点肯定都是有机会增广前面加入的玩家点的。
同时,还可以发现:

如果按照顺序加入玩家点来增广,被增广过的玩家点不可能再去增广别的点。

证明:
考虑新加入了一个玩家点 \(a_i\),它增广了玩家点 \(a_j\),连向了敌人点 \(b_j\),说明对于前面已经加入了的任意玩家点 \(k\),有\(a_k-b_k+c_k \geq a_j-b_j+c_j\)
这意味着 \(a_j\) 再不可能去增广别的玩家点了,所以可以直接删除点 \(a_j\)

因此,可以考虑将 \(a\) 排序,枚举 \(i\) ,然后把能够匹配的 \(b\) 加入到堆 \(q\) 中,如果成功匹配则往堆中加入 \(-a_i\) ,用来模拟退费的过程

代码
  

int n,m;
const int MAXN=1e5+7;
ll ans,inf=1e10;
struct node
{
    ll x,y;
    bool operator<(const node &p)const{
        return x==p.x?y<p.y:x<p.x;
    }
}a[MAXN<<1];
priority_queue<ll> q;
int main(){
    n=read(),m=read();
    for(ri i=1;i<=n;++i) a[i].x=read(),a[i].y=-1;
    for(ri i=n+1;i<=n+m;++i) a[i].x=read(),a[i].y=read();
    sort(a+1,a+n+m+1);
    for(ri i=1;i<=n+m;++i){
        if(a[i].y==-1){
            if(q.empty()||q.top()+a[i].x<0) continue;
            else ans+=q.top()+a[i].x,q.pop(),q.push(-a[i].x);
        }
        else q.push(a[i].y-a[i].x);
    }
    print(ans);
    return 0;
}  

这是最简单的一种模拟费用流的模型,接下来的限制将会越来越多,难度也随之提高,倘若碰到一些奇怪的限制,不妨想想如果直接建费用流模型会怎么做

UOJ455 雪灾与外卖

题面
这题实际上是上一题的加强版,看看多了什么新的限制/操作

  1. 每个点可以向前也可以向后匹配
  2. 每个人必须强制匹配一条边
  3. 每家店可以多次选择

第一个,人和后面的店匹配实际上是等价于后面的店和前面的人匹配的,因此可以直接排序,维护两个堆,一个维护人,另一个维护店
第二个,可以在一开始的时候就让人和一家花费为 \(inf\) 的店连接,这样就可以保证后面的店会增广这个人
第三个,可以用一个pair记录一下还可以选择几次

接下来分四类讨论,这里只据其中的一类

\(i\) ,人 \(j\) ,人 \(k\) ,意思是先是人 \(j\) 和店 \(i\) 匹配,人 \(k\) 再顶替人 \(j\) 和店 \(i\) 匹配

第一次收益 :\(W_1=x_j-x_i+w_i\)
第二次收益 :\(W_2=x_k-x_i+w_i\)
差值 \(d=x_k-x_j\)
不妨看做第一次 \(i\)\(j\) 匹配完之后出现了一家新的店 \(l\) ,满足 \(-x_l+w_l=-x_j\),这样就实现了一次店 人 人增广
另外三种类型一样分析

代码
  
#include<bits/stdc++.h>
using namespace std;
#define il inline
#define ri register int
#define ll long long
#define ui unsigned int
il ll read(){
    bool f=true;ll x=0;
    register char ch=getchar();
    while(ch<'0'||ch>'9') {if(ch=='-') f=false;ch=getchar();}
    while(ch>='0'&&ch<='9') x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    if(f) return x;
    return ~(--x);
}
il int read(char *s){
    int len=0;
    register char ch=getchar();
    while(ch==' '||ch=='\n') ch=getchar();
    while(ch!=' '&&ch!='\n'&&ch!=EOF) s[++len]=ch,ch=getchar();
    return len;
}
il void write(const ll &x){if(x>9) write(x/10);putchar(x%10+'0');}
il void print(const ll &x) {x<0?putchar('-'),write(~(x-1)):write(x);putchar('\n');}
il ll max(const ll &a,const ll &b){return a>b?a:b;}
il ll min(const ll &a,const ll &b){return a<b?a:b;}
ll ans,inf=1e13,n,m,cnt;
const int MAXN=2e5+7;
struct node
{
    ll x,w,c,t;//分别是坐标,价值,出现次数,种类
}a[MAXN];
bool cmp(node p,node q){
    return p.x<q.x;
}
#define pll pair<ll,ll>
#define fir first
#define sec second
priority_queue<pll,vector<pll>,greater<pll> > q,p;
int main(){
    // freopen("ex_hole6.in","r",stdin);
    // freopen("1.out","w",stdout);
    n=read(),m=read();
    for(ri i=1;i<=n;++i) a[i].x=read(),a[i].c=1,a[i].t=-1,a[i].w=0;
    for(ri i=n+1;i<=n+m;++i) a[i].x=read(),a[i].w=read(),a[i].c=read(),a[i].t=1,cnt+=a[i].c;
    if(cnt<n){
        puts("-1");
        return 0;
    }
    sort(a+1,a+n+m+1,cmp);
    for(ri i=1;i<=n+m;++i){
        if(a[i].t==1){//餐馆
            cnt=0;
            while(!q.empty()&&a[i].c&&q.top().fir+a[i].w+a[i].x<0){
                pll now=q.top();q.pop();
                int t=min(now.sec,a[i].c);
                ans+=(now.fir+a[i].w+a[i].x)*t;
                cnt+=t;
                now.sec-=t,a[i].c-=t;
                if(now.sec) q.push(now);
                p.push((pll){-now.fir-2*a[i].x,t});
            }
            if(a[i].c) p.push((pll){a[i].w-a[i].x,a[i].c});
            if(cnt) q.push((pll){-a[i].x-a[i].w,cnt});
        }
        else{//服务员
            if(!p.empty()){
                ans+=a[i].x+p.top().fir;
                pll now=p.top();p.pop();
                if(-a[i].x-now.fir<0) q.push((pll){-2*a[i].x-now.fir,1});
                now.sec--;
                if(now.sec) p.push(now);
            }
            else{
                ans+=inf;
                q.push((pll){-a[i].x-inf,1});
            }
        }
    }
    print(ans);
    return 0;
}

To Be Continued

posted @ 2021-03-25 19:21  krimson  阅读(142)  评论(0编辑  收藏  举报