大鱼吃小鱼

题目描述

许幼怡家里新买了一台电脑。电脑里有一款很好玩的游戏 —— 大鱼吃小鱼。别问我为什么问就是想给主角换一个人名。

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

  1. 一局新游戏开始了,许幼怡操控的 "大鱼" 的初始体积为 \(S\),当 "大鱼" 的体积达到 \(K\) 或更大时,这一局游戏通关。
  2. 游戏中新加入了一条体积为 \(W\) 的鱼。
  3. 游戏中某条体积为 \(W\) 的鱼消失了,保证在此之前游戏中至少存在一条体积为 \(W\) 的鱼。

在通关之后,被吃掉的鱼并不会从游戏中消失,只有发生 \(3\) 号事件时,鱼才会真的消失。另外,"大鱼" 只能吃掉比自己体积严格小的鱼。

\(1\le n\le 3\cdot 10^5,1\le q\le 10^5,1\le W_i\le 10^{12},1\le S,K\le 10^{18}\)

解法

首先贪心可知优先吃掉可吃的体积最大的鱼。假设目前最小的不能吃的鱼体积为 \(x\)。用平衡树/权值线段树维护可吃的鱼,从大到小选择一段鱼使得体积大于 \(x-S\)。继续这个过程,直到 \(x\ge K\),此时直接到 \(K\) 即可。

接下来证明时间复杂度。考虑吃一次有 \(S+\delta_1>x\),假设吃第二次时目标体积为 \(y\),有 \(S+\delta_1+\delta_2>y\),此时 "大鱼" 体积为 \(S+\delta_1+\delta_2\ge S+\delta_1+x>2S\)。吃两次体积至少翻倍,所以至多吃 \(\log K\) 次。总复杂度 \(\mathcal O(q\log n\log K)\)

最后讲一下平衡树如何复原,这也是线段树很难实现的一个点。将被吃的鱼的子树根节点丢进栈里,最后得到未被吃的树根 \(rt\) 后,倒序按被吃根节点的体积 split() 一下 \(rt\),再将被吃子树合并上去。正确性是由于吃鱼时是分裂一段权值连续的区间,所以用这个区间任意权值都可以定位这个区间的位置。

代码

#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;

const int maxn = 3e5+5;

int n,idx,rt;
ll val[maxn];
int tp,stk[maxn<<1];
struct node {
	ll s,val,mn;
	int ls,rs,siz,key;
} t[maxn<<1];

int NewNode(ll x) {
	static default_random_engine E;
	static uniform_int_distribution <int> U(-1e8,1e8);
	t[++idx].key=U(E);
	t[idx].val=t[idx].s=t[idx].mn=x;
	t[idx].siz=1;
	return idx;
}

void pushUp(int o) {
	t[o].siz = t[t[o].ls].siz+t[t[o].rs].siz+1;
	t[o].s = t[t[o].ls].s+t[t[o].rs].s+t[o].val;
	if(t[o].ls) t[o].mn=t[t[o].ls].mn;
	else t[o].mn=t[o].val;
}

void build(int &o,int l,int r) {
	int mid=l+r>>1; o=NewNode(val[mid]);
	if(l<mid) build(t[o].ls,l,mid-1);
	if(r>mid) build(t[o].rs,mid+1,r);
	pushUp(o);
}

int merge(int x,int y) {
	if(!x or !y)
		return x|y;
	if(t[x].key<t[y].key) {
		t[x].rs=merge(t[x].rs,y);
		pushUp(x);
		return x;
	}
	else {
		t[y].ls=merge(x,t[y].ls);
		pushUp(y);
		return y; 
	}
}

void split_siz(int o,int k,int &x,int &y) {
	if(!o) return x=y=0,void();
	if(k<=t[t[o].ls].siz)
		y=o,split_siz(t[o].ls,k,x,t[o].ls);
	else
		x=o,split_siz(t[o].rs,k-t[t[o].ls].siz-1,t[o].rs,y);
	pushUp(o);
}

void split_val(int o,ll k,int &x,int &y) {
	if(!o) return x=y=0,void();
	if(k<t[o].val)
		y=o,split_val(t[o].ls,k,x,t[o].ls);
	else
		x=o,split_val(t[o].rs,k,t[o].rs,y);
	pushUp(o);
}

void split(int o,ll k,int &x,int &y) {
	if(!o) return x=y=0,void();
	if(k>t[t[o].rs].s)
		y=o,split(t[o].ls,k-t[t[o].rs].s-t[o].val,x,t[o].ls);
	else
		x=o,split(t[o].rs,k,t[o].rs,y);
	pushUp(o);
} 

void reBuild(int x,int y) {
	rt=merge(x,y);
	while(tp) {
		int now=stk[tp--];
		split_val(rt,t[now].val,x,y);
		rt=merge(merge(x,now),y);
	}
}

