Sol - P9839
考场上,可以先考虑暴力,不仅有保底分,而且方便对拍。
测试点 \(1,2\)
大力枚举两个人接下来会出什么牌即可。期望得分 \(8\) 分。
测试点 \(3 \sim 5\)
和普通博弈论题目一样,考虑使用动态规划。状态设计和转移平凡,也可以使用记忆化搜索。期望得分 \(20\) 分。代码中我使用了记忆化搜索。
$20$ 分的记忆化搜索
namespace test_1_to_5{
int f[105][105][105];
int dfs(int x,int y,int p,int r){
if(f[x][y][p]!=INF)return f[x][y][p];
if(p>r)return f[x][y][p]=0; //平局
if((p&1) && x==a[p])return f[x][y][p]=1; //Alice 自摸
else if((p&1)==0 && y==a[p])return f[x][y][p]=2; //Bob 自摸
if(p&1){ //Alice 抽牌
int r1,r2; //Alice 丢这张牌会出现什么样的结果
if(a[p]==y)r1=2; else r1=dfs(x,y,p+1,r);
if(x==y)r2=2; else r2=dfs(a[p],y,p+1,r);
if(r1==2 && r2==2)return f[x][y][p]=2; //Bob 稳赢才算 Bob 赢
if(r1==1 || r2==1)return f[x][y][p]=1; //否则 Alice 有机会赢就算 Alice 赢
return f[x][y][p]=0; //平局
}
int r1,r2; //Bob 抽牌
if(a[p]==x)r1=1; else r1=dfs(x,y,p+1,r);
if(x==y)r2=1; else r2=dfs(x,a[p],p+1,r);
if(r1==1 && r2==1)return f[x][y][p]=1;
if(r1==2 || r2==2)return f[x][y][p]=2;
return f[x][y][p]=0;
}
void Main(){
while(m--){
memset(f,0x3f,sizeof(f));
//注意每次询问都得 memset
//经过测试,大样例下跑得飞快
//50ms 以内能够胜任 n=100 的测试点
int x,y,l,r; cin>>x>>y>>l>>r;
int res=dfs(x,y,l,r);
if(res==0)cout<<"D\n";
if(res==1)cout<<"A\n";
if(res==2)cout<<"B\n";
}
}
}
做完暴力,观察题目描述以及样例,不难发现如下性质:
- 放炮(也就是出一张和另外一个人手牌中颜色一样的牌)是不可能的,这是因为 Alice 和 Bob 都希望自己可以和牌并获胜,若自己无法和牌就会尽可能阻止对方和牌。
- 题目保证了 \(l\) 为奇数,所以每个人将抽到什么牌是固定的。
测试点 \(11\)
只有两个颜色,因此直接分类讨论,如果手里颜色相同就看谁会先抽到指定牌堆里那个相同颜色的牌,如果不相同,那么先看 Alice 会抽到什么牌,如果和手里的一样就直接和牌,不一样就将那张不一样的打出去,问题转化成手里颜色相同的情况。实现的时候使用二分查找即可。期望得分 \(24\) 分。
/*
解释一下:
e[x] 是一个 vector,存的是颜色 x 对应的位置
a[] 是原本的牌堆
*/
namespace test_11{
void Solve(int x,int y,int l,int r){
if(x==y){
auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r){ cout<<"D\n"; return; }
//注意二分到的位置也不能超过 r
else{ cout<<(((*it)&1)?"A\n":"B\n"); return; }
} if(a[l]==x){ cout<<"A\n"; return; }
x=a[l]; ++l; auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it>r)){ cout<<"D\n"; return; }
else{ cout<<(((*it)&1)?"A\n":"B\n"); return; }
}
void Main(){
while(m--){
int x,y,l,r; cin>>x>>y>>l>>r;
Solve(x,y,l,r);
}
}
}
测试点 \(16\)
测试点 \(11\) 的第一种情况,同样使用二分查找即可。期望得分 \(28\) 分,考场上这个分数是相当可观的,如果前面的题目不能太过保证的话回去检查前面的题目是一个非常不错的选择。
//vec 是存储了询问的 vector,e[x] 的含义和测试点 11 的一样
namespace test_16{
void Main(){
for(int i=1;i<=m;++i){
int x=vec[i-1].x,l=vec[i-1].l,r=vec[i-1].r;
auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r){ cout<<"D\n"; continue; }
cout<<(((*it)&1)?"A\n":"B\n");
}
}
}
测试点 \(1 \sim 10\)
根据测试点 \(11\) 和 \(16\) 的提示,我们考虑使用分类讨论下的贪心来解决问题。
由于题面中要求双方采取最优策略,因此有可能会出现有玩家发现自己已经不能胜利,使用尽力得到平局结果的情况。所以需要先假定平局算 Alice 胜,若 Bob 仍能胜出才算 Bob 胜;假定平局算 Bob 胜,若 Alice 仍能胜出才算 Alice 胜;否则,算平局。接下来分类讨论:
- 若双方手牌相同,显然他们将进入摸切(摸到什么出什么)的环节,直到一方自摸。
- 若双方手牌不同,他们会考虑在一个时刻摸到一张合适的牌之后,一直捏着它直到结束,并且如果摸到的牌会使得自己胜,那么越早越好;若会败,则越晚越好。
那么我们就需要计算一直捏着一张牌会在什么时刻产生定胜负的局面。如果自己捏着一张牌,我们需要考虑下一张牌和再下一张牌的位置对答案的影响。设摸到下一张牌的时刻是 \(x\)。
-
若下一张牌自己摸到,说明自己直接在 \(x\) 时刻胜利了。
-
若对方摸到,问题又转化成了测试点 \(16\) 的情况,此时双方不能丢掉自己的手牌。因此只需要计算下一张牌被谁摸到了即可,胜负判断仍然在 \(x\) 时刻。
-
若再下一张牌自己摸到,说明在 \(x\) 时刻自己胜利了。
-
若对方摸到,说明在 \(x\) 时刻自己失败了。
-
若不存在再下一张,算平局。
-
直接模拟这个过程即可,个人码量 \(1.8 \text{ KiB}\)。该算法时间复杂度 \(O(nm)\),算上测试点 \(11\) 和 \(16\) 期望得分 \(48\) 分。实际多过了一个测试点 \(12\),得 \(52\) 分,一般在考场上做到这个分数就可以回去检查前面的题目了。
namespace test_1_to_10{
int findpos(int x,int l,int r){
auto it=upper_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r)return -1;
return *it;
}
int calc(int x,int l,int r,int o,int flg){
//使用二分查找计算贡献
int pos=findpos(x,l+o,r);
if(!(~pos))return (((l&1)==flg)?(n+1):(-n-1));
if((pos&1)==(l&1))return pos;
int pos2=findpos(x,pos,r);
if(!(~pos2))return (((l&1)==flg)?pos:-pos);
else if((pos2&1)==(l&1))return pos;
else return -pos;
}
bool solve(int x,int y,int l,int r,int o){
if(x==y){ //同色,第一种情况
int pos=findpos(x,l-1,r);
if(!(~pos))return o;
else if((pos&1)==(l&1))return 1;
return 0;
}
int vx=calc(x,l-2,r,1,o),vy=calc(y,l-1,r,0,o);
//计算手牌贡献
for(int i=l;i<=r;++i){
if(vx==i)return 1; if(vy==i)return 0;
if(-vx==i)return 0; if(-vy==i)return 1;
//贡献产生,返回答案
if(x==y){ //同色,第一种情况
int pos=findpos(x,l-1,r);
if(pos==-1)return o;
else if((pos&1)==(l&1))return 1;
return 0;
}
//第二种情况,继续计算贡献,注意赢得越早越好,输得越晚越好
int val=calc(a[i],i,r,0,o);
if((i&1)==(l&1)){
if(val>0 && (val<vx || vx<0))vx=val,x=a[i];
else if(val<0 && val<vx && vx<0)vx=val,x=a[i];
}else{
if(val>0 && (val<vy || vy<0))vy=val,y=a[i];
else if(val<0 && val<vy && vy<0)vy=val,y=a[i];
}
}
return o;
}
void Main(){
while(m--){
int x,y,l,r; cin>>x>>y>>l>>r;
//假定平局也算一个人胜利
bool _A=solve(x,y,l,r,0);
bool _B=solve(x,y,l,r,1);
if(_A)cout<<"A\n"; //A 稳赢
else if(!_B)cout<<"B\n"; //B 稳赢
else cout<<"D\n"; //都不能稳赢
}
}
}
测试点 \(11 \sim 25\)
这部分参考了樱雪喵大佬的题解,在此表示感谢。
考虑快速获得结果。一般有两种方案:单次询问快速处理,或者离线处理多个询问。
根据题意,我们可以通过一定的观察,发现其实不需要维护接下来会使得自己输掉的牌。
证明:假设当前是第 \(x\) 回合,Alice 的手牌会使得她在回合 \(a\) 失败,并且她又摸到了一张会使得自己在回合 \(b\) 输掉的牌。显然,一定有 \(a>x,b>x,a \not = b\),且 \(a\) 与 \(b\) 都跟 \(x\) 的奇偶性不相同(有相同的就是自己抽到这张牌了,不符合使得自己失败的条件),即 \(\left|a-x\right| \bmod 2=1, \left|b-x\right| \bmod 2=1\)。因此,有至少一张牌是在 \(x+2\) 回合以后才会使得 Alice 失败,丢掉另一张牌就可以保证 Alice 不会立即失败。
注意需要特判 Bob 初始手牌会使得他立即输掉的情况。
根据这个性质,我们只需要获得最早使得自己胜利的牌的信息即可,最早胜利的牌就是每个区间内最小值所在位置,查询其奇偶性即可。我们发现区间内产生贡献的牌最多 \(3\) 张,并且会随着右端点的移动而发生贡献的更改,因此我们可以离线询问,从左向右遍历 \(r\),并使用一个支持单点修改,区间查询最小值位置的数据结构即可。线段树可以完美解决这个问题。
该算法时间复杂度 \(O((n+m) \log n)\)。
正解部分
#define N 200005
#define pii pair<int,int>
namespace Correct{
int ans[N][2];
vector<int>A[N];
struct que{ int x,y,l,r,id; };
vector<que>q[N];
int findpos(int x,int l,int r){
auto it=upper_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r)return -1;
return *it;
}
int calc(int x,int l,int r,int o,int flg){
int pos=findpos(x,l+o,r);
if(!(~pos))return INF;
if((pos&1)==(l&1))return pos;
int pos2=findpos(x,pos,r);
if(!(~pos2))return (((l&1)==flg)?pos:INF);
else if((pos2&1)==(l&1))return pos;
return INF;
//注意这里计算贡献的时候只考虑会产生胜利的情况,如果不能产生胜利贡献就应该返回 INF
}
struct segment_tree{ //单点修改,区间查询最小值及其位置
#define ls (k<<1)
#define rs (k<<1|1)
#define mid ((l+r)>>1)
pii sum[N<<2];
pii Min(pii x,pii y){
if(x.first<y.first)return x;
return y;
}
void buildseg(int k,int l,int r){
if(l==r){ sum[k]={INF,l}; return; }
buildseg(ls,l,mid); buildseg(rs,mid+1,r);
sum[k]=Min(sum[ls],sum[rs]);
}
void modify(int k,int l,int r,int pos,int val){
if(l==r){ sum[k]={val,l}; return; }
if(pos<=mid)modify(ls,l,mid,pos,val);
else modify(rs,mid+1,r,pos,val);
sum[k]=Min(sum[ls],sum[rs]);
}
pii query(int k,int l,int r,int ql,int qr){
if(ql<=l && r<=qr)return sum[k];
if(qr<=mid)return query(ls,l,mid,ql,qr);
if(ql>mid)return query(rs,mid+1,r,ql,qr);
return Min(query(ls,l,mid,ql,mid),query(rs,mid+1,r,mid+1,qr));
}
#undef ls
#undef rs
#undef mid
}seg;
int solve(int x,int y,int l,int r,int o){
if(x==y){
int pos=findpos(x,l-1,r);
if(!(~pos))return o;
else if((pos&1)==(l&1))return 1;
return 0;
} if(y==a[l]){
int pos=findpos(y,l,r);
if((~pos) && ((pos&1)==(l&1)))return 1;
else if(!(~pos))return o;
}
int vx=calc(x,l-2,r,1,o),vy=calc(y,l-1,r,0,o);
int res=min(vx,vy),P=(vx<vy)?(l-2):(l-1);
pii t=seg.query(1,1,n,l,r);
if(t.first<res)res=t.first,P=t.second;
if(res==INF)return o;
return ((P&1)==(l&1));
}
void Solve(int o){
//按照 r 从左向右扫一遍计算答案
seg.buildseg(1,1,n);
for(int r=1;r<=n;++r){
for(int i:A[r])seg.modify(1,1,n,i,calc(a[i],i,r,0,o));
//更改位置 i 的牌的贡献
for(auto i:q[r]){
int x=i.x,y=i.y,l=i.l,r=i.r,id=i.id;
ans[id][o]=solve(x,y,l,r,o);
}
}
}
void Main(){
for(int i=1;i<=m;++i){
q[vec[i-1].r].push_back({vec[i-1].x,vec[i-1].y,vec[i-1].l,vec[i-1].r,i});
} for(int i=1;i<=n;++i){
int nxt=findpos(a[i],i,n);
if(!(~nxt))continue;
A[nxt].push_back(i);
int nxt2=findpos(a[i],nxt,n);
if(~nxt2)A[nxt2].push_back(i);
} for(int flg=0;flg<=1;++flg)Solve(flg);
for(int i=1;i<=m;++i){
if(ans[i][0])cout<<"A\n";
else if(!ans[i][1])cout<<"B\n";
else cout<<"D\n";
}
}
}
完整代码,包含了所有的部分分,注意无注释
#include<bits/stdc++.h>
#define pii pair<int,int>
#define N 200005
using namespace std;
const int INF=0x3f3f3f3f;
int n,m,k,a[N];
vector<int>e[N];
struct QUE{ int x,y,l,r; };
vector<QUE>vec;
namespace test_1_to_5{
int f[105][105][105];
int dfs(int x,int y,int p,int r){
if(f[x][y][p]!=INF)return f[x][y][p];
if(p>r)return f[x][y][p]=0;
if((p&1) && x==a[p])return f[x][y][p]=1;
else if((p&1)==0 && y==a[p])return f[x][y][p]=2;
if(p&1){
int r1,r2;
if(a[p]==y)r1=2; else r1=dfs(x,y,p+1,r);
if(x==y)r2=2; else r2=dfs(a[p],y,p+1,r);
if(r1==2 && r2==2)return f[x][y][p]=2;
if(r1==1 || r2==1)return f[x][y][p]=1;
return f[x][y][p]=0;
}
int r1,r2;
if(a[p]==x)r1=1; else r1=dfs(x,y,p+1,r);
if(x==y)r2=1; else r2=dfs(x,a[p],p+1,r);
if(r1==1 && r2==1)return f[x][y][p]=1;
if(r1==2 || r2==2)return f[x][y][p]=2;
return f[x][y][p]=0;
}
void Main(){
while(m--){
memset(f,0x3f,sizeof(f));
int x,y,l,r; cin>>x>>y>>l>>r;
int res=dfs(x,y,l,r);
if(res==0)cout<<"D\n";
if(res==1)cout<<"A\n";
if(res==2)cout<<"B\n";
}
}
}
namespace test_11{
void Solve(int x,int y,int l,int r){
if(x==y){
auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r){ cout<<"D\n"; return; }
else{ cout<<(((*it)&1)?"A\n":"B\n"); return; }
} if(a[l]==x){ cout<<"A\n"; return; }
x=a[l]; ++l; auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it>r)){ cout<<"D\n"; return; }
else{ cout<<(((*it)&1)?"A\n":"B\n"); return; }
}
void Main(){
while(m--){
int x,y,l,r; cin>>x>>y>>l>>r;
Solve(x,y,l,r);
}
}
}
namespace test_1_to_10{
int findpos(int x,int l,int r){
auto it=upper_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r)return -1;
return *it;
}
int calc(int x,int l,int r,int o,int flg){
int pos=findpos(x,l+o,r);
if(!(~pos))return (((l&1)==flg)?(n+1):(-n-1));
if((pos&1)==(l&1))return pos;
int pos2=findpos(x,pos,r);
if(!(~pos2))return (((l&1)==flg)?pos:-pos);
else if((pos2&1)==(l&1))return pos;
else return -pos;
}
bool solve(int x,int y,int l,int r,int o){
if(x==y){
int pos=findpos(x,l-1,r);
if(!(~pos))return o;
else if((pos&1)==(l&1))return 1;
return 0;
}
int vx=calc(x,l-2,r,1,o),vy=calc(y,l-1,r,0,o);
for(int i=l;i<=r;++i){
if(vx==i)return 1; if(vy==i)return 0;
if(-vx==i)return 0; if(-vy==i)return 1;
if(x==y){
int pos=findpos(x,l-1,r);
if(pos==-1)return o;
else if((pos&1)==(l&1))return 1;
return 0;
}
int val=calc(a[i],i,r,0,o);
if((i&1)==(l&1)){
if(val>0 && (val<vx || vx<0))vx=val,x=a[i];
else if(val<0 && val<vx && vx<0)vx=val,x=a[i];
}else{
if(val>0 && (val<vy || vy<0))vy=val,y=a[i];
else if(val<0 && val<vy && vy<0)vy=val,y=a[i];
}
}
return o;
}
void Main(){
while(m--){
int x,y,l,r; cin>>x>>y>>l>>r;
bool _A=solve(x,y,l,r,0);
bool _B=solve(x,y,l,r,1);
if(_A)cout<<"A\n";
else if(!_B)cout<<"B\n";
else cout<<"D\n";
}
}
}
namespace test_16{
void Main(){
for(int i=1;i<=m;++i){
int x=vec[i-1].x,l=vec[i-1].l,r=vec[i-1].r;
auto it=lower_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r){ cout<<"D\n"; continue; }
cout<<(((*it)&1)?"A\n":"B\n");
}
}
}
namespace Correct{
int ans[N][2];
vector<int>A[N];
struct que{ int x,y,l,r,id; };
vector<que>q[N];
int findpos(int x,int l,int r){
auto it=upper_bound(e[x].begin(),e[x].end(),l);
if(it==e[x].end() || (*it)>r)return -1;
return *it;
}
int calc(int x,int l,int r,int o,int flg){
int pos=findpos(x,l+o,r);
if(!(~pos))return INF;
if((pos&1)==(l&1))return pos;
int pos2=findpos(x,pos,r);
if(!(~pos2))return (((l&1)==flg)?pos:INF);
else if((pos2&1)==(l&1))return pos;
return INF;
}
struct segment_tree{
#define ls (k<<1)
#define rs (k<<1|1)
#define mid ((l+r)>>1)
pii sum[N<<2];
pii Min(pii x,pii y){
if(x.first<y.first)return x;
return y;
}
void buildseg(int k,int l,int r){
if(l==r){ sum[k]={INF,l}; return; }
buildseg(ls,l,mid); buildseg(rs,mid+1,r);
sum[k]=Min(sum[ls],sum[rs]);
}
void modify(int k,int l,int r,int pos,int val){
if(l==r){ sum[k]={val,l}; return; }
if(pos<=mid)modify(ls,l,mid,pos,val);
else modify(rs,mid+1,r,pos,val);
sum[k]=Min(sum[ls],sum[rs]);
}
pii query(int k,int l,int r,int ql,int qr){
if(ql<=l && r<=qr)return sum[k];
if(qr<=mid)return query(ls,l,mid,ql,qr);
if(ql>mid)return query(rs,mid+1,r,ql,qr);
return Min(query(ls,l,mid,ql,mid),query(rs,mid+1,r,mid+1,qr));
}
#undef ls
#undef rs
#undef mid
}seg;
int solve(int x,int y,int l,int r,int o){
if(x==y){
int pos=findpos(x,l-1,r);
if(!(~pos))return o;
else if((pos&1)==(l&1))return 1;
return 0;
} if(y==a[l]){
int pos=findpos(y,l,r);
if((~pos) && ((pos&1)==(l&1)))return 1;
else if(!(~pos))return o;
}
int vx=calc(x,l-2,r,1,o),vy=calc(y,l-1,r,0,o);
int res=min(vx,vy),P=(vx<vy)?(l-2):(l-1);
pii t=seg.query(1,1,n,l,r);
if(t.first<res)res=t.first,P=t.second;
if(res==INF)return o;
return ((P&1)==(l&1));
}
void Solve(int o){
seg.buildseg(1,1,n);
for(int r=1;r<=n;++r){
for(int i:A[r])seg.modify(1,1,n,i,calc(a[i],i,r,0,o));
for(auto i:q[r]){
int x=i.x,y=i.y,l=i.l,r=i.r,id=i.id;
ans[id][o]=solve(x,y,l,r,o);
}
}
}
void Main(){
for(int i=1;i<=m;++i){
q[vec[i-1].r].push_back({vec[i-1].x,vec[i-1].y,vec[i-1].l,vec[i-1].r,i});
} for(int i=1;i<=n;++i){
int nxt=findpos(a[i],i,n);
if(!(~nxt))continue;
A[nxt].push_back(i);
int nxt2=findpos(a[i],nxt,n);
if(~nxt2)A[nxt2].push_back(i);
} for(int flg=0;flg<=1;++flg)Solve(flg);
for(int i=1;i<=m;++i){
if(ans[i][0])cout<<"A\n";
else if(!ans[i][1])cout<<"B\n";
else cout<<"D\n";
}
}
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
cin>>n>>m>>k; for(int i=1;i<=n;++i){
cin>>a[i]; e[a[i]].push_back(i);
}
if(n<=100){ test_1_to_5::Main(); return 0;}
if(k<=2){ test_11::Main(); return 0;}
if(n*m<=2000000){ test_1_to_10::Main(); return 0;}
bool FeatureC=1;
for(int i=1;i<=m;++i){
int x,y,l,r; cin>>x>>y>>l>>r;
if(x!=y)FeatureC=0;
vec.push_back({x,y,l,r});
}
if(FeatureC)test_16::Main();
else Correct::Main();
return 0;
}
事实上,只需要跑正解部分即可。