關於點分治,其實思想是非常好理解的,模擬在數列上或是在平面上的分治演算法(如歸併排序,平面最近點對等),我們可以從字面上理解該演算法:
以乙個點為界限,將一棵樹分成若干個子樹,當劃分到一定規模,就對每個子樹分別進行求解感性理解就好了
感受乙個演算法最直觀的辦法,就是來看一道模板題。
給定一棵有\(n\)個點的樹,詢問樹上長度為\(k\)的鏈是否存在。
首先可以很直觀的知道,對於樹上的任意乙個點,有很多條經過它的鏈
那麼,對於本題,我們是否可以在能夠接受的時間內對這些經過該點的鏈進行求解呢?
答案是肯定的,只需要以該節點為根節點,對整顆樹進行一遍\(\text\),求出各個點到該點的距離,然後就可以用桶排等方法解決該問題。
那麼對於剩下的沒有被處理到的鏈呢?
自然,我們可以以這個點,將整棵樹斷掉,將它的子樹分開遞迴分治求解,這樣這道題目就解決啦!
咳咳,真的這麼簡單嗎?
我們來看一張圖
多麼優雅的一條鏈!
如果我們一開始以\(1\)為根節點,按照這個思路,我們需要進行\(n\)次操作,這樣肯定是不行的。
也就是說,我們需要找到乙個節點,使得在將其斷掉之後,剩下的各個子樹的大小相對均勻,這樣在進行分治求解的時候就可以讓時間複雜度最優。
所以這裡需要引入乙個新的概念:
那麼如何求樹的重心呢?
我們可以採取一種類似於\(dp\)的演算法,因為我們要使最大的子樹節點數最少,於是我們可以任選乙個點進行\(dfs\),在搜尋過程中,記錄每乙個點的最大的子樹大小,然後進行操作,即
\[dp[u]=max(siz[son[u]],sum-siz[u])
\]\(sum\)表示這顆子樹一共有多少個節點,\(siz[i]\)即子樹大小
這樣的話,我們就只需要在該子樹中找到最小的\(dp[i]\),這樣\(i\)就是我們要找的重心了。
是不是很簡單?
貼一小段**
//root預設為0,dp[0]=inf
void get_root(int u,int fa,int sum)
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]那麼,如何統計答案呢?
對於本題,提供\(3\)種方法供君選擇
說明:\(dis[u]\)表示\(u\)節點到重心的距離,\(siz[u]\)表示以\(u\)為根的子樹大小,\(root\)表示當前子樹重心
\(1.\)暴力列舉法
我們將所有的節點到重心的距離\(dis[u]\)通過一遍\(dfs\)記錄下來,然後開乙個桶,兩兩組合,統計答案。這樣的話,會有乙個問題,就是在同一條路徑上的節點的答案也會被統計,比如\(dis[u]+dis[son[u]]=k\),但是這兩個節點並沒有到重心的一條鏈,所以需要刪去。
那麼如何做呢?
簡單容斥一下就好了
\[ans=ans(以重心為根的子樹)-\sum ans(以重心的孩子為根的子樹)
\]時間複雜度為單次\(o(siz[root]^2)\),且有一定侷限性——\(k\)太大時無法使用
$ 2.$配對法
這是乙個在本題跑得飛起的計算方法
假設一共有\(son_1,son_2,son_3,...,son_n\)這些多棵子樹
令\(vis[j]\)陣列表示在求解到第\(i\)棵子樹的答案時,前\(i-1\)棵子樹是否存在到重心長度為\(j\)的路徑
這樣一來,我們就只需要在每棵子樹當中對於每乙個詢問,列舉找到可以湊成答案的路徑即可
時間複雜度為單次\(o(m*siz[root])\),由於詢問較少,跑的飛起
但注意,在還原陣列的時候,需要將
同樣,也有一定的侷限性——\(k\)太大時同樣無法使用
\(3.\)two pointers
維護\(l,r\)兩個指標,將所有得到的\(dis[i]\)從小到大排序,這樣的話,就可以保證\(dis\)陣列單調遞增,有兩個思路供君選擇:
\(1)\)直接標記(僅針對本題)
在\(dfs\)求解\(dis[i]\)時,可以記錄每乙個節點對應來自哪一棵子樹,記為\(tag[i]\)然後可以按照這樣的思路:
令\(l=0,r=siz[root]\)
如果當前點已有答案,跳過
如果\(dis[l]+dis[r]>k\),就\(--r\),這樣才有可能有解
如果\(dis[l]+dis[r],就\(++l\),同理
如果\(dis[l]+dis[r]=k \quad且\quad tag[l]==tag[r]\),就看\(dis[r-1]\)的大小,並進行相應調整
如果上述條件都不滿足,則對於這個\(k\)有解
\(2)\)字首統計
我們可以化等為不等,記錄\(\le k\)和\(\le k-1\)的路徑條數
同樣令\(l=0,r=siz[root]\)
若\(dis[l]+dis[r]<=k\),則說明在\([l+1,r]\)的\(dis\)都可以組成答案,此時\(++l\);
否則\(--r\);
這種方法同樣需要容斥。
兩種方法的時間複雜度均為單次\(o(m*siz[root])\),且不受\(k\)的限制,同時這種思想也在非常多的題目上有所運用,如\(noi2019day1t3\)
大體思路就是這樣,共\(3\)步:
\(1.\)找樹的重心
\(2.\)求解經過重心的鏈對答案的貢獻
\(3.\)在各個子樹內求解
於是這個題目就完結辣owo~
貼**(上面講的很清楚了於是沒有注釋qwq)
#includeusing namespace std;
const int maxn=1e5+10;
int n,m;
struct cce[maxn<<2];
int head[maxn],cnt;
int siz[maxn],dp[maxn],vis[maxn],q[maxn],ans[maxn];
void add(int x,int y,int z)
int root=0;
void get_root(int u,int fa,int sum)
dp[u]=max(dp[u],sum-siz[u]);
if(dp[u]
}int dep[maxn],dis[maxn],tot;
void get_dis(int u,int fa)
}void get_ans(int u,int now,int val)
}void solve(int u)
}int main()
點分治學習筆記
點分治主要用來處理樹上路徑問題,可以統計樹上點到點的所有路徑,複雜度o nlogn 基於樹上的結點進行分治,不斷將一棵樹拆成多顆子樹處理 選擇點時為了防止退化成鏈的情況,如果選點後左右子樹越大,遞迴層數越多,時間越慢,反之則越快,我們每次選擇子樹內的重心 void getroot int u,int...
點分治學習筆記
point divide and rule 澱粉質就是在樹上,依靠不停的遞迴和分治,解決相同的子問題 先來看看模板題 tree 就是找樹上 k 的路徑有多少 我們可以分兩種情況討論 1.經過根節點 p 的路徑 2.不經過根節點 p 的路徑 第二種情況可以通過遞迴來處理,我們直接來討論第一種情況 設當...
點分治學習筆記
銀月城傳送門 集訓day9上午各種神仙分治真心聽不懂,自己頹著把點分治學了,簡單 並不 寫一下再加深一下理解吧。一 點分治是個啥 點分治是通常用來處理樹上路徑統計問題的一種分治方法。尤其適用於大規模的資料處理。時間複雜度最高為o nlogn 例如求樹上兩點間距離,點分治就遠優於dfs求兩點距離 o ...