void Eat(ll now,ll goal) {
	int x=0,y=rt,a,ans=0; ll nxt;
	while(now<goal) {
		split_val(y,now-1,a,y);
		x=merge(x,a);
		nxt = y?min(t[y].mn+1,goal):goal;
		if(now+t[x].s<nxt) {
			reBuild(x,y);
			return puts("-1"),void();
		}
		split(x,nxt-now,x,a);
		stk[++tp]=a;
		ans+=t[a].siz; now+=t[a].s;
	}
	print(ans,'\n');
	reBuild(x,y);
}

int main() {
	n=read(9);
	for(int i=1;i<=n;++i)
		val[i]=read(9ll);
	sort(val+1,val+n+1);
	build(rt,1,n);
	int a,b,c;
	for(int q=read(9);q;--q) {
		int op; ll x,y;
		op=read(9),x=read(9ll);
		if(op==1) {
			y=read(9ll);
			Eat(x,y);
		}
		else if(op==2) {
			split_val(rt,x,a,b);
			rt=merge(merge(a,NewNode(x)),b);
		}
		else {
			split_val(rt,x,a,b);
			split_siz(a,t[a].siz-1,a,c);
			rt=merge(a,b);
		}
	}
	return 0;
}

黑客

题目描述

有一个长度为 \(n\) 的数列,每个数的范围是 \([1,k]\)。有 \(m\) 个限制,第 \(i\) 个限制为 \((a_i,b_i)\),表示 \(a_i\) 不能出现在 \(b_i\) 之前。请计算出满足条件数列的个数和它们的十进制之和。

\(1\le n\le 500,1\le m\le 100,k\le 9\)

解法

这题状压 \(\mathtt{dp}\) 提示地挺明显了,但还是没看出来…

\(f_{i,s},g_{i,s}\) 为选到第 \(i\) 位,数字集合为 \(s\) 的数字和与个数。注意要卡常的话,可以把 \(\mathtt{dp}\) 转移揉成一个操作符,还要压 \(17\) 位高精。

代码

#include <cstring>
typedef long long ll;

const int bas=17;
const ll mod=1e17;

int n,m,k,no[10][10];
int nxt[1<<9][10];
bool vis[10];

inline ll inc(ll x,ll y) {
	return x+y>=mod?x+y-mod:x+y;
}

struct Int {
	
	ll a[60]; int len;
	
	Int() {memset(a,0,sizeof a);}
	
	Int operator * (int mul) const {
		Int r; r.len=len;
		ll add=0,tmp;
		for(int i=1;i<=len;++i) {
			tmp = inc(a[i]*mul%mod,add);
			add = (a[i]*mul+add)/mod;
			r.a[i] = tmp;
		}
		if(add) r.a[++r.len]=add;
		return r;
	}
	
	Int operator + (const Int &t) const {
		Int r; r.len=1;
		while(r.len<=len or r.len<=t.len) {
			if(r.a[r.len]+a[r.len]+t.a[r.len]>=mod) {
				r.a[r.len+1] = 1;
				r.a[r.len] = r.a[r.len]+a[r.len]+t.a[r.len]-mod;
			}
			else
				r.a[r.len] = r.a[r.len]+a[r.len]+t.a[r.len];
			++r.len;
		}
		if(!r.a[r.len]) --r.len;
		return r;
	}
	
	void Print() {
		write(a[len]);
		for(int i=len-1;i>=1;--i)
			printf("%017lld",a[i]);
		puts("");
	}
	
	void Clear() {
		for(int i=1;i<=len;++i)
			a[i]=0;
		len=1;
	}
} f[2][1<<9],g[2][1<<9];

int main() {
	n=read(9),m=read(9),k=read(9);
	for(int i=1;i<=m;++i) {
		int x,y;
		x=read(9),y=read(9);
		no[x][y]=1;
	}
	int lim = (1<<k);
	for(int s=0;s<lim;++s) {
		for(int i=1;i<=k;++i)
			vis[i]=0;
		for(int i=1;i<=k;++i)
			if(s>>i-1&1)
				for(int j=1;j<=k;++j)
					if(no[i][j]) vis[j]=1;
		for(int i=1;i<=k;++i)
			if(!vis[i])
				nxt[s][++nxt[s][0]]=i;
	}
	g[0][0].a[1]=g[0][0].len=1;
	for(int i=1;i<=n;++i) {
		for(int s=lim-1;s>=0;--s) {
			for(int j=1;j<=nxt[s][0];++j) {
				f[i&1][s|(1<<nxt[s][j]-1)] = f[i&1][s|(1<<nxt[s][j]-1)]+f[(i&1)^1][s]*10+g[(i&1)^1][s]*nxt[s][j];
				g[i&1][s|(1<<nxt[s][j]-1)] = g[i&1][s|(1<<nxt[s][j]-1)]+g[(i&1)^1][s];
			}
		}
		for(int s=lim-1;s>=0;--s)
			f[(i&1)^1][s].Clear(),
			g[(i&1)^1][s].Clear();
	}
	g[n&1][0].a[1]=0;
	for(int s=1;s<lim;++s)
		f[n&1][0] = f[n&1][0]+f[n&1][s],
		g[n&1][0] = g[n&1][0]+g[n&1][s];
	g[n&1][0].Print(),f[n&1][0].Print();
	return 0;
}

