P9839 四暗刻单骑
题解参考了 here。
Solution
1. x=y
-
不难发现,此时无论是 Alice 还是 Bob,都不能弃掉初始手牌,否则对方就和牌了。
因此,赢家是先成功自摸的一个。找出
[l,r] 中x 的首次出现位置(vector 存每种值的出现位置,每次在对应 vector 中二分),若是 Alice 取的,则 Alice 赢;若是 Bob 取的,则 Bob 赢;若没有出现,则平局。
2. \mathcal O(nm\log n)
(
-
根据
x=y 的做法,我们发现,若某个时刻两人手牌相同了,之后他们的打法就是唯一的,结局就注定了。假设
x\neq y ,目前牌堆顶部的牌为z 。对于当前操作者而言:- 若
z 与自己的手牌相同,则成功自摸,胜利。 - 若
z 与对方的手牌相同,那么自己取了z 后必然不能将它舍弃(否则对方就和牌了),转化为了上述x=y 的情形,此后虽然牌局尚未结束,但已决胜负。
综上,若
x,y,z 中有某两个相同,结局就注定了。 - 若
-
于是,对于位置
i ,设a_i 下一次出现位置在j (若不存在则为\infty ),那么若操作者取了i 后始终不把它弃掉,且游戏在j 处时还未终止,则在j 处时结局就注定了。因此,一张牌i ,可以转化为二元组(time_i,op_i) ,表示 若保留i ,且time_i 时游戏还没有结束,则到time_i 处时结局注定是op_i 。处理出
[l,r] 中的牌以及初始手牌对应的(time_i,op_i) 后,从前往后决策,Alice 和 Bob 每次都会选择更有利于自己的牌留下。具体地,赢的牌一定比输的优,赢的牌中time 越小越优(减小对手翻盘的希望),输的牌中time 越大越优(增大自己翻盘的希望,因为可能后面能摸到胜利的牌就能翻盘)。(
(time_i,op_i) 描述的其实是一种趋势,博弈论一般是从后往前做的,但是这里已经预处理了进行每次决策后的趋势,并且我们可以比较每种趋势的牌选哪种在之后的局面中更优,所以可以从前往后进行决策)注意,
op_i 仅仅是表示一种结局的趋势,因为 有可能根本等不到time_i 去注定op_i 。所以就有一个问题:我们不知道 赢的牌 与 平局的牌 哪个优,因为赢的牌在
time 之前可能对方就已经赢了,结局是对方赢;而平局的牌可能因为time 比较小,等不到对方赢,结局是平局。而 赢的牌 与 输的牌 是可以比较的,选赢的牌总归结果不会更差。所以必须把平局的牌处理掉。
-
对平局的处理:如果平局算作 Bob 赢的情况下,Alice 还能赢,说明必然是 Alice 赢;如果平局算作 Alice 赢的情况下,Bob 还能赢,说明必然是 Bob 赢;否则平局。
(设 Alice 赢 / Bob 赢 / 平局 的局面集合为
A,B,C ,初始局面为x 。我们先判断了x\in A 还是x\in B\cup C ;再判断了x\in B 还是x\in A\cup C 。根据这个信息就能判断x\in A 还是x\in B 还是x\in C 了)对平局算作 Bob/Alice 赢分别跑一次即可。
这样就只有 赢和输 两种牌了。
//52 分暴力
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,k,a[N],o,x,y,l,r; //o=1/0:平局算 Alice/Bob 赢
vector<int>v[N];
array<int,2>A,B;
bool calc(int l,int r,int x){ //x=y,牌堆为 [l,r],结局是什么
int i=*lower_bound(v[x].begin(),v[x].end(),l);
return i>r?o:i&1;
}
array<int,2> get(int who,int i,int x){ //当前 who 取第 i 张牌(who=1/0: Alice/Bob),第 i 张牌数字是 x,这张牌对应的 (time,op=1/0: Alice/Bob 赢)
int j=*upper_bound(v[x].begin(),v[x].end(),i),op;
if(j>r) op=o;
else{
if((j&1)==who) op=who;
else op=calc(j+1,r,x);
}
return {j,op};
}
bool work(){
if(x==y) return calc(l,r,x);
A=get(1,l-1,x),B=get(0,l-1,y);
for(int i=l;i<=r;i++){
auto p=get(i&1,i,a[i]);
if(i&1){
if(p[1]>A[1]||(p[1]==A[1]&&((p[1]&&p[0]<A[0])||(!p[1]&&p[0]>A[0])))) A=p;
}
else
if(p[1]<B[1]||(p[1]==B[1]&&((!p[1]&&p[0]<B[0])||(p[1]&&p[0]>B[0])))) B=p;
if(A[0]==i) return A[1];
if(B[0]==i) return B[1];
}
return o;
}
signed main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),v[a[i]].push_back(i);
for(int i=1;i<=k;i++) v[i].push_back(n+1);
while(m--){
scanf("%d%d%d%d",&x,&y,&l,&r);
int a,b;
o=0,a=work(),o=1,b=work();
puts(a?"A":(!b?"B":"D"));
}
return 0;
}
3. \mathcal O((n+m)\log n)
-
一个重要的结论是,若 Alice 在
i 处取了a_i ,其对应的二元组为(j,0) ,那么 Alice 一定不会因此在j 处输掉(一定不会始终保留这张i ,直到因为这张牌在j 输掉);同理,若 Bob 在i 处取了a_i ,其对应的二元组为(j,1) ,那么 Bob 一定不会因此在j 处输掉。(其中,op=1 表示 Alice 赢,op=0 表示 Bob 赢)换句话说,对于牌堆里的一个让自己输的牌
i ,一定会在j 之前换牌。证明:以 Alice 为例。因为输掉,所以
j 一定是 Bob 摸的,那么j-1 会是 Alice 摸的,而j-1 处的二元组一定比(j,0) 优,就算前面没有换牌,到j-1 处时 Alice 也一定会换牌。注意初始手牌比较特别,如果初始手牌是
(l,?) ,没办法在区间外(l-1 处)换牌,所以需要特判;而牌堆里的牌显然有j>l 。 -
特判掉初始手牌后,本题的关键性质就出来了:我们可以 忽略所有让自己输的牌。
好处是,原本我们需要依次枚举每张牌比较优劣,因为要依次扫过去才知道是赢的牌还是输的牌。现在只要考虑赢的牌,那么只要直接找到
time 最小的就好了。找到所有 让自己赢的牌中,
time 最小的,看是被谁取的 即可。 -
最后的问题是,怎么快速算所有
(time,op) 。对于牌
l\leq i ,设下一次出现在j ,下下次出现在k 。根据二元组的计算方式,可以发现我们只关心j,k 是否在区间内。分别算出r<j 、r\geq j 、r\geq k 三种情况分别对应的三元组即可。扫描线
r ,线段树来支持“单点修改,区间查询最小值和最小值的位置”即可。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
int n,m,k,a[N],r,o,ans[2][N];
vector<int>v[N],upd[N];
array<int,2>A,B,mn[N<<2];
vector<array<int,4> >q[N];
bool calc(int l,int r,int x){
int i=*lower_bound(v[x].begin(),v[x].end(),l);
return i>r?o:i&1;
}
array<int,2> get(int who,int i,int x){
int j=*upper_bound(v[x].begin(),v[x].end(),i),op;
if(j>r) j=n+1,op=o;
else{
if((j&1)==who) op=who;
else op=calc(j+1,r,x);
}
return {j,op};
}
void modify(int p,int l,int r,int pos,int v){
if(l==r){mn[p]={v,l};return ;}
int mid=(l+r)/2;
if(pos<=mid) modify(p<<1,l,mid,pos,v);
else modify(p<<1|1,mid+1,r,pos,v);
mn[p]=min(mn[p<<1],mn[p<<1|1]);
}
auto query(int p,int l,int r,int lx,int rx){
if(l>=lx&&r<=rx) return mn[p];
int mid=(l+r)/2;
array<int,2>ans={n+2,0};
if(lx<=mid) ans=query(p<<1,l,mid,lx,rx);
if(rx>mid) ans=min(ans,query(p<<1|1,mid+1,r,lx,rx));
return ans;
}
signed main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),v[a[i]].push_back(i);
for(int i=1;i<=k;i++)
v[i].push_back(n+1),v[i].push_back(n+2);
for(int i=1;i<=n;i++){
int j=*upper_bound(v[a[i]].begin(),v[a[i]].end(),i),
k=*upper_bound(v[a[i]].begin(),v[a[i]].end(),j);
for(int x:{i,j,k}) upd[x].push_back(i);
}
for(int i=1,x,y,l;i<=m;i++)
scanf("%d%d%d%d",&x,&y,&l,&r),q[r].push_back({x,y,l,i});
for(o=0;o<2;o++){
for(int i=1;i<=(n<<2);i++) mn[i][0]=1e9;
for(r=1;r<=n;r++){
for(int i:upd[r]){
auto p=get(i&1,i,a[i]);
modify(1,1,n,i,p[1]==(i&1)?p[0]:1e9);
}
for(auto i:q[r]){
int x=i[0],y=i[1],l=i[2],&f=ans[o][i[3]];
if(x==y){f=calc(l,r,x);continue;}
A=get(1,l-1,x),B=get(0,l-1,y);
if(A[0]==l) f=A[1];
else if(B[0]==l) f=B[1];
else{
auto x=query(1,1,n,l,r);
if(A[1]) x=min(x,{A[0],1});
if(!B[1]) x=min(x,{B[0],0});
f=x[0]<=r?x[1]&1:o;
}
}
}
}
for(int i=1;i<=m;i++)
puts(ans[0][i]?"A":(!ans[1][i]?"B":"D"));
return 0;
}
有错请指出 qwq。