《大鱼吃小鱼》是一款经典的儿童益智类游戏,在游戏中,玩家所操控的“大鱼”只能吃掉体积严格小于自己的“小鱼”,然后玩家所操控的“大鱼”的体积就会增加“小鱼”的体积这么多的量。
知名主播 Bychaha 是《大鱼吃小鱼》这款游戏国服排行榜的前 50 名,为了辅助自己玩这款游戏,Bychaha 研发了一个脚本,该脚本能在游戏开始时快速计算出 Bychaha 需要按照什么顺序吃掉哪些鱼,才能在吃掉最少的鱼的前提下,达到通关所需的体积。在这个强大脚本的辅助下,Bychaha 很快便成功问鼎国服。
很遗憾的是,这一切都是 Bychaha 做的梦,他并不会写代码,所以他想要求助擅长写代码的你来帮助他实现这个脚本。为了降低难度,Bychaha 只需要知道他最少需要吃掉几只鱼才能达到通关的要求。
《大鱼吃小鱼》游戏总共可能发生以下三类事件:
《大鱼吃小鱼》游戏的每一关都是模拟吃鱼的过程,所以在通关之后,被吃掉的鱼并不会从游戏中消失,只有发生(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
第一遍读完题,应该就有很粗暴的思路:贪心地从能吃的最大体积鱼开始吃,一旦体积变大后可以吃新的大鱼,就又去吃新的大鱼,知道到达通关体积为止
有了主干,再修订一下细节枝叶:每次肯定是吃尽量少的鱼,体积刚好可以解锁一种新大鱼就吃新大鱼,如果新大鱼体积不小于通关体积,就只用吃到通关体积即可。
通关体积是大于等于,吃鱼必须严格大于
最后开始寻找工具实现:发现若干问题,想到用二分假模拟,利用线段树维护吃的鱼区间及个数,很不幸这非常难以实现 在经过2h的调试加入新工具再维护后果断得出的结论
实际上,这种看似暴力模拟思路就是正解
直接模拟这个过程,可能会吃 n n n次鱼,考虑当前不能吃的最小体积的鱼
显然只有体积严格大鱼这种鱼的体积的时候,可吃鱼集合才会发生改变
如果这种鱼体积大于等于我们的通关体积,就只用吃到通关体积就行了
所以,我们只需要求每一步,对于不同可吃集合的鱼,需要最少吃多少条才能解锁下一步
由于贪心的吃法一定是吃一段连续的鱼(鱼体积是有序的),这个可以二分求出
题解说的线段树二分毁了我好多温柔,还是fhq-treap大法好
接下来是时间复杂度的问题
假设现在我们的体积为 A A A,不能吃的鱼体积为 B B B,显然 A ≤ B A\le B A≤B
我们会吃掉一些鱼 x x x,得到 A + x > B A+x>B A+x>B
此时不能吃的鱼体积变为 C C C,显然 A + x ≤ C A+x\le C A+x≤C
这个时候又再次吃掉一些鱼 y y y,得到 A + x + y > C A+x+y>C A+x+y>C
且注意到 y ≥ B y\ge B y≥B,因为解锁可以吃 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+y≥A+x+B>A+B≥A+A
也就是说两次二分(两个局面)就至少会翻倍最初体积,时间复杂度自然是 log \log log级别了
最后说说二分的实现
题解说线段树二分,结果造成了2h的无效输出,毁了我好多温柔,果然还是fhq-treap好
鱼的体积就是点的权值,一条鱼就是一个点,暴力贪心模拟
首先按现在鱼的体积分裂出能吃的鱼的集合
然后每一个局面可以知道现在体积与开启下一个局面的差,利用这个按体积和在可吃集合里分裂就能顺便知道使用了多少条鱼
接着就跳到下一个局面,如果开启不了就是 − 1 -1 −1
重点是吃了的鱼下一个局面需要直接跳过,我们的操作是合并是将吃的鱼暂时不合并回去,反而是将这个操作记录到栈里面
也就是说,做到了fhq-treap里面的鱼都是还未被吃掉了
最后这个询问完成后,一定要回溯操作,把模拟假装吃掉的鱼重新合并回去
#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;
}