[bzoj4977] [Lydsy1708月赛] 跳伞求生

Description

\(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\) 战队的指挥,请制定一套最优的降落方案,使得最后获得的团队奖励总积分最大。

Input

第一行包含两个正整数 \(n\) , \(m\) ( $1 \leq n,m \leq 100000 $ ),分别表示战队的玩家数和地面上的房间数。
第二行包含 \(n\) 个正整数 \(a_1,a_2,...,a_n\) ( \(1 \leq a_i \leq 100000\) ),分别表示每个玩家的子弹数。
接下来 \(m\) 行,每行两个正整数 \(b_i,c_i\) ( \(1 \leq b_i,c_i \leq 100000\) ),分别表示每个敌人的子弹数和奖励积分。

Output

输出一行一个整数,即最后获得的团队奖励总积分的最大值。

Sample Input

3 3

4 4 4

2 3

1 3

5 3

Sample Output

11


想法

神仙题!貌似有两种做法——

做法一:贪心

\(a_i\)\(b_i\) 将玩家和敌人分别 从小到大排序
从小到大贪心考虑每名玩家,用两个优先队列维护,瞎搞一搞。。。
交上去发现 \(WA\) 了……原因是这样做是让发挥作用的玩家数最多,但不一定是总分最大
(有可能 \(c_i-b_i\) 的值为负且绝对值大于某一个 \(a_i\) ,那么把他们俩都去掉后总分更大)
那么把贪心改进一下,在上面的贪心进行完后,每次去掉 \(a_i\) 最小的玩家和 \(c_i-b_i\) 最小的敌人(可以保证这样做后配对仍合法),更新答案。
据说这样是可以 \(A\) 掉的。(然而我莫名 \(WA\) 了【逃】)

做法二:线段树模拟费用流

放一下官方题解:

不难发现 \(a\) 越大的玩家越容易占领房子,并且收益越大,因此最优解中一定是选 \(a\) 最大的若干个玩家
为了方便起见,我们设 \(v[i]=c[i]−b[i]\) ,即每个房子的收益。
将玩家和房子混在一起,按 \(a\)\(b\) 从小到大排序,对于相同的情况,将玩家优先放在前面,那么每个玩家能占领的房子就是它前面的所有房子
如果我们将方案中选取的玩家看成右括号,房子看成左括号,那么方案必定是一个合法的括号序列
即设 \(s[i]\) 表示前 \(i\) 个位置中选取的房子减去玩家的个数,那么必有 \(min(s[i]) \leq 0\)\(s[n+m]=0\)
因为 \(a\) 越大越好,因此我们从大到小考虑每个玩家
对于当前这个玩家,我们先将其位置填上右括号,对应 \(s\) 中一段后缀减去 \(1\)
我们希望找到一个没用过的收益最大的房子,满足加入那个房子后仍然是一个合法的括号序列
假设房子位于位置 \(j\) ,那么加入 \(j\) 会导致 \(s[j..n+m]\) 加上\(1\)
因此只要 \(min(s[1..j−1]) \leq 0\)\(min(s[j..n+m]) \geq −1\) 即是合法的房子
而区间加减、 区间最小值查询则是线段树的经典操作
\(v\) 从大到小考虑每个未使用的房子,如果它合法,那么将其纳入答案,同时加入该左括号,然后考虑下一个玩家
如果非法,因为我们按照 \(a\) 从大到小考虑每个玩家,今后的条件只会越来越苛刻,因此它永远都不可能合法,直接抛弃即可
注意到上述问题本质是在用线段树模拟费用流的增广,因此当增广路长度 \(<0\) 时,即可终止算法
时间复杂度 \(O((n+m)log(n+m))\)

既然是模拟费用流,那我就画一下费用流的图长啥样子——

