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

大鱼吃小鱼

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 <cstdio>
#include <iostream>
#include <algorithm>
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;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可持久化splay是一种数据结构,它是对splay树进行修改和查询的一种扩展。在传统的splay树中,对树的修改操作会破坏原有的树结构,而可持久化splay树则允许我们对树进行修改、查询,并且可以保存修改后的每个版本的树结构。 在可持久化splay树中,我们不会直接对原树进行修改,而是通过复制每个节点来创建新的版本。这样,每个版本都可以独立地修改和查询,保留了原有版本的结构和状态。每个节点保存了其左子树和右子树的引用,使得可以在不破坏原有版本的情况下进行修改和查询。 为了实现可持久化splay树,我们可以使用一些技巧,比如引用中提到的哨兵节点和假的父节点和孩子节点。这些技巧可以帮助我们处理根节点的旋转和其他操作。 此外,可持久化splay树还可以与其他数据结构相结合,比如引用中提到的可持久化线段树。这种结合可以帮助我们解决更复杂的问题,比如区间修改和区间查询等。 对于可持久化splay树的学习过程,可以按照以下步骤进行: 1. 理解splay树的基本原理和操作,包括旋转、插入、删除和查找等。 2. 学习如何构建可持久化splay树,包括复制节点、更新版本和保存历史版本等。 3. 掌握可持久化splay树的常见应用场景,比如区间修改和区间查询等。 4. 深入了解与可持久化splay树相关的其他数据结构和算法,比如可持久化线段树等。 在解决问题时,可以使用二分法来确定答案,一般称为二分答案。通过对答案进行二分,然后对每个答案进行检查,以确定最终的结果。这种方法可以应用于很多问题,比如引用中提到的在线询问问题。 综上所述,可持久化splay是一种对splay树进行修改和查询的扩展,可以通过复制节点来创建新的版本,并且可以与其他数据结构相结合解决更复杂的问题。学习过程中可以按照一定的步骤进行,并且可以使用二分法来解决一些特定的问题。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [[学习笔记]FHQ-Treap及其可持久化](https://blog.csdn.net/weixin_34283445/article/details/93207491)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [可持久化数据结构学习笔记](https://blog.csdn.net/weixin_30376083/article/details/99902410)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值