大鱼吃小鱼(fhq-treap/线段树二分+贪心)

大鱼吃小鱼

  • description
  • solution
  • code

description

《大鱼吃小鱼》是一款经典的儿童益智类游戏,在游戏中,玩家所操控的“大鱼”只能吃掉体积严格小于自己的“小鱼”,然后玩家所操控的“大鱼”的体积就会增加“小鱼”的体积这么多的量。

知名主播 Bychaha 是《大鱼吃小鱼》这款游戏国服排行榜的前 50 名,为了辅助自己玩这款游戏,Bychaha 研发了一个脚本,该脚本能在游戏开始时快速计算出 Bychaha 需要按照什么顺序吃掉哪些鱼,才能在吃掉最少的鱼的前提下,达到通关所需的体积。在这个强大脚本的辅助下,Bychaha 很快便成功问鼎国服。

很遗憾的是,这一切都是 Bychaha 做的梦,他并不会写代码,所以他想要求助擅长写代码的你来帮助他实现这个脚本。为了降低难度,Bychaha 只需要知道他最少需要吃掉几只鱼才能达到通关的要求。

《大鱼吃小鱼》游戏总共可能发生以下三类事件:

  • l 1 S K 表示一局新游戏开始了,Bychaha 操控的“大鱼”的初始体积为 S,当“大鱼”的体积达到 K 或更大时,这一局游戏通关
  • l 2 W 表示游戏中新加入了一条体积为 W 的鱼
  • l 3 W 表示游戏中某条体积为 W 的鱼消失了,保证在此之前游戏中至少存在一条体积为 W 的鱼

《大鱼吃小鱼》游戏的每一关都是模拟吃鱼的过程,所以在通关之后,被吃掉的鱼并不会从游戏中消失,只有发生(3)号事件时,鱼才会真的消失。

【输入格式】
第一行一个整数 N,表示一开始游戏中存在的鱼的数量。
第二行 N 个数",表示初始在游戏中的每条鱼的体积。
第三行一个整数 Q,表示事件的数量。
接下来 Q 行,表示发生的事件,格式如题面所述。
【输出格式】
对于每个(1)号事件,输出一个数,表示通关最少需要吃掉几只鱼,如果
无法通关,输出-1。
【样例输入输出】

4 
1 4 8 1 
15 
1 2 3 
1 2 4 
1 2 5 
1 3 3 
1 3 5 
1 3 16 
1 4 16 
1 8 17 
1 100 101 
1 100 115 
1 3 9 
2 2 
1 3 9 
3 4 
1 3 9 
1 
2 
-1 
0 
2 
4 
3 
2 
1 
-1 
3 
2 
-1 

【提示】
游戏过程中 Bychaha 操控的“大鱼”是凭空产生的,在通关或者通关失败之后自动消失。
【数据规模与约定】
对于 10%的数据,,Q ≤ 10
对于 30%的数据,,Q ≤ 1000
另有 10%的数据,1 ≤ S,K ≤ 1012,且保证数据完全随机
另有 20%的数据,在任意时刻游戏中至多只有 100 种体积不同的鱼
对于 100%的数据,1 ≤ ≤ 3 ∗ 105,1 ≤ ≤ 105 1 ≤ ≤ 1012,1 ≤ S,K ≤ 1018

solution

第一遍读完题,应该就有很粗暴的思路:贪心地从能吃的最大体积鱼开始吃,一旦体积变大后可以吃新的大鱼,就又去吃新的大鱼,知道到达通关体积为止

有了主干,再修订一下细节枝叶:每次肯定是吃尽量少的鱼,体积刚好可以解锁一种新大鱼就吃新大鱼,如果新大鱼体积不小于通关体积,就只用吃到通关体积即可。

通关体积是大于等于,吃鱼必须严格大于

最后开始寻找工具实现:发现若干问题,想到用二分假模拟,利用线段树维护吃的鱼区间及个数,很不幸这非常难以实现 在经过2h的调试加入新工具再维护后果断得出的结论


实际上,这种看似暴力模拟思路就是正解

直接模拟这个过程,可能会吃 n n n次鱼,考虑当前不能吃的最小体积的鱼

显然只有体积严格大鱼这种鱼的体积的时候,可吃鱼集合才会发生改变

如果这种鱼体积大于等于我们的通关体积,就只用吃到通关体积就行了

所以,我们只需要求每一步,对于不同可吃集合的鱼,需要最少吃多少条才能解锁下一步

由于贪心的吃法一定是吃一段连续的鱼(鱼体积是有序的),这个可以二分求出

题解说的线段树二分毁了我好多温柔,还是fhq-treap大法好

接下来是时间复杂度的问题

  • 假设现在我们的体积为 A A A,不能吃的鱼体积为 B B B,显然 A ≤ B A\le B AB

    我们会吃掉一些鱼 x x x,得到 A + x > B A+x>B A+x>B

    此时不能吃的鱼体积变为 C C C,显然 A + x ≤ C A+x\le C A+xC

    这个时候又再次吃掉一些鱼 y y y,得到 A + x + y > C A+x+y>C A+x+y>C

    且注意到 y ≥ B y\ge B yB,因为解锁可以吃 B B B的局面后还不能吃到 C C C,需要吃更多的鱼

    所以有 A + x + y ≥ A + x + B > A + B ≥ A + A A+x+y\ge A+x+B>A+B\ge A+A A+x+yA+x+B>A+BA+A

    也就是说两次二分(两个局面)就至少会翻倍最初体积,时间复杂度自然是 log ⁡ \log log级别了