石子游戏

题目描述

AliceBob 在玩取石子游戏。

他们共有 \(N\) 堆石子,第 \(i\) 堆石子有 \(a_{i}\) 个石子。AliceBob 轮流取石子,Alice 先取,每一次取石子,当前取石子的人可以任选一堆还没有被取完的石子,从中取出至少 \(1\) 个,至多 \(x\) 个石子。如果当前取石子的人没有石子堆可选,那么他(她)就输掉了游戏。

他们想知道,如果 AliceBob 都用最优策略玩游戏的话,谁会胜利。

由于 AliceBob 还没商量好 \(x\) 取多少,所以对于每个 \(1\)\(N\) 之间的 \(x\),你都需要告诉他们谁将取得胜利。

\(1\le N\le 5\cdot 10^5,1\le a_i\le N\)

解法

首先需要了解一下 巴什博弈。根据巴什博弈,我们可以将每堆石子的个数取模 \(x+1\)。正确性先埋坑。

所以问题转化成,对于每个 \(x\in[1,n]\),求出 \(\text{xor}_{i=1}^n (a_i\bmod (x+1))\)

模运算似乎很难处理,但是对于 权值 区间 \([kx,kx+(x+1))\) 这可以转化为 权值 区间 \([0,x+1)\) 的异或和!这个东西就和 \(x\) 是什么没有关系了。这令我们自然地想到倍增预处理:输入时将 \(v_{a_i}\text{ xor }1\) 以得到权值区间,再对其做一个前缀异或和(为什么下文再讲)。令 \(f_{i,j}\) 为从权值 \(i\) 开始,长度为 \(2^j\) 的区间内的异或和,即 \(\text{xor}_{k=i}^{i+2^j-1}v_k\)。递推式:

\[f_{i,j}=f_{i,j-1}\text{ xor }f_{i+2^{j-1},j-1} \]

但是,\(f_{i+2^{j-1},j-1}\) 是从 \(0\) 开始计算的,而我们实际上想要从 \(2^{j-1}\) 开始。其实这是可以处理的:由于 \(f_{i+2^{j-1},j-1}\) 的第 \(j-1\) 位肯定为 \(0\),所以我们只用判断在权值区间 \([i+2^{j-1},i+2^{j}-1]\) 的数字个数的奇偶性即可,如果是奇数,就在 \(f_{i,j}\) 上异或 \(2^{j-1}\)

对于每个 \(x\),我们将权值区间 \([0,n]\)(开始时需要取到 \(0\))分成 \(\frac{n+1}{x}\) 段,对于每段分别计算。假设查询 \([l=kx,r]\),我们发现答案合并也有转移的那个问题。这其实可以类似地解决:考虑每次从 \(l\) 移动到 \(p\),当前倍增倍数为 \(i\)。那么 \([l,p)\) 的长度一定严格大于 \([p,r]\) 的长度,便可得到右边区间的二进制的第 \(i\) 位一定为 \(0\),所以可以用上文的方法。

关于时间复杂度,由于分段是调和级数,所以是 \(\mathcal O(n\log^2 n)\) 的。

代码

#include <iostream>
using namespace std;

const int maxn = 5e5+5;

int n,a[maxn];
int f[20][maxn],lg[maxn];

int calc(int l,int r) {
	r=min(r,n);
	int ret=0;
	for(int i=lg[r-l+1];i>=0;--i) {
		if(l+(1<<i)-1<=r) {
			ret^=f[i][l];
			l+=(1<<i);
			if(a[r]^a[l-1])
				ret^=(1<<i);
		}
	}
	return ret;
}

int main() {
	n=read(9);
	for(int i=1;i<=n;++i)
		a[read(9)]^=1;
	for(int i=1;i<=n;++i)
		a[i]^=a[i-1];
	for(int i=2;i<=n+1;++i)
		lg[i]=lg[i>>1]+1;
	for(int j=1;j<=lg[n+1];++j)
		for(int i=0;i<=n;++i) {
			if(i+(1<<j)-1>n) break;
			f[j][i]=f[j-1][i]^f[j-1][i+(1<<j-1)];
			if(a[i+(1<<j)-1]^a[i+(1<<j-1)-1])
				f[j][i]^=(1<<j-1);
		}
	for(int i=1;i<=n;++i) {
		int ans=0;
		for(int j=0;j<=n;j+=i+1)
			ans^=calc(j,j+i);
		printf("%s ",ans?"Alice":"Bob");
	}
	return 0;
}
posted on 2021-10-06 22:58  Oxide  阅读(89)  评论(0编辑  收藏  举报