動態dp
這個鬼玩意我已經點開-關上無數次了,當時dkw給洛谷貢模板題的時候還問過我一次來着......然而我並不會,然后,,,,然后NOIP就爆炸了。
所以,趁着難得滾到機房的時間,趕快學習一下QwQ。
直接搬洛谷上的模板題吧
題面
給定一棵\(n\)個節點的樹,點有點權,有\(m\)次修改單點點權的操作,回答每次操作之后的最大獨立集大小。
solution
首先有一個\(O(nm)\)的傻逼做法,顯然對於每次修改之后我們只需要\(O(n)\)的做一遍樹\(dp\)就可以了。當然了,既然叫做動態dp,那么自然和dp逃不開關系了,所以我們還是簡單的考慮一下這個轉移。
設\(f_{i,0}\)表示\(i\)這個點不選時,子樹中的最大獨立集;\(f_{i,1}\)表示\(i\)這個點選時,子樹中的最大獨立集。那么轉移很顯然:\(f_{i,0}=\sum max(f_{v,0},f_{v,1})\),\(f_{i,1}=V_i+\sum f_{v,0}\)。其中\(v\)是\(i\)的兒子,\(V_i\)是\(i\)的點權。首先這個\(dp\)有一個弱智的加速方法,就是對於每一次修改的時候只修改這個點到根這一條鏈上的所有點就好了,然而隨便把復雜度卡滿。
類似\(Floyd\)的轉移,我們這里的轉移是\(max\)和\(+\)的形式,既然\(Floyd\)的\(min\)和\(+\)可以矩乘,那么這里也可以矩乘。(你把所有數取個相反數不就從\(max\)變成\(min\)了嗎?)。說白點,就是原本矩陣轉移是\(C_{i,j}=\sum_k A_{i,k}*B_{k,j}\),將其變成\(C_{i,j}=max_k(A_{i,k}+B_{k,j})\)就好了。
那么,在這道題目中,我們顯然是一個一個子樹的貢獻逐個加入進當前節點的。我們先從比較容易的地方入手,先考慮一條鏈應該怎么做。顯然一條鏈的轉移很簡單,即\(f_{i,0}=max(f_{v,0},f_{v,1}),f_{i,1}=V_i+f_{v,0}\)。那么把轉移寫成矩陣就是:
我知道這樣子是假的矩乘啊,你把小點的矩陣自己補成\(2*2\)的就好了
那么,對於一條鏈的時候,我們只需要用線段樹維護中間的那個\(2*2\)的矩陣就可以很容易的解決問題了。(如果這樣子的話似乎\(NOIP\)的\(D2T3\)就可以拿到\(70+\)的分數了,突然覺得我自己菜爆了)
回到樹上,考慮如何解決樹上的問題。想清楚樹和鏈的區別在哪里,顯然,在於一個只有一個兒子,另外一個有多個兒子。考慮多個兒子如何合並答案,我們假裝前面若干子樹合並一起的結果是\(f'_{i,0},f'_{i,1}\),並且\(f_{i,1}\)中已經考慮完自身權值的貢獻了。現在要合並進來的兒子是\(v\)。那么我們考慮一下當前的轉移是什么:\(f_{i,0}=f'_{i,0}+max(f_{v,0},f_{v,1}),f_{i,1}=f'_{i,1}+f_{v,0}\)。
然而,我們發現我們似乎沒法讓\(f'_i\)構成的矩陣變成\(f_i\)的矩陣。
然而為啥我們要吊死在一棵樹上啊(當然是在周圍再找一棵樹吊上去啊),我們把\(f_v\)的值寫成方程就好了啊。
很完美啊,雖然這玩意目前還是一個需要暴力修改到根節點的東西。
恩,暴力修改到根節點,我們怎么樣才能讓一個點到根節點的跳躍次數復雜度很對呢?——樹鏈剖分啊。
似乎有點想法了,我們發現一個點的\(dp\)可以用它的所有兒子轉移過來。而在進行修改操作的時候,我們只考慮等於重鏈條數的修改操作。想想這里要怎么實現:我們只修改重鏈條數次,意味着我們會在重鏈頂端修改重鏈頂端對於其父親的貢獻,但是修改當前位置的時候我們並沒有順勢修改其重鏈上的父親的值,那么我們顯然就無法知道重鏈端點的值了。看起來似乎是這樣一回事,仔細想想,重鏈是啥?是一條鏈。那么其重兒子的貢獻可以單獨用上面鏈的轉移貢獻進來,即用線段樹維護每個節點的矩陣,這樣子重兒子的值是可以很容易得到的。那么考慮當前重鏈端點對於上面父親的貢獻的時候就可以直接修改了。
總結一下的話,大致的過程是這樣子的:首先我們考慮我們的轉移方程,發現能夠將其改寫為矩乘的形式,那么我們首先將轉移改為矩乘。我們把輕鏈和重鏈的轉移分開考慮。這樣子想,我們的重鏈被我們單獨拎了出來,每個重鏈上都掛上了若干輕兒子,顯然輕兒子對於重鏈上的獨立集的選擇是沒有影響的,換而言之,如果輕兒子的貢獻考慮完之后,那么等價於鏈上每個點選或者不選有一個權值,求最大獨立集。而對於鏈上的這個東西,是可以直接用線段數維護矩陣支持修改和查詢的,那么這題就只需要這么做就好了。即只修改這個點到達根節點上的所有輕鏈對於父親的貢獻,對於重兒子的貢獻先暫時不考慮,每次線段樹查詢矩陣乘積即可求解出每個點的矩陣,就可以快速求解答案了。