题解:The Game (Easy Version & Hard Version)
前言
这是最近 VP CF 遇到的。
感觉是套着博弈壳子的树上 DS,做起来思路也很自然,于是记录之。
思路分析
E1
经过手玩样例发现,对于 ,如果存在 不在 子树内且 ,此时 最大的 一定是必胜点。
原因是,这样的 满足,无论后手操作任意 ,先手都无法再次操作。
考虑用反证法证明。
假设操作完 后,依然存在两点 ,使得后手操作 后,先手依然可以操作 ,不难发现 。
考虑分类讨论:
-
如果 存在子孙关系,那么只能是 是 的祖先,否则 不是 最大的点, 才是。而如果 是 的祖先,那么操作完 后无法再操作 ,矛盾;
-
如果 不存在子孙关系,那么 一定可以取代 成为点权最大的点,矛盾。
所以,我们的结论是正确的。
实现上,可以用树状数组记录不同点权点的出现次数,在 dfs 的过程中差分即可。
总体复杂度 。
E2
感觉是一个题,会 E1 就会 E2。
根据 E1 的结论,当前局面是必败局面当且仅当不存在一个点 ,使得 子树外的点 满足 。
也就是说,只要在先手操作后依然存在一个合法的 ,他必败。
为了获胜,他必须将当前局面变成必败局面,也就是使得图上不存在上述合法的 。
然后,考虑哪些点满足在操作完它之后,当前局面是必败局面。
不难发现,这样的 ,对于任意 ,应当满足:
-
是 的祖先;
-
令 表示满足点权 且不在 子树内的点的共同 lca, 是 的祖先。
两条满足任意一个即可。也很好理解,要么 不存在,要么让后手选 之后必输。
然后按点权从大到小加入点,用 dfs 序线段树维护每个 对应的 ,再用一个树状数组做树上差分即可。
总体复杂度 ,常数很小,可以通过。
代码实现
E1
#include<bits/stdc++.h>
#define int long long
using namespace std;
int t,n,x,y,ans,a[400005],b[400005],c[400005],dfn[400005];
int head[400005],nxt[800005],target[800005],tot;
void add(int x,int y){
tot++;
nxt[tot]=head[x];
head[x]=tot;
target[tot]=y;
}
bool cmp(int x,int y){
return a[x]>a[y];
}
#define lowbit(i) (i&(-i))
void modify(int x,int k){
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=k;
}
}
int query(int x){
int ans=0;
for(int i=x;i;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
void dfs(int x,int fa){
int now=query(n)-query(a[x]);
modify(a[x],1);
for(int i=head[x];i;i=nxt[i]){
int y=target[i];
if(y==fa) continue;
dfs(y,x);
}
if(b[x]==query(n)-query(a[x])-now) b[x]=0;
else b[x]=1;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];
modify(a[i],1);
dfn[i]=i;
}
for(int i=1;i<=n;i++){
b[i]=query(n)-query(a[i]);
}
for(int i=1;i<n;i++){
cin>>x>>y;
add(x,y);
add(y,x);
}
dfs(1,0);
sort(dfn+1,dfn+1+n,cmp);
for(int i=1;i<=n;i++){
if(b[dfn[i]]){
ans=dfn[i];
break;
}
}
cout<<ans<<'\n';
ans=tot=0;
for(int i=1;i<=n;i++){
head[i]=b[i]=c[i]=0;
}
}
return 0;
}
E2
#include<bits/stdc++.h>
using namespace std;
int t,n,m,x,y,a[400005],num,tmp1,tmp2;
vector<int> ans,v[400005];
int head[400005],nxt[800005],target[800005],tot;
void add(int x,int y){
tot++;
nxt[tot]=head[x];
head[x]=tot;
target[tot]=y;
}
int siz[400005],dfn[400005],rnk[400005],hson[400005],top[400005],dep[400005],f[400005],cnt;
void dfs1(int x,int fa){
siz[x]=1;
for(int i=head[x];i;i=nxt[i]){
int y=target[i];
if(y==fa) continue;
dep[y]=dep[x]+1;
f[y]=x;
dfs1(y,x);
siz[x]+=siz[y];
if(siz[hson[x]]<siz[y]) hson[x]=y;
}
}
void dfs2(int x,int t){
cnt++;
dfn[x]=cnt;
rnk[cnt]=x;
top[x]=t;
if(!hson[x]) return;
dfs2(hson[x],t);
for(int i=head[x];i;i=nxt[i]){
int y=target[i];
if(y==f[x] || y==hson[x]) continue;
dfs2(y,y);
}
}
int lca(int x,int y){
if(!x) return y;
if(!y) return x;
while(top[x]^top[y]){
if(dep[top[x]]>dep[top[y]]) x=f[top[x]];
else y=f[top[y]];
}
if(dfn[x]<dfn[y]) return x;
else return y;
}
int val[800005],ls[800005],rs[800005],dcnt,rt;
void build(int l,int r,int &x){
x=++dcnt;
if(l==r) return;
int mid=(l+r)>>1;
build(l,mid,ls[x]);
build(mid+1,r,rs[x]);
}
void modify(int l,int r,int pos,int x){
if(l==r){
val[x]=rnk[l];
return;
}
int mid=(l+r)>>1;
if(pos<=mid) modify(l,mid,pos,ls[x]);
else modify(mid+1,r,pos,rs[x]);
val[x]=lca(val[ls[x]],val[rs[x]]);
}
int query(int l,int r,int ql,int qr,int x){
if(ql<=l && r<=qr) return val[x];
int mid=(l+r)>>1,ans=0;
if(ql<=mid) ans=lca(ans,query(l,mid,ql,qr,ls[x]));
if(qr>=mid+1) ans=lca(ans,query(mid+1,r,ql,qr,rs[x]));
return ans;
}
int c[400005];
#define lowbit(i) (i&(-i))
void chg(int x,int k){
if(!x) return;
for(int i=x;i<=n;i+=lowbit(i)){
c[i]+=k;
}
}
int get(int x){
int ans=0;
for(int i=x;i;i-=lowbit(i)){
ans+=c[i];
}
return ans;
}
void init(){
for(int i=1;i<=n;i++){
v[i].clear();
head[i]=dfn[i]=rnk[i]=siz[i]=hson[i]=top[i]=c[i]=0;
}
for(int i=1;i<=dcnt;i++){
val[i]=ls[i]=rs[i]=0;
}
ans.clear();
cnt=tot=num=dcnt=rt=0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>t;
while(t--){
cin>>n;
init();
for(int i=1;i<=n;i++){
cin>>a[i];
v[a[i]].push_back(i);
}
for(int i=1;i<n;i++){
cin>>x>>y;
add(x,y);
add(y,x);
}
dfs1(1,0);
dfs2(1,1);
build(1,n,rt);
for(int i=n;i>=1;i--){
for(int j=0;j<v[i].size();j++){
x=v[i][j];
if(dfn[x]==1) tmp1=0;
else tmp1=query(1,n,1,dfn[x],rt);
if(dfn[x]+siz[x]-1==n) tmp2=0;
else tmp2=query(1,n,dfn[x]+siz[x],n,rt);
y=lca(tmp1,tmp2);
if(!y) continue;
//cout<<"qq"<<x<<' '<<get(dfn[x]+siz[x]-1)<<' '<<get(dfn[x]-1)<<'\n';
if(get(dfn[x]+siz[x]-1)-get(dfn[x]-1)==num) ans.push_back(x);
}
for(int j=0;j<v[i].size();j++){
x=v[i][j];
if(dfn[x]==1) tmp1=0;
else tmp1=query(1,n,1,dfn[x],rt);
if(dfn[x]+siz[x]-1==n) tmp2=0;
else tmp2=query(1,n,dfn[x]+siz[x],n,rt);
y=lca(tmp1,tmp2);
//cout<<"qwq"<<x<<' '<<y<<'\n';
if(y){
num++;
if(x==y){
chg(dfn[x],1);
}else{
chg(dfn[x],1);
chg(dfn[y],1);
chg(dfn[lca(x,y)],-1);
}
}
}
for(int j=0;j<v[i].size();j++){
x=v[i][j];
modify(1,n,dfn[x],rt);
}
}
sort(ans.begin(),ans.end());
cout<<ans.size()<<' ';
for(int i=0;i<ans.size();i++){
cout<<ans[i]<<' ';
}
cout<<'\n';
}
return 0;
}
总结
总体来说我很喜欢这道题,由 E1 的提示引出了 E2 的正解,在得出正确的博弈策略之后,还需要用恰当的方式刻画博弈策略,以方便后续的维护。这既考察了选手对于复杂博弈问题的分析与刻画能力,在实现时也需要比较扎实的维护树上信息的能力,我认为这也是这道题能被被评到 *3000 的主要原因。
本文作者:Kenma
本文链接:https://www.cnblogs.com/Kenma/p/18699705
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步