我们需要的是最大费用流(但不一定是最大流),所以找最长路,当最长路长度 \(<0\) 时终止
题解中的左右括号是非常形象的理解方式【%%%】
\(s[i]\) 就相当于中间那一坨边的 逆流边 可流的流量多少,这样就解决了逆流的问题~太巧妙了!


代码

\(WA\) 了好久好久……

#include<cstdio>
#include<iostream>
#include<algorithm>
 
using namespace std;
 
int read(){
    int x=0;
    char ch=getchar();
    while(!isdigit(ch)) ch=getchar();
    while(isdigit(ch)) x=x*10+ch-'0',ch=getchar();
    return x;
}
 
const int N = 100005;
typedef long long ll;
 
int n,m,w;
struct data{
    int a,c;
    bool operator < (const data &b) const { return a<b.a || (a==b.a && c<b.c); }
}d[N*2];
struct pos{
    int x,id;
    pos() { x=id=0; }
    pos(int _x,int _id) { x=_x; id=_id; }
    bool operator < (const pos &b) const { return x>b.x; }
}po[N];
 
struct tree{
    tree *ch[2];
    int Min,lazy;
}pool[N*4],*root;
int cnt;
void build(tree *p,int l,int r){
    p->Min=p->lazy=0;
    if(l==r) return;
    int mid=(l+r)>>1;
    build(p->ch[0]=&pool[++cnt],l,mid);
    build(p->ch[1]=&pool[++cnt],mid+1,r);
}
void pushdown(tree *p){
    if(!p->lazy) return;
    p->ch[0]->Min+=p->lazy; p->ch[0]->lazy+=p->lazy;
    p->ch[1]->Min+=p->lazy; p->ch[1]->lazy+=p->lazy;
    p->lazy=0;
}
void add(tree *p,int l,int r,int L,int R,int c){
    if(l==L && r==R) { p->lazy+=c; p->Min+=c; return; }
    pushdown(p);
    int mid=(l+r)>>1;
    if(R<=mid) add(p->ch[0],l,mid,L,R,c);
    else if(L>mid) add(p->ch[1],mid+1,r,L,R,c);
    else{
        add(p->ch[0],l,mid,L,mid,c);
        add(p->ch[1],mid+1,r,mid+1,R,c);
    }
    p->Min=min(p->ch[0]->Min,p->ch[1]->Min);
}
int Min(tree *p,int l,int r,int L,int R){
    if(L>R) return 0;
    if(l==L && r==R) return p->Min;
    pushdown(p);
    int mid=(l+r)>>1;
    if(R<=mid) return Min(p->ch[0],l,mid,L,R);
    else if(L>mid) return Min(p->ch[1],mid+1,r,L,R);
    return min(Min(p->ch[0],l,mid,L,mid),Min(p->ch[1],mid+1,r,mid+1,R));
}
 
int main()
{
    n=read(); m=read();
    w=n+m;
    for(int i=1;i<=n;i++) d[i].a=read(),d[i].c=0;
    for(int i=1;i<=m;i++) d[i+n].a=read(),d[i+n].c=read();
     
    sort(d+1,d+1+w);
    int t=0;
    for(int i=1;i<=w;i++)
        if(d[i].c) po[++t]=pos(d[i].c-d[i].a,i);
    sort(po+1,po+1+m);
     
    build(root=&pool[++cnt],1,w);
    ll ans=0; t=1;
    for(int i=w;i>0;i--){
        if(d[i].c) continue;
        add(root,1,w,i,w,-1);
        while(t<=m){
            if(Min(root,1,w,1,po[t].id-1)>=0 && po[t].x+d[i].a>0) break;
            t++;
        }
        if(t<=m) {
            ans+=po[t].x+d[i].a;
            add(root,1,w,po[t].id,w,1);
            t++;
        }
        else break;
    }
    printf("%lld\n",ans);
     
    return 0;
}
posted @ 2019-08-16 16:55  秋千旁的蜂蝶~  阅读(206)  评论(0编辑  收藏  举报