很久沒寫博客了
此次準備在 cnblogs 和 個人博客 上同步更新~ios
一個 \(n\) 個點 \(m\) 條邊的 DAG,有若干個特殊點,邊有權值。數據結構
一個單詞定義爲從點 \(1\) 出發到達一個特殊點的路徑上,邊權按訪問順序構成的一個序列。spa
\(Q\) 次詢問,每次給出 \(k\),求全部單詞中字典序從小到大第 \(k\) 個單詞的長度,或回答「不存在」。code
\(2\le n\le10^5\),\(0\le m\le10^5\),\(1\le k\le10^8\);保證 \(1\) 不是特殊點且 \(1\) 沒有入度;保證一個點的全部出邊的權值兩兩不一樣。blog
思路挺讓人震驚的……原來不只樹能夠剖分遞歸
惋惜的是,但凡 \(k\) 的範圍開大到 \(10^9\) 就放不過那種直接跑出 \(10^8\) 的答案的暴力 awaci
首先不難想到DP,先求出 \(f_u\) 表示從 \(u\) 出發可以獲得多少個單詞。轉移式很簡單,pdo
不過考慮到單詞數會很是大,遠遠大於 \(10^8\),會爆 int
,因此當 \(f_u>2\times10^8\) 時就把 \(f_u\) 賦值爲 \(2\times10^8\)(不和 \(10^8\) 取 min,仍是留一點超出的空間)。字符串
而後先想暴力,每次詢問從 \(1\) 出發,按權值從小到大查看出邊,若是構成的單詞數量足夠 \(k\),就從那條出邊遞歸到子問題。複雜度是 \(O(Qn)\) 的。get
具體看一下怎麼遞歸子問題,設點 \(u\) 的出邊按權值從大到小爲 \(w_1,w_2,\dots,w_t\),若是咱們發現:
說明應從邊 \(i\) 轉移,因而遞歸子問題 k-=w[1]+w[2]+...+w[i-1]
。
注意到每次 \(k\) 都會減少一些,因而就有了一個很妙的想法——對於一個點 \(u\),定義其重兒子爲 dp 值最大的一個出點,其餘出點就是輕兒子。
實際上直接這樣定義,理論複雜度會有小問題(可是實測可過),理論複雜度正確的定義應是「dp 值小於 \(2\times10^8\) 的 dp 值最大的出點」
也就是說忽略 dp 值太大的點,如下均假設 \(f_u\le10^8\)。
這樣有什麼性質?設 \(u\) 的重兒子是 \(p\),一個輕兒子是 \(q\),則 \(f_p\ge f_q\)。若是咱們要轉移到 \(q\),轉移後的子問題爲 \(k'\),則 \(k'\le f_q\le \frac{f_p+f_q}2\le\tfrac12f_u\)。
這樣的話,\(k\) 的上界每次減半,只能減 \(O(\log f)\) 次,因而只會通過 \(O(\log f)\) 次輕邊。
二者都是利用的這種「上界減半」的思想。
樹鏈剖分中若是走輕兒子,則子樹大小必定減半,那麼只會走 $O(\log n)$ 次輕兒子,也就是「一條鏈只會被剖成 $O(\log n)$ 條重鏈」這一結論。而走重兒子這種狀況,重兒子構成重鏈,鏈是一種更簡單的結構,所以能夠用一些處理序列的數據結構維護。
而這道題將 DAG 剖成「重鏈」——實際上不是鏈,而是一棵樹,一樣簡化了圖結構。接下來就能夠用一些樹的技巧解題了。
那若是從重兒子轉移呢?這一就不必定知足「上界減半」。可是不難發現重兒子構成了內向樹森林 的結構,能不能用什麼樹的技巧來加速走重兒子的過程?
考慮倍增,維護 \(u\) 的第 \(2^i\) 級祖先 jump[u][i]
,以及從 \(\mathbf u\) 轉移到該祖先之間有多少個字符串 jumpcst[u][i]
。畫個圖可能會好理解一些:
(\(tag_u=1\) 表示 \(u\) 是特殊點)好比從 \(u\) 到 \(a\),\(a\) 左邊的全部兒子表明的單詞都會被跳過,並且若是 \(u\) 自己是特殊點,在 \(u\) 結尾的單詞也會被跳過。
那麼能夠從 \(u\) 直接倍增到 jump[u][i]
的條件就是
別忘了還有上界。
顯然能夠倍增,複雜度 \(O(\log n)\)。而走輕兒子就能夠直接二分,預處理每一個點的出邊權值從小到大的 dp 值前綴和便可。
總的複雜度,每次查詢會走 \(O(\log f)\) 次輕兒子,每次 \(O(\log n)\) 二分;會走 \(O(\log f)\) 次重鏈,每次 \(O(\log n)\) 倍增,總的複雜度加上預處理爲 \(O(Q\log n\log f+n\log n)\)。
/*Lucky_Glass*/ #include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm> using namespace std; inline int Rint(int &r){ int b=1,c=getchar();r=0; while(c<'0' || '9'<c) b=c=='-'? -1:b,c=getchar(); while('0'<=c && c<='9') r=(r<<1)+(r<<3)+(c^'0'),c=getchar(); return r*=b; } const int N=1e5+10; #define ci const int & typedef pair<int,int> pii; int n,m,Q,cas; int sptag[N],dp[N],son[N],dpcst[N],jump[N][20],jumpcst[N][20]; bool dpdone[N]; vector<pii> lnk[N]; vector<int> key[N]; //dpcst[u]: 從u到son[u]須要跳過多少字符串 //key[u][i]: 從u到lnk[u][i]須要跳過多少字符串 int DP(ci u){ if(dpdone[u]) return dp[u]; dpdone[u]=true,dpcst[u]=dp[u]=sptag[u]; for(int it=0,iit=lnk[u].size();it<iit;it++){ int v=lnk[u][it].second; DP(v); //實際上不加 dp[u]<=1e8 也能過…… if(dp[son[u]]<dp[v] && dp[u]<=1e8) son[u]=v,dpcst[u]=dp[u]; dp[u]+=dp[v]; if(dp[u]>2e8) dp[u]=2e8; key[u].push_back(dp[u]); } jump[u][0]=son[u],jumpcst[u][0]=dpcst[u]; for(int i=1;i<20;i++){ jump[u][i]=jump[jump[u][i-1]][i-1]; jumpcst[u][i]=jumpcst[u][i-1]+jumpcst[jump[u][i-1]][i-1]; if(jumpcst[u][i]>2e8) jumpcst[u][i]=2e8; } return dp[u]; } int Jump(int rnk){ int u=1,ret=0; while(true){ if(rnk>dp[u]) exit(0); for(int i=19;~i;i--) if(jump[u][i] && rnk>jumpcst[u][i]){ int v=jump[u][i]; if(rnk>jumpcst[u][i]+dp[v]) continue; rnk-=jumpcst[u][i]; u=v,ret+=(1<<i); } if(rnk==1 && sptag[u]) return ret; int tmp=int(lower_bound(key[u].begin(),key[u].end(),rnk)-key[u].begin()); if(tmp) rnk-=key[u][tmp-1]; else rnk-=sptag[u]; u=lnk[u][tmp].second,ret++; } } void Solve(){ Rint(n),Rint(m),Rint(Q); for(int i=1;i<=n;i++){ if(i>1) Rint(sptag[i]); dpdone[i]=false; lnk[i].clear(),key[i].clear(); son[i]=dpcst[i]=0; for(int j=0;j<20;j++) jump[i][j]=jumpcst[i][j]=0; } for(int i=1,u,v,varc;i<=m;i++){ Rint(u),Rint(v),Rint(varc); lnk[u].push_back(make_pair(varc,v)); } for(int i=1;i<=n;i++) sort(lnk[i].begin(),lnk[i].end()); DP(1); while(Q--){ int rnk;Rint(rnk); if(rnk>dp[1]) printf("-1\n"); else printf("%d\n",Jump(rnk)); } } int main(){ // freopen("input.in","r",stdin); Rint(cas); for(int i=1;i<=cas;i++){ printf("Case #%d:\n",i); Solve(); } return 0; }
> Linked 今日重到蘇瀾橋-網易雲