6438. 【GDOI2020模拟01.16】树上的鼠

4 篇文章 0 订阅
4 篇文章 0 订阅

题目

由于时间过于久远,而且题面本身也很清晰,所以就懒得另外叙述题目大意了(还有思考历程)。

正解

先考虑一条链的情况(长度为奇数,这里的长度是指点的数量):
如果根在中点,先手无论移到哪里,后手都可以移到它的对称点去。
此时先手必败;
如果根不在中点,先手只要一开始移到中点,先手就赢了。
若长度为偶数,就将中间的两个点都看成中点。
先手第一步先移到离根比较远的那个中点上,以后就用一样的策略,每次到达对方的对称点。所以偶数时先手必胜。
然后这就可以推广到一棵树的情况。
可以发现先手必败的情况当且仅当满足以下条件:
树的直径的长度为奇数,并且根是直径的中点。

于是就可以DP了。设 f i , j f_{i,j} fi,j表示 i i i为根的子树,最深点的深度为 j j j的方案数。
发现直接跑这个东西会挂。
改一下定义,将“最深点深度为 j j j”改成“最深点深度不超过 j j j
考虑转移。直接转移还是会挂。
然后就有了这个套路做法:长链剖分,在转移的时候先继承重儿子的信息,再和轻儿子的信息合并。
信息合并的时候,共同有的长度(两块信息的最小长度)上的信息可以暴力做,至于剩下的信息,可以发现就是个区间乘的操作。
线段树?没必要,直接打标记就可以了(有点像差分)。
所以信息合并的时间复杂度是两块信息的最小长度。
合并一次相当于减少了一条重链,总的时间复杂度就是所有重链的长度加起来,也就是 O ( n ) O(n) 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;
}

总结

做这种博弈题的时候,将当前局面转化成“位置一样,但选择变少”是一种比较妙的决策。
对于这种有关链的长度的信息的合并,可以考虑一下长链剖分。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值