6438. 【GDOI2020模拟01.16】树上的鼠
题目
由于时间过于久远,而且题面本身也很清晰,所以就懒得另外叙述题目大意了(还有思考历程)。
正解
先考虑一条链的情况(长度为奇数,这里的长度是指点的数量):
如果根在中点,先手无论移到哪里,后手都可以移到它的对称点去。
此时先手必败;
如果根不在中点,先手只要一开始移到中点,先手就赢了。
若长度为偶数,就将中间的两个点都看成中点。
先手第一步先移到离根比较远的那个中点上,以后就用一样的策略,每次到达对方的对称点。所以偶数时先手必胜。
然后这就可以推广到一棵树的情况。
可以发现先手必败的情况当且仅当满足以下条件:
树的直径的长度为奇数,并且根是直径的中点。
于是就可以DP了。设\(f_{i,j}\)表示\(i\)为根的子树,最深点的深度为\(j\)的方案数。
发现直接跑这个东西会挂。
改一下定义,将“最深点深度为\(j\)”改成“最深点深度不超过\(j\)”
考虑转移。直接转移还是会挂。
然后就有了这个套路做法:长链剖分,在转移的时候先继承重儿子的信息,再和轻儿子的信息合并。
信息合并的时候,共同有的长度(两块信息的最小长度)上的信息可以暴力做,至于剩下的信息,可以发现就是个区间乘的操作。
线段树?没必要,直接打标记就可以了(有点像差分)。
所以信息合并的时间复杂度是两块信息的最小长度。
合并一次相当于减少了一条重链,总的时间复杂度就是所有重链的长度加起来,也就是\(O(n)\)
至于统计答案,这是有点复杂的,不过可以推。
这里就不详细解释了。
代码
代码可能有点丑,因为信息很多都是用链表来存的。
常数也很大。
using namespace std;
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cassert>
#include <list>
#define N 1000010
#define ll long long
#define mo 998244353
ll qpow(ll x,int y){
ll res=1;
for (;y;y>>=1,x=x*x%mo)
if (y&1)
res=res*x%mo;
return res;
}
int n;
struct EDGE{
int to;
EDGE *las;
} e[N*2];
int ne;
EDGE *last[N];
int q[N],fa[N],len[N],hs[N];
void getq(){
int head=1,tail=1;
q[1]=1;
while (head<=tail){
int x=q[head++];
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa[x]){
fa[ei->to]=x;
q[++tail]=ei->to;
}
}
}
list<ll> _data[N*2],*f[N],*tag[N];
int cnt;
void pd(list<ll>::iterator pf,list<ll>::iterator pt,list<ll> *t){
// assert(pt!=t->end());
ll tmp=*pt;
*pf=*pf*tmp%mo;
*pt=1;
++pt;
if (pt!=t->end())
*pt=*pt*tmp%mo;
}
ll pro[N],tagp[N],sum[N],ans,all[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d",&n);
for (int i=1;i<n;++i){
int u,v;
scanf("%d%d",&u,&v);
e[ne]={v,last[u]};
last[u]=e+ne++;
e[ne]={u,last[v]};
last[v]=e+ne++;
}
getq();
for (int i=n;i>=1;--i){
int x=q[i];
len[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (len[ei->to]+1>len[x])
len[x]=len[ei->to]+1,hs[x]=ei->to;
}
for (int i=n;i>=2;--i){
int x=q[i];
if (!hs[x]){
f[x]=&_data[++cnt];
tag[x]=&_data[++cnt];
f[x]->push_back(1);
tag[x]->push_back(1);
continue;
}
f[x]=f[hs[x]];
f[x]->push_front(1);
tag[x]=tag[hs[x]];
tag[x]->push_front(1);
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa[x] && ei->to!=hs[x]){
int y=ei->to;
auto pfx=f[x]->begin(),pfy=f[y]->begin();
auto ptx=tag[x]->begin(),pty=tag[y]->begin();
ll sumx=0,sumy=1;
pd(pfx,ptx,tag[x]);
sumx+=*pfx;
pfx++,ptx++;
for (int k=1;k<=f[y]->size();++k,++pfx,++pfy,++ptx,++pty){
pd(pfx,ptx,tag[x]),pd(pfy,pty,tag[y]);
(sumx+=*pfx)%=mo,(sumy+=*pfy)%=mo;
*pfx=((sumx*(*pfy)+sumy*(*pfx)-(*pfx)*(*pfy))%mo+mo)%mo;
}
if (pfx!=f[x]->end())
(*ptx*=sumy)%=mo;
}
}
// return 0;
for (int i=1;i<=n;++i)
pro[i]=1,tagp[i]=1;
int maxd=0;
for (EDGE *ei=last[1];ei;ei=ei->las){
int y=ei->to;
auto pfy=f[y]->begin(),pty=tag[y]->begin();
// printf("%d ",y);
for (int k=0;k<f[y]->size();++k,++pfy,++pty){
pd(pfy,pty,tag[y]);
// printf("%d ",*pfy);
}
// printf("\n");
maxd=max(maxd,(int)f[y]->size());
ll s=1;
pfy=f[y]->begin();
int k;
for (k=1;pfy!=f[y]->end();++pfy,++k){
s=(s+*pfy)%mo;
pro[k]=pro[k]*s%mo;
sum[k]=(sum[k]+*pfy*qpow((s-*pfy+mo)%mo,mo-2))%mo;
}
tagp[k]=tagp[k]*s%mo;
}
pro[0]=1;
for (int i=1;i<=maxd;++i){
pro[i]=pro[i]*tagp[i]%mo;
tagp[i+1]=tagp[i+1]*tagp[i]%mo;
tagp[i]=1;
ans=(ans+pro[i]-pro[i-1]-sum[i]*pro[i-1]%mo+mo+mo)%mo;
}
ans+=1;
for (int i=n;i>=1;--i){
int x=q[i];
all[x]=1;
for (EDGE *ei=last[x];ei;ei=ei->las)
if (ei->to!=fa[x])
all[x]=all[x]*(all[ei->to]+1)%mo;
}
printf("%lld\n",(all[1]-ans+mo)%mo);
return 0;
}
总结
做这种博弈题的时候,将当前局面转化成“位置一样,但选择变少”是一种比较妙的决策。
对于这种有关链的长度的信息的合并,可以考虑一下长链剖分。