【洛谷5291】[十二省联考2019] 希望(容斥+长链剖分+回退数据结构+离线求逆元)
大致题意: 给你一棵树,让你找出\(k\)个连通块,使得这些连通块交集中存在一点让这些连通块中任意一点到这个点的距离不超过\(L\)。求选择连通块的方案数。
容斥
考虑如果直接求每个点对答案的贡献,即找出对于每个点存在多少符合条件的连通块方案数,显然会算重。
不难发现一个结论,即对于固定的\(k\)个连通块,满足条件的点必然会构成一个连通块
考虑到这是一棵树,而树上连通块一个最基本的性质就是点数\(=\)边数\(+1\)。
所以,如果我们用每个点的贡献,减去每条边的贡献,就恰好把每种情况计算了一次。
其中边的贡献就是指满足其两端点皆符合条件的连通块方案数。
于是,这里就完成了第一步预处理。
暴力\(DP\)
接下来,我们考虑一个\(O(nL)\)的暴力\(DP\)。
设\(f_{x,i}\)表示在\(x\)的子树内,与\(x\)距离不超过\(i\)的连通块个数\(+1\)(\(+1\)是为了方便转移),\(g_{x,i}\)表示在\(x\)的子树外,包括\(x\)且不包括\(x\)子树内其他节点,与\(x\)距离不超过\(i\)的连通块个数。
然后易推得暴力转移方程:
由于所有距离不超过\(x\)的子节点\(i-1\)的点,与\(x\)的距离必然不超过\(i\),因此可以这样转移。
由于父节点的一个子节点,到当前点需要经过父节点再到达当前点,相差距离为\(2\),所以要从\(i-2\)转移。
考虑如何计算答案。
点的贡献自然就是\(((f_{x,L}-1)\cdot g_{x,L})^k\)。
而对于边我们只考虑点向父亲的连边,则满足条件的连通块要到当前点及其父节点距离均不超过\(L\),也就是\(((f_{x,L-1}-1)(g_{x,L}-1))^k\),注意,这里\(g_{x,L}\)要减\(1\)是要除去选择\(x\)的贡献。
以上就是暴力\(DP\)的全部内容了。
长链剖分优化\(f\)
由于这个式子与节点深度有一定关系,所以我们可以用长链剖分来进行优化。
考虑对于\(f\),就是比较经典的长链剖分优化题,直接从长儿子那里继承贡献,非长儿子暴力转移。
注意式子后面的\(+1\),这可以通过给整段数组打加法标记来解决。
但有一个细节,即我们在这里对于\(f_{v}\)只求到第\(len_v\)位,而由于我们定义为小于等于,所以实际上第\(len_v+1\sim∞\)位都是有值的,且都等于\(f_{v,len_v}\)。
可转移时,对于后面那些值我们不能暴力转移,不然就劣化了复杂度。
考虑如果\(f_{v,len_v}\)为\(0\),则相当于把后面这一部分全部赋值为\(0\),因此我们维护一个标记\(Lim_x\)表示当前\(f_x\)从多少位开始全部为\(0\)即可。
如果不为\(0\),那么我们考虑给全局打一个乘法标记,然后对于序列的前面不应受乘法影响的部分暴力乘上\(f_{v,len_v}\)的逆元即可。
这样转移的复杂度与前面\(DP\)的时间复杂度是一致的,因此可以保证复杂度。
长链剖分优化\(g\)
\(g\)看似难搞,实际上也可以用长链剖分来转移。
考虑到\(g\)是由父亲向子节点转移的,因此我们可以采用与普通长链剖分类似的套路,将父节点的答案遗传给长儿子,然后非长儿子暴力转移。
但主要要考虑\(\prod f_{son_{fa_x}(≠x),i-2}\)怎么算。
一个比较\(naive\)的想法,就是可以发现这个式子相当于:
这样似乎可以了?
且慢,\(f_{x,i-2}\)是有可能为\(0\)的!那不是凉了。
所以,我们要换个想法。
我们可以逐一枚举\(fa_x\)的子节点,然后统计当前点之前的积,求出当前点之后的积,这样就能非常方便地计算当前点的\(g\)值了。
当前点之前的积可以边做边统计,但当前点之后的积该怎么求,就有点麻烦了。
回退数据结构:求当前点之后的积
要求当前点之后的积,我们可以发现,如果我们在求\(f\)的时候按照子节点\(len\)由大到小的顺序去做,然后求\(g\)的时候换个方向按照子节点\(len\)由小到大的顺序去做,那么求\(g\)时当前点之后的积,就是求\(f\)时当前点之前的积。
而求\(f\)时我们是边做边统计积的,所以处理到每个点时的\(f\)值,其实就是当前点之前的积。
如果开棵主席树,就可以很方便地维护下后缀积,但\(n=10^6\)时这个\(log\)显得很不优秀。
于是就要用到回退数据结构这个神奇的东西。
说起来其实也很简单,我们把每次操作修改的值全部存下原来的值扔到一个栈里,然后每扫到一个点就将这个点栈清空,把栈中所有元素还原为原先的值。
这就是回退数据结构了。
注意这里的栈最好是使用\(STL\)的\(list\)而不是\(stack\),因为\(list\)用指针实现,而\(stack\)用迭代器实现,\(list\)内存较小。
离线求逆元
回顾一下之前的全部内容,我们可以发现还有一个地方会使复杂度带上一个\(log\),即对\(f\)进行转移时给全局打上乘法标记并暴力给不应受影响部分乘逆元时求逆元的过程。
这看起来似乎无法解决,但是,我们可以发现,我们只需使用\(f_{v,len_v}\)的逆元!
所以,我们可以先扫一遍求出每一个\(f_{x,len_x}\),然后再将它们一起离线求一遍逆元。
什么?你问怎么离线求逆元?
假设我们要求出\(a_1,a_2,a_3,...,a_t\)的逆元,则我们先做一遍前缀积,使得
然后我们求出\(s_t\)的逆元即:
考虑用\(s_{t-1}\)去乘上这个式子,得到:
这样我们就求出了\(a_t\)的逆元。
则我们用\(a_t\)乘上\(s_t\)的逆元,得到\(s_{t-1}\)的逆元,接下来以这种方式继续搞下去就得到了所有逆元。
这种思想应该还是很妙的。
其实线性求阶乘逆元就用了这种思想。
代码
最后放上代码。。。顺便说明标程\(800\)多行是因为手写\(STL\),如果直接调用\(list\)正常情况下也就\(100\)多行而已。
#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 1000000
#define X 998244353
#define min(x,y) ((x)<(y)?(x):(y))
#define max(x,y) ((x)>(y)?(x):(y))
#define Gmin(x,y) (x>(y)&&(x=(y)))
#define Gmax(x,y) (x<(y)&&(x=(y)))
#define Inc(x,y) ((x+=(y))>=X&&(x-=X))
#define Dec(x,y) ((x-=(y))<0&&(x+=X))
#define XSum(x,y) ((x)+(y)>=X?(x)+(y)-X:(x)+(y))
#define XSub(x,y) ((x)<(y)?(x)-(y)+X:(x)-(y))
#define pb push_back
#define Assign(x) (f[x]=pf,pf+=T.len[x]+2,pg+=T.len[x]+2,g[x]=pg-max(l-T.len[x],0),pg+=T.len[x]+2)
using namespace std;
int n,l,k,ee,ans,lnk[N+5];vector<int> e[N+5];
int _f[N<<4],_g[N<<4],*f[N+5],*g[N+5],*pf=_f,*pg=_g;
class FastIO
{
private:
#define FS 100000
#define tc() (A==B&&(B=(A=FI)+fread(FI,1,FS,stdin),A==B)?EOF:*A++)
#define tn (x<<3)+(x<<1)
#define D isdigit(c=tc())
char c,*A,*B,FI[FS];
public:
I FastIO() {A=B=FI;}
Tp I void read(Ty& x) {x=0;W(!D);W(x=tn+(c&15),D);}
Ts I void read(Ty& x,Ar&... y) {read(x),read(y...);}
#undef FS
}F;
I int Qpow(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;}//快速幂
class TreeIniter//初始化
{
private:
int u[N+5],w[N+5];vector<int> B[N+5];//B用于桶排,避免产生log
I void Pre(CI x,CI lst)//初始化
{
RI i,j,sz,Mx;for(Inv[x]=1,i=0,sz=e[x].size();i^sz;++i) e[x][i]^lst&&
(Pre(e[x][i],x),len[e[x][i]]>len[s[x]]&&(s[x]=e[x][i]),Inv[x]=1LL*Inv[x]*Inv[e[x][i]]%X);//求出长儿子,Inv这里求出f[x][len[x]]
if(len[x]=len[s[x]]+1,Inc(Inv[x],1),!s[x]) return;//如果没有长儿子(即没有子节点)就退出函数
for(Mx=i=0;i^sz;++i) e[x][i]^lst&&e[x][i]^s[x]&&
(B[len[e[x][i]]].pb(e[x][i]),Gmax(Mx,len[e[x][i]]));//扔桶里,同时确定桶排上界
for(i=Mx;~i;--i) {for(j=0,sz=B[i].size();j^sz;++j) S[x].pb(B[i][j]);B[i].clear();}//桶排,注意此处无需加入长儿子
}
public:
int len[N+5],s[N+5],Inv[N+5];vector<int> S[N+5];
I void Init()//初始化
{
RI i,x,t=0;for(len[0]=-1,Pre(1,0),w[0]=i=1;i<=n;++i) Inv[i]&&(u[++t]=i,w[t]=1LL*w[t-1]*Inv[i]%X);//统计前缀和
for(w[t]=Qpow(w[t],X-2),i=t;i;--i) x=Inv[u[i]],Inv[u[i]]=1LL*w[i]*w[i-1]%X,w[i-1]=1LL*w[i]*x%X;//计算逆元
}
}T;
class BackStack//回退数据结构
{
private:
struct Pr {int *w,v;};list<Pr> t;
public:
I void Ins(int& x) {t.pb(Pr{&x,x});}//加入元素
I void Regain() {W(!t.empty()) *(t.back().w)=t.back().v,t.pop_back();}//还原
}S[N+5];
class Solver_for_F//求f
{
private:
int Mul[N+5],Add[N+5],IMul[N+5],Lim[N+5],Zero[N+5];
I void dfs(CI x,CI lst)
{
RI ls=T.s[x];if(!T.s[x]) {Lim[x]=Mul[x]=IMul[x]=Add[x]=1;goto End;}//如果没有儿子
f[T.s[x]]=f[x]+1,dfs(T.s[x],x),Mul[x]=Mul[T.s[x]],Add[x]=Add[T.s[x]],IMul[x]=IMul[T.s[x]],
Lim[x]=Lim[T.s[x]]+1,Zero[x]=Zero[T.s[x]],f[x][0]=IGV(x,1);//处理长儿子,继承信息
RI i,j,v,w,sz;for(i=0,sz=T.S[x].size();i^sz;++i)
{
for(v=T.S[x][i],ls=v,Assign(v),dfs(v,x),j=0;j<=T.len[v]+1;++j)
Lim[x]==j&&(S[v].Ins(Lim[x]),S[v].Ins(f[x][j]),f[x][Lim[x]++]=Zero[x]),//更新从哪一位开始为0
S[v].Ins(f[x][j]),f[x][j]=IGV(x,1LL*GV(x,j)*(j?GV(v,j-1):1)%X);//更新
if(T.len[x]<=T.len[v]+1) continue;if(!(w=GV(v,T.len[v])))//如果为0
S[v].Ins(Lim[x]),S[v].Ins(Zero[x]),Lim[x]=T.len[v]+1,Zero[x]=IGV(x,0);//特殊处理
else
{
S[v].Ins(Mul[x]),S[v].Ins(Add[x]),S[v].Ins(IMul[x]),//加入
Mul[x]=1LL*Mul[x]*w%X,Add[x]=1LL*Add[x]*w%X,IMul[x]=1LL*IMul[x]*T.Inv[v]%X;//打全局标记
for(j=0;j<=T.len[v]+1;++j) S[v].Ins(f[x][j]),f[x][j]=IGV(x,1LL*GV(x,j)*T.Inv[v]%X);//前缀乘逆元
}
}S[ls].Ins(Add[x]);End:Inc(Add[x],1);//最后加1
}
public:
I int GV(CI x,RI p) {return Gmin(p,T.len[x]),XSum(1LL*Mul[x]*(p<Lim[x]?f[x][p]:Zero[x])%X,Add[x]);}//求出某一位置的实际值
I int IGV(CI x,CI v) {return 1LL*IMul[x]*XSub(v,Add[x])%X;}I void Solve() {Assign(1),dfs(1,0);}//将实际值变成序列中的值
}FS;
class Solver_for_G//求出g
{
private:
int u[N+5],Mul[N+5],Add[N+5],IMul[N+5],Lim[N+5],Zero[N+5];
I void dfs(CI x,CI lst)
{
u[0]=1,reverse(T.S[x].begin(),T.S[x].end()),Inc(ans,Qpow(1LL*XSub(FS.GV(x,l),1)*GV(x,l)%X,k)),//翻转儿子序列,统计答案
lst&&Dec(ans,Qpow(1LL*XSub(FS.GV(x,l-1),1)*XSub(GV(x,l),1)%X,k));if(!T.s[x]) return;//统计答案
RI i,j,v,w,sz,Prod=1,Mx=0;for(i=0,sz=T.S[x].size();i^sz;++i)
{
S[v=T.S[x][i]].Regain(),Lim[v]=l+1,Mul[v]=IMul[v]=1;//还原,初始化(注意这里的数组与上面的数组不一样
for(j=max(l-T.len[v],0);j<=l;++j) g[v][j]=
XSum(1LL*(j?GV(x,j-1):0)*(j>1?1LL*FS.GV(x,j-1)*(j-2>Mx?Prod:u[j-2])%X:1)%X,1);//转移
for(j=0;j<=T.len[v];++j) u[j]=1LL*(j>Mx?Prod:u[j])*FS.GV(v,j)%X;//统计某一位置之前的积
Prod=1LL*Prod*FS.GV(v,Mx=T.len[v])%X;//统计超出长度的位置的积
}
g[T.s[x]]=g[x]-1,Mul[T.s[x]]=Mul[x],Add[T.s[x]]=Add[x],IMul[T.s[x]]=IMul[x],//将信息遗传给长儿子
Lim[T.s[x]]=Lim[x]+1,Zero[T.s[x]]=Zero[x],l<=T.len[T.s[x]]&&(g[T.s[x]][0]=IGV(T.s[x],0)),
Gmax(Lim[T.s[x]],l-T.len[T.s[x]]);for(i=0;i^sz;++i)//处理其他点对长儿子的影响
{
for(v=T.S[x][i],j=max(l-T.len[T.s[x]],0);j<=min(l,T.len[v]+2);++j)
Lim[T.s[x]]==j&&(g[T.s[x]][Lim[T.s[x]]++]=Zero[T.s[x]]),//更新从哪一位开始为0
g[T.s[x]][j]=IGV(T.s[x],1LL*GV(T.s[x],j)*(j>1?FS.GV(v,j-2):1)%X);//更新
if(l<=T.len[v]+2) continue;if(!(w=FS.GV(v,T.len[v])))//如果为0
Lim[T.s[x]]=T.len[v]+2,Zero[T.s[x]]=IGV(T.s[x],0);//特殊处理
else
{
Mul[T.s[x]]=1LL*Mul[T.s[x]]*w%X,Add[T.s[x]]=1LL*Add[T.s[x]]*w%X,IMul[T.s[x]]=1LL*IMul[T.s[x]]*T.Inv[v]%X;//打全局标记
for(j=max(l-T.len[T.s[x]],0);j<=min(l,T.len[v]+2);++j) g[T.s[x]][j]=IGV(T.s[x],1LL*GV(T.s[x],j)*T.Inv[v]%X);//前缀乘逆元
}
}
for(Inc(Add[T.s[x]],1),dfs(T.s[x],x),i=0;i^sz;++i) (v=T.S[x][i])^lst&&(dfs(v,x),0);//处理其他儿子
}
public:
I int GV(CI x,CI p) {return XSum(1LL*Mul[x]*(p<Lim[x]?g[x][p]:Zero[x])%X,Add[x]);}//与之前类似
I int IGV(CI x,CI v) {return 1LL*IMul[x]*XSub(v,Add[x])%X;}
I void Solve() {Mul[1]=Add[1]=IMul[1]=1,Lim[1]=l+1,dfs(1,0);}
}GS;
int main()
{
RI i,x,y;for(F.read(n,l,k),i=1;i^n;++i) F.read(x,y),e[x].pb(y),e[y].pb(x);//读入
return T.Init(),FS.Solve(),GS.Solve(),printf("%d",ans),0;//求解并输出答案
}