大鱼吃小鱼
题目描述
许幼怡家里新买了一台电脑。电脑里有一款很好玩的游戏 —— 大鱼吃小鱼。别问我为什么问就是想给主角换一个人名。
大鱼吃小鱼总共可能发生以下三类事件:
- 一局新游戏开始了,许幼怡操控的 "大鱼" 的初始体积为 \(S\),当 "大鱼" 的体积达到 \(K\) 或更大时,这一局游戏通关。
- 游戏中新加入了一条体积为 \(W\) 的鱼。
- 游戏中某条体积为 \(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;
}
石子游戏
题目描述
Alice 和 Bob 在玩取石子游戏。
他们共有 \(N\) 堆石子,第 \(i\) 堆石子有 \(a_{i}\) 个石子。Alice 和 Bob 轮流取石子,Alice 先取,每一次取石子,当前取石子的人可以任选一堆还没有被取完的石子,从中取出至少 \(1\) 个,至多 \(x\) 个石子。如果当前取石子的人没有石子堆可选,那么他(她)就输掉了游戏。
他们想知道,如果 Alice 和 Bob 都用最优策略玩游戏的话,谁会胜利。
由于 Alice 和 Bob 还没商量好 \(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+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;
}