最后说说二分的实现

  • 题解说线段树二分,结果造成了2h的无效输出,毁了我好多温柔,果然还是fhq-treap好

    鱼的体积就是点的权值,一条鱼就是一个点,暴力贪心模拟

    首先按现在鱼的体积分裂出能吃的鱼的集合

    然后每一个局面可以知道现在体积与开启下一个局面的差,利用这个按体积和在可吃集合里分裂就能顺便知道使用了多少条鱼

    接着就跳到下一个局面,如果开启不了就是 − 1 -1 1

    重点是吃了的鱼下一个局面需要直接跳过,我们的操作是合并是将吃的鱼暂时不合并回去,反而是将这个操作记录到栈里面

    也就是说,做到了fhq-treap里面的鱼都是还未被吃掉了

    最后这个询问完成后,一定要回溯操作,把模拟假装吃掉的鱼重新合并回去

code

#include 
#include 
#include 
using namespace std;
#define int long long
#define maxn 400005
struct node {
      int key, val, Min, sum, siz, lson, rson; }t[maxn];
int n, Q, cnt, root, top;
int v[maxn], sta[maxn];
 
int NewNode( int x ) {
     
    int now = ++ cnt;
    t[now].siz = 1;
    t[now].key = rand();
    t[now].sum = t[now].Min = t[now].val = x;
    return now; 
}
 
void pushup( int x ) {
     
    t[x].siz = t[t[x].lson].siz + t[t[x].rson].siz + 1;
    t[x].sum = t[t[x].lson].sum + t[t[x].rson].sum + t[x].val;
    if( t[x].lson ) t[x].Min = t[t[x].lson].Min;
    else t[x].Min = t[x].val;
}
 
int build( int l, int r ) {
     
    if( l > r ) return 0;
    int mid = ( l + r ) >> 1;
    int now = NewNode( v[mid] );
    t[now].lson = build( l, mid - 1 );
    t[now].rson = build( mid + 1, r );
    pushup( now );
    return now;
}
 
int merge( int x, int y ) {
     
    if( ! x or ! y ) return x + y;
    if( t[x].key < t[y].key ) {
     
        t[x].rson = merge( t[x].rson, y );
        pushup( x );
        return x;
    }
    else {
     
        t[y].lson = merge( x, t[y].lson );
        pushup( y );
        return y;
    }
}
 
void split_val( int now, int val, int &x, int &y ) {
     
    if( ! now ) x = y = 0;
    else {
     
        if( t[now].val <= val ) x = now, split_val( t[now].rson, val, t[now].rson, y );
        else y = now, split_val( t[now].lson, val, x, t[now].lson );
        pushup( now );
    }
}
 
void split_siz( int now, int siz, int &x, int &y ) {
     
    if( ! now ) x = y = 0;
    else {
     
        if( t[t[now].lson].siz >= siz ) y = now, split_siz( t[now].lson, siz, x, t[now].lson );
        else x = now, split_siz( t[now].rson, siz - t[t[now].lson].siz - 1, t[now].rson, y );
        pushup( now );
    }
}
 
void split_sum( int now, int sum, int &x, int &y ) {
     
    if( ! now ) x = y = 0;
    else {
     
        if( t[t[now].rson].sum >= sum )  x = now, split_sum( t[now].rson, sum, t[now].rson, y );
        else y = now, split_sum( t[now].lson, sum - t[t[now].rson].sum - t[now].val, x, t[now].lson );
        pushup( now );
    }
}
 
void rebuild( int x, int y ) {
     
    root = merge( x, y );
    while( top ) {
     
        split_val( root, t[sta[top]].val, x, y );
        root = merge( merge( x, sta[top] ), y );
        top --;
    }
}
 
void solve( int S, int T ) {
     
    int L = 0, R = root, x, y, ans = 0, nxt;
    while( S < T ) {
     
        split_val( R, S - 1, x, y );
        L = merge( L, x ), R = y;
        if( ! R ) nxt = T;
        else nxt = min( T, t[R].Min + 1 );
        if( t[L].sum + S < nxt ) {
     
            printf( "-1\n" );
            rebuild( L, R );
            return;
        }
        split_sum( L, nxt - S, x, y );
        ans += t[y].siz, S += t[y].sum;
        L = x, sta[++ top] = y;
    }
    printf( "%lld\n", ans );
    rebuild( L, R );
}
 
void Insert( int val ) {
     
    int x, y;
    split_val( root, val, x, y );
    root = merge( merge( x, NewNode( val ) ), y );
}
 
void Delete( int val ) {
     
    int x, y, l, r;
    split_val( root, val, x, y );
    split_siz( x, t[x].siz - 1, l, r );
    root = merge( l, y );
}
 
signed main() {
     
    scanf( "%lld", &n );
    for( int i = 1;i <= n;i ++ ) scanf( "%lld", &v[i] );
    sort( v + 1, v + n + 1 );
    root = build( 1, n );
    scanf( "%lld", &Q );
    while( Q -- ) {
     
        int opt, s, k, w;
        scanf( "%lld", &opt );
        switch( opt ) {
     
            case 1 : {
     
                scanf( "%lld %lld", &s, &k );
                solve( s, k );
                break;
            }
            case 2 : {
     
                scanf( "%lld", &w );
                Insert( w );
                break;
            }
            case 3 : {
     
                scanf( "%lld", &w );
                Delete( w );
                break;
            }
        }
    }
    return 0;
}

你可能感兴趣的