點分治
關於點分治,其實思想是很是好理解的,類比在數列上或是在平面上的分治算法(如歸併排序,平面最近點對等),咱們能夠從字面上理解該算法:c++
以一個點爲界限,將一棵樹分紅若干個子樹,當劃分到必定規模,就對每一個子樹分別進行求解算法
感性理解就行了數組
感覺一個算法最直觀的辦法,就是來看一道模板題。函數
【模板】 點分治
給定一棵有$n$個點的樹,詢問樹上長度爲$k$的鏈是否存在。post
首先能夠很直觀的知道,對於樹上的任意一個點,有不少條通過它的鏈url
那麼,對於本題,咱們是否能夠在可以接受的時間內對這些通過該點的鏈進行求解呢?spa
答案是確定的,只須要以該節點爲根節點,對整顆樹進行一遍$\text{DFS}$,求出各個點到該點的距離,而後就能夠用桶排等方法解決該問題。.net
那麼對於剩下的沒有被處理到的鏈呢?指針
天然,咱們能夠以這個點,將整棵樹斷掉,將它的子樹分開遞歸分治求解,這樣這道題目就解決啦!code
咳咳,真的這麼簡單嗎?
咱們來看一張圖
多麼優雅的一條鏈!
若是咱們一開始以$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]=0,siz[u]=1;//初始化 for(int i=head[u];i;i=e[i].nex) { int v=e[i].to; if(v==fa||vis[u]) continue;//vis[u]表示該節點是否被看成根節點操做過,同時保證該函數只在本子樹內操做 get_root(v,u,sum); siz[u]+=siz[v]; dp[u]=max(dp[u],siz[v]); } dp[u]=max(dp[u],sum-siz[u]); if(dp[u]<dp[root]) root=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]<k$,就$++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)
#include<bits/stdc++.h> using namespace std; const int maxn=1e5+10; int n,m; struct cc{ int to,nex,w; }e[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) { ++cnt; e[cnt].to=y; e[cnt].nex=head[x]; e[cnt].w=z; head[x]=cnt; } int root=0; void get_root(int u,int fa,int sum) { dp[u]=0,siz[u]=1; for(int i=head[u];i;i=e[i].nex) { int v=e[i].to; if(v==fa||vis[v]) continue; get_root(v,u,sum); siz[u]+=siz[v]; dp[u]=max(dp[u],siz[v]); } dp[u]=max(dp[u],sum-siz[u]); if(dp[u]<dp[root]) root=u; } int dep[maxn],dis[maxn],tot; void get_dis(int u,int fa) { dep[++tot]=dis[u]; for(int i=head[u];i;i=e[i].nex) { int v=e[i].to; if(v==fa||vis[v]) continue; dis[v]=dis[u]+e[i].w,get_dis(v,u); } } void get_ans(int u,int now,int val) { dis[u]=now,tot=0; get_dis(u,0); stable_sort(dep+1,dep+tot+1); for(int i=1;i<=m;++i) { int s1=0,s2=0,l=1,r=tot; while(l<r) if(dep[l]+dep[r]<=q[i]) s1+=r-l,++l; else --r; l=1,r=tot; while(l<r) if(dep[l]+dep[r]<q[i]) s2+=r-l,++l; else --r; ans[i]+=(s1-s2)*val; } } void solve(int u) { get_ans(u,0,1); vis[u]=1; for(int i=head[u];i;i=e[i].nex) { int v=e[i].to; if(vis[v]) continue; get_ans(v,e[i].w,-1); root=0; get_root(v,u,siz[v]),solve(root); } } int main() { int a,b,c; scanf("%d%d",&n,&m); dp[0]=n; for(int i=1;i<n;++i) scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c); for(int i=1;i<=m;++i) scanf("%d",&q[i]); get_root(1,0,n);solve(root); for(int i=1;i<=m;++i) printf("%s\n",ans[i]?"AYE":"NAY"); return 0; }