【洛谷5281】[ZJOI2019] Minimax搜索(毒瘤动态DP)

点此看题面

  • 给定一棵(n)个点的树,规定初始情况下叶节点的权值为其自身编号。
  • 在这棵树中,深度为奇数的非叶节点权值为子节点权值最大值,深度为偶数的点为子节点权值最小值。
  • 你可以无限次花费(1)的代价给任意叶节点的权值(±1)
  • 对于一个叶节点集合,定义它的稳定度为只修改这个集合中的点权使得根节点权值发生变化的最小代价(如果始终无法改变根节点权值则代价为(n))。
  • 对于(Lsim R)中的所有稳定度,求对应的叶节点集合个数。
  • (nle2 imes10^5,1le Lle Rle n)

贡献路径

直接(DP)应该没有特别好的思路,所以即便是暴力也是需要经过一定转化的。

考虑我们先建出初始的树,因为叶节点的权值互不相同,所以我们可以把从根节点权值对应的叶节点到根的路径,称作对于答案的贡献路径。

显然,根节点的权值不会改变,充要于这条贡献路径上的所有权值都不会改变。

于是我们可以把这棵树按照贡献路径上的点拆成若干个独立的部分,每个部分都以一个贡献点为根(其实就是对应每个贡献点(x),把(x)子节点中贡献点(y)对应的子树从(x)的子树中删去)。

现在就把原问题分成了若干个相同的子问题。

暴力动态规划

根据题目的要求,我们去枚举每种稳定度计算方案。

当然计算稳定度恰好为(d)的方案并不好搞,所以我们计算稳定度小于等于(d)的方案(ans_d),那么稳定度恰为(d)的方案数就是(ans_d-ans_{d-1})

稳定度小于等于(d)的方案,其实就是给集合内某个叶节点权值改变(d)会影响根权值的叶节点集合个数。

不同的贡献子树之间的影响显然是独立的,所以我们只需对于每个贡献点(x)求出它子树内稳定度小于等于(d)的集合个数(f_x)和集合总数(ct_x)(设(m)(x)子树内的叶节点个数,则(ct_x=2^m)),则(prod (ct_x-f_x))就是稳定度大于(d)的集合个数。

(tot)为叶节点集合总数,那么(ans_d)就等于(tot-prod(ct_x-f_x))

发现(f_x)表示稳定度小于等于(d)的集合个数这个定义还是太抽象了。

所以,对于深度为奇数的贡献点的子树内,我们定义深度为奇数的点的(f_x)为能使(x)的权值大于(v_1)(根节点的初始权值)的集合个数,深度为偶数的点的(f_x)为能使(x)的权值小于等于(v_1)的集合个数。同理,对于深度为偶数的贡献点的子树内,定义深度为偶数的点的(f_x)为能使(x)的权值小于等于(v_1)的集合个数,深度为奇数的点的(f_x)为能使(x)的权值大于(v_1)的集合个数。

考虑这样定义的好处,就是统一了(DP)的转移方程:

[f_x=ct_x-prod_{yin son_x}f_y ]

以深度为奇数的贡献点子树内一个深度为奇数的点为例解释一下,因为奇数对应的是最大值,所以除非全部子节点权值都小于等于(v_1)(对应方案数为(prod_{yin son_x}f_y)),就必然能使(x)符合条件,那么就用总方案数(ct_x)减去不合法方案数即可。(其他三类情况容易发现也有类似的解释)

但我们还需要求出(DP)的边界,即叶节点的(f)值。

要求这个我们还要知道一个比较显然的结论,就是对于一个深度为奇数的点,我们必然只可能它子树内的叶节点加上(d),反之对于深度为偶数的点必然只可能子树内的叶节点减去(d)

又由于一个叶节点只有被选入和不被选入两种情况,所以以深度为奇数的贡献点子树内一个深度为奇数的叶节点(x)为例,就应该有:

[f_x=[v_x>v_1]+[v_x+d>v_1] ]

所以讲了这么多我们终于解决了暴力(DP)

从暴力(DP)向动态(DP)

发现转移方程都是一样的,在(d)变化的过程中发生变化的只有叶节点的(f)值,符合动态(DP)的条件。

刚好这题要按照贡献路径上的点拆树,因此自然而然想到用(LCT)求解动态(DP)

