一次模擬賽的\(T3\):傳送門html
只會\(O(n^2)\)的我就\(gg\)了,而且對於題解提供的\(\text{dsu on tree}\)的作法一臉懵逼。node
看網上的其餘大佬寫的筆記,我本身畫圖看了一天才看懂(我太蒻了),因而就有了這篇學習筆記。git
如今考慮這樣一類樹上統計問題:github
無修改操做,詢問容許離線算法
對子樹信息進行統計(鏈上的信息在某些條件下也能夠統計)數組
樹上莫隊?點分治?數據結構
\(\text{dsu on tree}\)能夠把它們吊起來打!學習
\(\text{dsu on tree}\)運用樹剖中的輕重鏈剖分,將輕邊子樹信息累加到重鏈上進行統計,擁有\(O(nlogn)\)的優秀複雜度,常數還賊TM小,你值得擁有!優化
//雖然說是dsu on tree,但某個毒瘤@noip說這是靜態鏈分治 //還有其餘的數據結構神du仙liu說它能夠被當作是靜態的樹剖(由於其在樹上有強大的統計信息的能力,但不能支持修改操做),與正常的樹鏈剖分相對 //因此我同時保留這幾種說法,但願數據結構神du仙liu們不要噴我這個juruo
遍歷全部輕兒子,遞歸結束時消除它們的貢獻ui
遍歷全部重兒子,保留它的貢獻
再計算當前子樹中全部輕子樹的貢獻
更新答案
若是當前點是輕兒子,消除當前子樹的貢獻
那麼這裏有人可能就要問了,爲何不保留求出的全部答案呢?這樣複雜度就更優了啊
若是這樣的話,當你處理完一顆子樹的信息時,再遞歸去求解另外一顆子樹時,
已有的答案就會與當前子樹信息相混淆,就會產生錯誤答案。
因此,從這咱們看出,一個節點只能選擇一個子節點來保留答案
其它的都要去暴力求解
那麼選擇哪個節點能使複雜度最優呢?
顯然,咱們要儘可能均衡答案被保留的子樹和不被保存的子樹的大小
這是否是就很像樹鏈剖分劃分輕重兒子了呢?
由於窩太蒻了一開始沒怎麼理解它,因此有了圖解這個環節23333。
同理,\(2\)號點也可進行相似操做,由於它的重兒子\(6\)號節點已保存了這顆子樹的答案,只需上傳便可,
不用再從\(6\)這個位置再往下走統計答案,惟一會暴力統計答案的只有它的輕兒子\(5\)號節點
inline void calc(int x,int fa,int val) { ...................... /* 針對不一樣的問題 採起各類操做 */ for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i]; if(vis[v] || v==fa) continue; calc(v,x,val); } } inline void dfs(int x,int fa,int keep)//keep表示當前是否爲重兒子 { for(int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; dfs(v,x,0); } if(son[x]) dfs(son[x],x,1),vis[son[x]]=true;//標記重兒子 calc(x,fa,1);vis[son[x]]=false;//計算貢獻 ans[x]=....;//記錄答案 if(!keep) calc(x,fa,-1);//不是重兒子,撤銷其影響 }
若是是維護路徑上的信息,大概還能夠這麼寫:(若是有錯,請大佬指出)
ps:關於\(\text{dsu on tree}\)對路徑上信息進行維護的精彩應用,能夠看最後\(3\)道例題
inline void dfs(int x,int fa) { siz[x]=1,dep[x]=dep[fa]+1,nid[rev[x]=++idx]=x; //再次藉助樹剖的思想,子樹內節點順序轉爲線性 for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v,w=G[x][i].w; if(v==fa) continue; dfs(v,x),siz[x]+=siz[v]; if(!son[x] || siz[v]>siz[son[x]]) son[x]=v; } } inline void calc(int x,int val) {//對x這一節點進行單獨處理 if(val>0) //計算貢獻 else //撤銷影響 } inline void dfs2(int x,int fa,int keep) { for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; dfs2(v,x,0); } if(son[x]) dfs2(son[x],x,1); for(rg int i=0;i<(int)G[x].size();++i) { int v=G[x][i].v; if(v==fa || v==son[x]) continue; for(rg int j=0;j<siz[v];++j) { int vv=nid[rev[v]+j]; .......... //更新答案 } for(rg int j=0;j<siz[v];++j) calc(nid[rev[v]+j],1); } calc(x,1); ..........//更新答案 if(!keep) for(rg int i=0;i<siz[x];++i) calc(nid[rev[x]+i],-1); }
不感興趣的大佬能夠跳過這一段。(蒟蒻本身亂\(yy\)的證實,若是有錯請大佬指出)
顯然,根據上面的圖解,一個點只有在它到根節點的路徑上遇到一條輕邊的時候,本身的信息纔會被祖先節點暴力統計一遍
而根據樹剖相關理論,每一個點到根的路徑上有\(logn\)條輕邊和\(logn\)條重鏈
即一個點的信息只會上傳\(logn\)次
若是一個點的信息修改是\(O(1)\)的,那麼總複雜度就是\(O(nlogn)\)
此題來自洛咕日報第\(65\)篇做者\(\text{codesonic}\)。
咱們能夠維護一個全局數組\(cnt\),表明正在被計算的子樹的每種顏色的數量
每次計算子樹貢獻的時候,把節點信息往裏面加就好了,若是一個顏色第一次出現,則顏色種類數\(top++\)
對於須要撤銷影響的子樹,把信息從裏面丟出來便可,若是被刪除的顏色只有這一個,則顏色種類數\(top--\)
公認的\(\text{dsu on tree}\)模板題,相比於上題只是增長了對每種數量的顏色和的統計。
咱們能夠維護\(cnt\)數組,表示某個顏色出現的次數;再維護一個\(sum\)數組,表示當前子樹出現了\(x\)次的顏色的編號和
對節點信息統計時,先把它在\(sum\)數組裏的貢獻刪掉,更新了\(cnt\)數組後再添回去
而後別忘了開\(long \, long\)(血的教訓)
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
窩太菜了,不會二進制優化,只會\(O(26*nlogn)\)
首先,由於要造成迴文串、又能夠對字符進行任意排列,因此最多隻能有一種字母的出現次數爲奇數
而後咱們維護一個\(cnt\)數組,統計每一個深度全部字母的出現次數:
cnt[dep[x]][s[x]-'a']+=val;
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
首先用\(map\)把給的全部名字哈希成\(1\)到\(n\)的數字
題目就能夠轉化爲求出每一個深度有多少不一樣的數
一樣,對每一個深度開個\(set\)去重並統計
而後就是套板子的事情了
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
顯然原問題能夠轉化爲求該點的\(k\)級祖先有多少個\(k\)級兒子(若是沒有\(k\)級祖先,答案就是0)
而一個點\(x\)的\(k\)級兒子即爲在以\(x\)爲根節點的子樹中有多少點的\(dep\)爲\(dep[x] + k\)
把全部詢問讀進來,求出相關的點的\(k\)級祖先(能夠離線\(O(n)\)處理,也能夠倍增\(O(nlogn)\)搞;若是時空限制比較緊,就採起前者吧)
而後由於是統計節點數,因此開一個普通的\(cnt\)數組維護便可。最後答案別忘了\(-1\),由於算了本身
扔一個增強版的(\(N \le 10^6\),\(128MB,1s\)):\(\color{#66ccff}{\texttt{-> 傳送門 <-}}\)
友情提醒:上面這道良心題不只卡空間,還卡時間(若是你用dsu on tree)
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
點分治的題怎麼能用點分治呢?並且這仍是dsu on tree學習筆記
首先,這道題是對鏈的信息進行統計,就不能再像對子樹的統計方法去搞♂了,因此須要一些奇技淫巧
思路與點分治同樣,對於每一個節點\(x\),統計通過\(x\)的路徑的信息
注意到這道題鏈上的信息是可加減的,因此咱們能夠不保存\(x\)的子孫\(\rightarrow x\)的信息,而是保存每一個節點到根節點的信息,在統計的時候在減去\(x \rightarrow\)根節點的信息
而後咱們考慮如何統計,咱們能夠在每一個節點維護一個桶\(cnt\),記錄從這個點\(x\)往下走的全部路徑中,能造成的每種路徑權值和以及其所須要的最少的邊的數量:
對於\(v_{ij}\),計算出其到\(x\)的距離\(dis\)及深度差\(d\)(能夠當作路徑上的節點數),並用\(d\) \(+\) \(cnt[\)k−dis\(]\)來更新答案。
而後用剛纔獲得的\(dis\)對應的\(d\)來更新\(cnt[dis]\)的值。
這樣就至關於,用每一個\(v_{ij}\)到\(x\)的鏈,與以前桶中所保存某條鏈的路徑權值和之和恰爲\(k\)的拼成一條路徑,並更新答案。而後,再把它也加入桶中
再套上\(\text{dsu on tree}\)的板子,每一個節點保存它的重兒子的 桶的信息便可
雖然是\(O(nlog^2n)\)的,但常數小,咱不慌
可是窩太菜了,用\(map\)做桶不開\(O2\)會\(T \, 3\)個點(畢竟用了\(STL\),還有兩隻\(log\)),有空再重寫一遍233
貌似用\(unodered_{}map\)不開\(O2\)也卡得過去。。
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
考慮在第一段路徑上一點\(u\)能觀測到該玩家的條件是:\(dep[S] - dep[u] = w[u]\)
同理,在第二段路徑上一點\(u\)能觀測到該玩家的條件是:\(dep[T] - dep[u] = dis(S,T) - w[u]\),即\(dep[S] - 2 \times dep[lca(S,T)] = w[u] - dep[u]\)
而後能夠用差分的思想,對每一個節點開兩個桶\(up\)、\(down\)進行統計
在\(S\)的\(up\)中插入\(dep[S]\)
在\(T\)的\(down\)中插入\(dep[S] - 2 \times dep[lca(S,T)]\)
由於\(lca(S,T)\)會對\(S \rightarrow T\)和\(T \rightarrow S\)都進行統計,因此在其\(up\)中刪除\(dep[S]\)
同理,在\(fa[lca(S,T)]\)的\(down\)中刪除\(dep[S] - 2 \times dep[lca(S,T)]\)
而後用\(\text{dsu on tree}\)統計便可,答案爲\(up[w[u]+dep[u]] + down[w[u] - dep[u]]\)
注意到\(w[u] - dep[u]\)可能小於零,爲了不負數下標、又不想套\(map\),咱們可使用以下\(trick\)
int up[N],CNT[N<<1],*down=&CNT[N]; //把donw[0]指向CNT[N],這樣就能夠給負數和正數都分配大小爲N的空間
跑的雖然沒有普通的差分快,不過吊打線段樹合併仍是綽綽有餘的
\[\color{orange}{\texttt{-> 原題傳送門 <-}}\]
跟每天愛跑步差很少,就不畫圖了(~懶)
同上題,用差分的思想,對每一個節點的增長和刪除開兩個桶統計
同時,這題要維護每一個點出現的最多物品的種類,直接開個線段樹維護就行了
\(O(nlog^2n)\),常數應該和樹剖差很少,不過由於每一個點都要進行增長刪除兩個操做,常數大了一倍,並且還用了線段樹,因此\(\cdots\)
不過依然比部分線段樹合併跑的快2333
由以上三題,咱們能夠看出,在必定條件下,\(\text{dsu on tree}\)也是能夠在鏈上搞♂事情的
好比\(Race\)知足鏈上信息可加減性,後兩道題能夠用差分將鏈上的修改/詢問轉化爲點上的修改/詢問
但\(\text{dsu on tree}\)能夠應用的條件確定不止以上兩種,由於窩太蒻了,只見識了這些題,之後看到其餘類型的也會補上來
\[\color{orange}{\texttt{-> 提交地址 <-}}\]
如今終於能夠回過頭來解決這個題了
留給你們思考吧,要代碼的話能夠私信我
雖然有不少大佬會線段樹合併或虛樹上\(dp\)秒切這道題,不過仍是但願用\(dsu \; AC\)
之後還會不按期地添加\(\text{dsu on tree}\)的相關題目~
若是有須要,我會把最後那道題的代碼貼出來