[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;
}