每个点(x)的子节点(y)对它的贡献都可以表示为(f_x=kf_y+b)的形式,其中(k=-prod_{zin son_x,z ot=y}f_z)(也就是所有虚儿子(f_z)的乘积),(b=ct_x)

(LCT)上维护一个点所有虚儿子的信息应该也是一个比较套路的问题,相信写过动态(DP)的人应该都会,这里就不多提了。

有一个问题是我们需要维护乘积,而具体实现过程中很可能会出现(0),而乘上了(0)是无法直接撤回的。

所以对于每个数都应该把它看作是由一个非零数和若干个(0)相乘得到的,那么只要维护值的同时维护好(0)的个数即可,这可以直接绑一个(struct)

由于除法需要求逆元,所以复杂度应该是两个(log)的。

代码:(O(nlog^2n))

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 200000
#define X 998244353
#define add(x,y) (e[++ee].nxt=lnk[x],e[lnk[x]=ee].to=y)
using namespace std;
int n,L,R,a[N+5],k[N+5],ct[N+5],ans[N+5],ee,lnk[N+5];struct edge {int to,nxt;}e[N<<1];
I int QP(RI x,RI y) {RI t=1;W(y) y&1&&(t=1LL*t*x%X),x=1LL*x*x%X,y>>=1;return t;}
struct Data
{
	int t,x,v;I Data(CI i=0,CI a=0,CI b=0):t(i),x(a),v(b){}
	I bool operator < (Con Data& o) Con {return t<o.t;}
}s[N+5];
namespace FastIO
{
	#define FS 100000
	#define tc() (FA==FB&&(FB=(FA=FI)+fread(FI,1,FS,stdin),FA==FB)?EOF:*FA++)
	#define pc(c) (FC==FE&&(clear(),0),*FC++=c)
	int OT;char oc,FI[FS],FO[FS],OS[FS],*FA=FI,*FB=FI,*FC=FO,*FE=FO+FS;
	I void clear() {fwrite(FO,1,FC-FO,stdout),FC=FO;}
	Tp I void read(Ty& x) {x=0;W(!isdigit(oc=tc()));W(x=(x<<3)+(x<<1)+(oc&15),isdigit(oc=tc()));}
	Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
	Tp I void write(Ty x) {W(OS[++OT]=x%10+48,x/=10);W(OT) pc(OS[OT--]);pc(' ');}
}using namespace FastIO;
class LinkCutTree
{
	private:
		#define PU(x) (O[x].G=DV((X-O[x].IV.Val())%X,O[x].B),
			O[x].S[0]&&(O[x].G=O[O[x].S[0]].G*O[x].G,0),O[x].S[1]&&(O[x].G=O[x].G*O[O[x].S[1]].G,0))//动态DP经典上传信息
		#define IR(x) (O[O[x].F].S[0]^x&&O[O[x].F].S[1]^x)
		#define Wh(x) (O[O[x].F].S[1]==x)
		#define Co(x,y,d) (O[O[x].F=y].S[d]=x)
		struct Num
		{
			int v,c;I Num(CI x,CI y):v(x),c(y){}I Num(CI x=1) {x?(v=x,c=0):(v=c=1);}//v记录非零值,c记录0的个数
			I Num operator * (Con Num& o) Con {return Num(1LL*v*o.v%X,c+o.c);}//乘法
			I Num operator / (Con Num& o) Con {return Num(1LL*v*QP(o.v,X-2)%X,c-o.c);}//除法
			I int Val() {return c?0:v;}//实际值,判断是否有0
		}res;
		struct DV
		{
			int k,b;I DV(CI x=0,CI y=0):k(x),b(y){}
			I DV operator * (Con DV& o) {return DV(1LL*k*o.k%X,(1LL*k*o.b+b)%X);}//k1(k2x+b2)+k1
			I int Calc(CI x) {return (1LL*k*x+b)%X;}//将x代入求值,实际上只会用到Calc(1)
		};
		struct node {DV G;Num IV;int B,F,S[2];}O[N+5];
		I void Ro(RI x) {RI f=O[x].F,p=O[f].F,d=Wh(x);
			!IR(f)&&(O[p].S[Wh(f)]=x),O[x].F=p,Co(O[x].S[d^1],f,d),Co(f,x,d^1),PU(f);}
		I void S(RI x) {RI f;W(!IR(x)) f=O[x].F,!IR(f)&&(Ro(Wh(x)^Wh(f)?x:f),0),Ro(x);PU(x);}
		I void Ac(RI x)
		{
			for(RI y=0,z;x;O[x].S[1]=y,PU(x),x=O[y=x].F) S(x),
				(z=O[x].S[1])&&(O[x].IV=O[x].IV*O[z].G.Calc(1),0),y&&(O[x].IV=O[x].IV/O[y].G.Calc(1),0);//注意维护虚儿子总乘积
		}
	public:
		I void Init(CI x,CI v,CI fg=0) {fg&&(O[x].IV=Num(0),0),O[x].B=v,PU(x);}//对于叶节点(fg=1),令其虚儿子总乘积为0
		I void Link(CI x,CI y) {O[y].F=x,O[x].IV=O[x].IV*Num(O[y].G.Calc(1));}//连边,维护虚儿子总乘积
		I void A(CI x) {Ac(x),res=res*Num((ct[x]-O[x].G.Calc(1)+X)%X);}//计入贡献
		I void E(CI x) {Ac(x),res=res/Num((ct[x]-O[x].G.Calc(1)+X)%X);}//除自贡献
		I void U(CI x,CI v) {Ac(x),S(x),O[x].B+=v,PU(x);}I int Q() {return res.Val();}//单点修改;询问总乘积
}LCT;
int v[N+5],Lf[N+5];I void dfs(CI x,CI lst=0)//求出初始所有点的权值
{
	v[x]=k[x]?0:n,Lf[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&
		(k[e[i].to]=k[x]^1,dfs(e[i].to,x),v[x]=k[x]?max(v[x],v[e[i].to]):min(v[x],v[e[i].to]),Lf[x]=0);
	Lf[x]&&(v[x]=x);//叶节点权值为自身标号
}
int cnt,tot=1,id[N+5];I void Init(CI x,CI lst=0)//假设d=0,完成初始局面的DP
{
	if(id[x]=v[x]^v[1]?id[lst]:x,Lf[x])//id记录每个点所在的贡献子树的根
	{
		if(ct[x]=2,tot=2LL*tot%X,v[x]==v[1]) return;//叶节点ct=2,tot记录集合总数,如果当前点是贡献点则不考虑
		if(k[id[x]]) LCT.Init(x,2*((x<=v[1])^k[x]),1),x<=v[1]&&(s[++cnt]=Data(v[1]-x+1,x,k[x]?1:-1),0);//如果在深度为奇数的贡献点子树内,且还要再分深度讨论
		else LCT.Init(x,2*((x<v[1])^k[x]),1),x>=v[1]&&(s[++cnt]=Data(x-v[1]+1,x,k[x]?-1:1),0);//如果在深度为偶数的贡献点子树内,且还要再分深度讨论
		return LCT.Link(lst,x);//和父节点连边
	}
	ct[x]=1;for(RI i=lnk[x];i;i=e[i].nxt) e[i].to^lst&&
		(Init(e[i].to,x),v[e[i].to]^v[1]&&(ct[x]=1LL*ct[x]*ct[e[i].to]%X));//ct记录贡献子树的总方案数
	LCT.Init(x,ct[x]),v[x]^v[1]?LCT.Link(lst,x):LCT.A(x);//如果不是贡献点则和父节点连边,如果是贡献点则把当前点的值计入总乘积
}
int main()
{
	RI i,j,x,y;for(read(n,L,R),i=1;i^n;++i) read(x,y),add(x,y),add(y,x);
	for(k[1]=1,dfs(1),Init(1),sort(s+1,s+cnt+1),i=j=1;i^n;ans[i++]=(tot-LCT.Q()+X)%X)//总方案-不合法方案
		W(j<=cnt&&s[j].t==i) LCT.E(id[s[j].x]),LCT.U(s[j].x,s[j].v),LCT.A(id[s[j++].x]);//处理在d=i时变化的叶节点,除去贡献、更新值、计回贡献
	for(ans[n]=tot-1,i=n;i;--i) ans[i]=(ans[i]-ans[i-1]+X)%X;//差分求出恰好为d的方案
	for(i=L;i<=R;++i) write(ans[i]);return clear(),0;
}
败得义无反顾,弱得一无是处
原文地址:https://www.cnblogs.com/chenxiaoran666/p/Luogu5281.html