【十二省联考2019】希望【点边容斥】【换根dp】【长链剖分】【线性数据结构】【回退数据结构】【离线逆元】
題意:給一棵樹,兩個參數 k,Lk,Lk,L,需要選擇 kkk 個連通塊,使得這 kkk 個連通塊存在一個公共點,且該公共點到 kkk 個連通塊內的任意一點的距離不超過 LLL,求方案數 模 998244353998244353998244353。兩種方案不同當且僅當連通塊的集合不同。
n≤106,k≤10n\leq 10^6,k\leq 10n≤106,k≤10
已經寫絕望了
對于一種連通塊的集合,合法的欽定的點一定是一個連通塊。所以可以通過 點數?邊數=1點數-邊數=1點數?邊數=1 來容斥。即考慮每個點的貢獻,再減去對于每條邊,兩個端點都合法的方案。
然后考慮暴力 dp。
設 f(u,L)f(u,L)f(u,L) 為以 uuu 為根的子樹內,包含 uuu,到的距離不超過 LLL 的連通塊個數 +1+1+1(為了方便轉移,也可理解為允許為空)。
g(u,L)g(u,L)g(u,L) 表示 uuu 往上走,即必須包含 uuu,且不能包含 uuu 子樹內其他結點,到 uuu 的距離不超過 LLL 的連通塊個數。(注意不 +1+1+1,即不能為空。)
得到轉移
f(u,L)=∏v∈son(u)f(v,L?1)+1f(u,L)=\prod_{v\in son(u)}f(v,L-1)+1f(u,L)=v∈son(u)∏?f(v,L?1)+1
邊界 f(u,0)=1f(u,0)=1f(u,0)=1
g(u,L)=g(fau,L?1)∏v∈son(fau),v≠uf(v,L?2)+1g(u,L)=g(fa_u,L-1)\prod_{v\in son(fa_u),v\neq u}f(v,L-2)+1g(u,L)=g(fau?,L?1)v∈son(fau?),v?=u∏?f(v,L?2)+1
邊界 f(u,0)=f(u,?1)=1f(u,0)=f(u,-1)=1f(u,0)=f(u,?1)=1。后面這個 +1+1+1 表示 {u}\{u\}{u} 這個連通塊。
最終答案為
∑u=1n(f(u,L)?1)kg(u,L)k?[u≠rt](f(u,L?1)?1)k(g(u,L)?1)k\sum_{u=1}^n(f(u,L)-1)^kg(u,L)^k-[u\neq rt](f(u,L-1)-1)^k(g(u,L)-1)^ku=1∑n?(f(u,L)?1)kg(u,L)k?[u?=rt](f(u,L?1)?1)k(g(u,L)?1)k
發現狀態和深度有關,考慮長鏈剖分。以下設 mxumx_umxu? 表示 uuu 到子樹內最遠點經過的 點數,簡稱深度。
f(u,L)=∏v∈son(u)f(v,L?1)+1f(u,L)=\prod_{v\in son(u)}f(v,L-1)+1f(u,L)=v∈son(u)∏?f(v,L?1)+1
這個是經典的長鏈剖分的形式,直接繼承長兒子的信息,短兒子暴力轉移。
然后狀態定義的是不超過,所以你維護的只是 DP 數組里 [0,mxu)[0,mx_u)[0,mxu?) 的信息, [mxu,+∞)[mx_u,+\infin)[mxu?,+∞) 也是有值的。如果暴力到長兒子的深度會讓復雜度退化。
不過注意到 [mxu,+∞)[mx_u,+\infin)[mxu?,+∞) 內的值都是 f(u,mxu?1)f(u,mx_u-1)f(u,mxu??1),所以相當于是個后綴乘法。然后 DP 式子后面還有個 +1 ,相當于要維護以下操作:
這可以通過打全局標記來實現。具體來講,我們對當前點 uuu 維護兩個標記 mulu,plsumul_u,pls_umulu?,plsu?,表示存儲的一個數 xxx 表示的真實值為 mulux+plsumul_u x+pls_umulu?x+plsu?。
2 操作直接改標記,3操作修改 mulumul_umulu? 后把 [0,x)[0,x)[0,x) 乘上逆元,1 操作改完后倒著把存儲的值算出來放進去,就可以做到 O(n)O(n)O(n)。
你以為這就完了?奶義務!
乘上的這個數可能在模意義下為 000,是沒有逆元的,并且不像一年后的某道莫反矩陣樹縫合怪題,這個東西非常好構造,直接連長度分別為 2,2,?,2?23,6,16\begin{matrix} \underbrace{ 2,2,\cdots,2 } \\ 23\end{matrix},6,162,2,?,2?23?,6,16 的鏈就可以了。
所以我為什么沒在 CSP 前看到這個東西
所以我們需要再開兩個標記 limu,valulim_u,val_ulimu?,valu?,表示 [limu,+∞)[lim_u,+\infin)[limu?,+∞) 這一段的存儲的值是 valuval_uvalu?。如果這個數是 000,相當于后綴賦值,把 limulim_ulimu? 賦值為 xxx,valuval_uvalu? 賦值為真實值為 000 時對應的存儲值。
Q:為什么不能定義為"limulim_ulimu? 及之后的數都是 000",還可以少開個標記?
A:因為這里只是暫時為 000,之后的全局加對這里是有影響的。
這樣做到了 O(nlog?P)O(n\log P)O(nlogP)。注意到每次求逆元的都是 f(v,mxv?1)f(v,mx_v-1)f(v,mxv??1) ,即不限制距離的方案數,所以可以先做一個簡單的 DP 算出來,然后 O(n)O(n)O(n) 離線求逆元,注意要跳過為 000 的。維護 mulumul_umulu? 標記的時候順便維護一下它的逆元,就可以 O(n)O(n)O(n) 了。
g(u,L)=g(fau,L?1)∏v∈son(fau),v≠uf(v,L?2)+1g(u,L)=g(fa_u,L-1)\prod_{v\in son(fa_u),v\neq u}f(v,L-2)+1g(u,L)=g(fau?,L?1)v∈son(fau?),v?=u∏?f(v,L?2)+1
大家可能會覺得很奇怪,這個往上走的 DP 怎么能用長鏈剖分優化呢?
注意到我們答案需要的只有 g(u,L)g(u,L)g(u,L),所以對于一個葉子結點,它沒有兒子需要它的其他信息,所以只需要維護 g(u,L)g(u,L)g(u,L) 這一個位置。類似的,對于點 uuu ,我們只需要維護 [L?mxu+1,L][L-mx_u+1,L][L?mxu?+1,L] 中的值。
也就是說我們規定 g(u,…)g(u,\dots)g(u,…) 的定義域只有 [max?(L?mxu+1,0),L][\max(L-mx_u+1,0),L][max(L?mxu?+1,0),L],這樣狀態數就和深度正相關了。
把信息直接繼承給長兒子,短兒子暴力轉移,再乘上一個 f(u,L?1)?1f(v,L?2)\frac{f(u,L-1)-1}{f(v,L-2)}f(v,L?2)f(u,L?1)?1?
然后你又錯了,因為 f(v,L?2)f(v,L-2)f(v,L?2) 可能沒有逆元。所以我們只能算前綴積和后綴積了。
前綴積在遍歷的時候可以順便維護。為了方便實現,可以把每個結點的輕兒子按深度從小到大排序,這樣你只需要記 333 個標記。嚴格意義上需要桶排保證復雜度,不過直接 sort 也能過。之后假裝這個排序是 O(n)O(n)O(n) 的。
然后開一個數組 preprepre ,用 preipre_iprei? 記錄 f(v,i)f(v,i)f(v,i) 的前綴積就可以了,配合后綴賦值標記就可以維護整個前綴積。
對于后綴積是不能跑一遍記下來的,因為開不下……
但我們在計算 fff 的時候做了一遍這東西,怎么能浪費了呢?
我們在計算 fff 的時候倒著做,即按輕兒子深度從大到小排序。對于 dpdpdp 值和 555 個標記的修改,把它修改的過程記錄下來,對就是可撤銷并查集的那個東西。
然后在算 ggg 的時候不斷把修改撤銷,這樣 f(u,L?1)f(u,L-1)f(u,L?1) 維護的就是后綴積。
這樣只能算出輕兒子,重兒子因為撤回不了,所以需要再利用之前你算的前綴積單獨搞一下。
因為還是有乘法和全局加操作,所以你還是得維護一堆標記。并且盡管定義域很有限,為了保證復雜度,你還是得維護后綴賦值標記。注意這個標記和前綴積的標記沒有關系。
需要注意的細節:
復雜度 O(nlog?k)O(n\log k)O(nlogk)
用盡各種毒瘤方法把一個不可做的計數題做到線性,最后卻因為一個 101010 的快速冪無法把復雜度寫成 O(n)O(n)O(n),真是悲壯……
代碼中的迷惑部分都有注釋。
#include <iostream> #include <cstdio> #include <cstring> #include <cctype> #include <vector> #include <utility> #include <list> #include <algorithm> #define MAXN 1000005 using namespace std; inline int read() {int ans=0;char c=getchar();while (!isdigit(c)) c=getchar();while (isdigit(c)) ans=(ans<<3)+(ans<<1)+(c^48),c=getchar();return ans; } const int MOD=998244353; typedef long long ll; inline int add(const int& x,const int& y){return x+y>=MOD? x+y-MOD:x+y;} inline int dec(const int& x,const int& y){return x<y? x-y+MOD:x-y;} inline int qpow(int a,int p) {int ans=1;while (p){if (p&1) ans=(ll)ans*a%MOD;a=(ll)a*a%MOD,p>>=1;}return ans; } vector<int> T[MAXN],e[MAXN];//T 是所有相鄰的點,e 是所有輕兒子 int fa[MAXN],son[MAXN],mx[MAXN],s[MAXN],sinv[MAXN],n,L,k; void dfs(int u,int f) {fa[u]=f,s[u]=1;for (int i=0;i<(int)T[u].size();i++)if (T[u][i]!=f){dfs(T[u][i],u);if (mx[T[u][i]]>mx[son[u]]) son[u]=T[u][i];s[u]=(ll)s[u]*s[T[u][i]]%MOD;}mx[u]=mx[son[u]]+1;s[u]=add(s[u],1); } int fac[MAXN],finv[MAXN]; inline bool cmp(const int& x,const int& y){return mx[x]>mx[y];} inline void init() {fac[0]=1;for (int i=1;i<=n;i++)if (s[i]) fac[i]=(ll)fac[i-1]*s[i]%MOD;else fac[i]=fac[i-1];finv[n]=qpow(fac[n],MOD-2);for (int i=n-1;i>=1;i--)if (s[i+1]) finv[i]=(ll)finv[i+1]*s[i+1]%MOD;//跳過 0,后同else finv[i]=finv[i+1];for (int i=1;i<=n;i++) if (s[i]) sinv[i]=(ll)finv[i]*fac[i-1]%MOD;for (int i=1;i<=n;i++) stable_sort(e[i].begin(),e[i].end(),cmp);//stable 是為了方便調試 } void dfs(int u) {if (son[u]) dfs(son[u]);for (int i=0;i<(int)T[u].size();i++)if (T[u][i]!=fa[u]&&T[u][i]!=son[u])e[u].push_back(T[u][i]),dfs(T[u][i]); } int F1[MAXN],F2[MAXN],G1[MAXN]; struct BackDS {typedef pair<int*,int> pi;list<pi> his;inline void modify(int& x,int v){his.push_back(make_pair(&x,x)),x=v;}inline void undo(){while (!his.empty()) *his.back().first=his.back().second,his.pop_back();} }q[MAXN]; namespace F {int buf[MAXN],*cur=buf;int* dp[MAXN];inline int* newbuf(int x){int* p=cur;cur+=x;return p;}int mul[MAXN],inv[MAXN],pls[MAXN],lim[MAXN],val[MAXN];inline int calc(int u,int i)//計算真實值{if (i<lim[u]) return ((ll)mul[u]*dp[u][i]+pls[u])%MOD;else return ((ll)mul[u]*val[u]+pls[u])%MOD;}inline int clac(int u,int v){return (ll)dec(v,pls[u])*inv[u]%MOD;}//根據真實值的得到應該存儲的值void dfs(int u){if (son[u]){dp[son[u]]=dp[u]+1;dfs(son[u]);mul[u]=mul[son[u]],inv[u]=inv[son[u]],pls[u]=pls[son[u]];lim[u]=lim[son[u]]+1,val[u]=val[son[u]];dp[u][0]=clac(u,1); }else{mul[u]=inv[u]=lim[u]=1,pls[u]=F1[u]=F2[u]=2;return;}int las=0;for (int k=0;k<(int)e[u].size();k++){int v=las=e[u][k];dp[v]=newbuf(mx[v]),dfs(v);for (int i=1;i<=mx[v];i++){if (i==lim[u]) q[v].modify(dp[u][i],val[u]), q[v].modify(lim[u],lim[u]+1);q[v].modify(dp[u][i],clac(u,(ll)calc(u,i)*calc(v,i-1)%MOD));}if (s[v]){q[v].modify(mul[u],(ll)mul[u]*s[v]%MOD);q[v].modify(inv[u],(ll)inv[u]*sinv[v]%MOD);q[v].modify(pls[u],(ll)pls[u]*s[v]%MOD);for (int i=0;i<=mx[v];i++) q[v].modify(dp[u][i],clac(u,(ll)calc(u,i)*sinv[v]%MOD));}else q[v].modify(lim[u],mx[v]+1),q[v].modify(val[u],clac(u,0));}if (las) q[las].modify(pls[u],add(pls[u],1));//把全局加掛在最后一個輕兒子上,這樣一來就會撤回else pls[u]=add(pls[u],1);//沒有輕兒子的話反正都沒有用,隨便加F1[u]=calc(u,L),F2[u]=calc(u,L-1);}inline void solve(){dp[1]=newbuf(mx[1]),dfs(1);} } namespace G {int buf[MAXN],pre[MAXN],*cur=buf;int* dp[MAXN];inline int* newbuf(int x){int* p=cur;cur+=x;return p;}int mul[MAXN],inv[MAXN],pls[MAXN],lim[MAXN],val[MAXN];inline int calc(int u,int i){if (i<lim[u]) return ((ll)mul[u]*dp[u][i]+pls[u])%MOD;return ((ll)mul[u]*val[u]+pls[u])%MOD;}inline int clac(int u,int v){return (ll)dec(v,pls[u])*inv[u]%MOD;}void dfs(int u){G1[u]=calc(u,L);pre[0]=1;int pos=1,cur=1,cinv=1;for (int k=(int)e[u].size()-1;k>=0;k--)//按深度從小到達枚舉{int v=e[u][k];q[v].undo();dp[v]=newbuf(mx[v])-max(0,L-mx[v]+1);mul[v]=inv[v]=1,lim[v]=L+1;for (int i=max(0,L-mx[v]+1);i<=L;i++){int t=1;if (i) t=(ll)t*calc(u,i-1)%MOD;if (i>1){t=(ll)t*F::calc(u,i-1)%MOD;//見細節4if (i-2<pos) t=(ll)t*pre[i-2]%MOD;else t=(ll)t*cur%MOD; }dp[v][i]=clac(v,t);}pls[v]=add(pls[v],1);if (L-mx[v]+1<=0) dp[v][0]=clac(v,1);//是否在定義域內for (int i=0;i<=mx[v];i++){if (i<pos) pre[i]=(ll)pre[i]*F::calc(v,i)%MOD;else pre[i]=(ll)cur*F::calc(v,i)%MOD;}pos=mx[v]+1;cur=(ll)cur*s[v]%MOD,cinv=(ll)cinv*sinv[v]%MOD;}int v=son[u];if (v){mul[v]=mul[u],inv[v]=inv[u],pls[v]=pls[u],lim[v]=lim[u]+1,val[v]=val[u];dp[v]=dp[u]-1;for (int i=max(2,L-mx[v]+1);i<=pos+1;i++){if (i==lim[v]) dp[v][lim[v]++]=val[v];dp[v][i]=clac(v,(ll)calc(v,i)*pre[i-2]%MOD); } if (cur){mul[v]=(ll)mul[v]*cur%MOD;pls[v]=(ll)pls[v]*cur%MOD;inv[v]=(ll)inv[v]*cinv%MOD;for (int i=max(0,L-mx[v]+1);i<=pos+1;i++) dp[v][i]=clac(v,(ll)calc(v,i)*cinv%MOD);}else lim[v]=pos+1,val[v]=clac(v,0);pls[v]=add(pls[v],1);if (L-mx[v]+1<=0) dp[v][0]=clac(v,1);dfs(v);}for (int i=0;i<(int)e[u].size();i++) dfs(e[u][i]);//算完再遞歸,避免 pre 沖突}inline void solve(){dp[1]=newbuf(mx[1])-max(L-mx[1]+1,0),mul[1]=inv[1]=pls[1]=1,lim[1]=L+1,dfs(1);} } int main() {n=read(),L=read(),k=read();if (!L) return printf("%d\n",n),0;for (int i=1;i<n;i++) {int u,v;u=read(),v=read();T[u].push_back(v),T[v].push_back(u);}dfs(1,0),dfs(1);init();F::solve(), G::solve();int ans=0;for (int i=1;i<=n;i++){ans=add(ans,qpow((ll)dec(F1[i],1)*G1[i]%MOD,k));if (i>1) ans=dec(ans,qpow((ll)dec(F2[i],1)*dec(G1[i],1)%MOD,k));}cout<<ans;return 0; }總結
以上是生活随笔為你收集整理的【十二省联考2019】希望【点边容斥】【换根dp】【长链剖分】【线性数据结构】【回退数据结构】【离线逆元】的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 主机电源指示灯亮一下就熄灭主机电源亮了一
- 下一篇: 【十二省联考2019】皮配【分部dp】