點分治一般是用於解決樹上路徑問題。
樹的重心:把重心這個點割掉後,使所形成的最大的聯通塊大小最小的點。
可以證明重心子樹的大小最大不會超過 \(n\over 2\)
重心可以通過 \(dfs\) 一遍求出。
//maxsiz[x] 表示割掉點x後所形成的的最大的聯通塊的大小
void dfs(int x,int fa)
max_siz[x] = max(max_siz[x],n-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}
先來看到例題吧
給定一棵樹和乙個整數 \(k\),求樹上邊數等於 \(k\) 的路徑有多少條我們的暴力做法就是列舉每條兩個點,然後在判斷他們兩個的距離是否為 \(k\)
大概是 o(\(n^3\)) 的複雜度,優化一下的話可以跑到 o(\(n^2 log n\))
這 \(n\) 的範圍一大,還是會 \(tle\), 考慮優化一下複雜度 。
我們現在主要想解決的是在以 \(s\) 為根的子樹中符合條件的路徑個數。
不難發現路徑一共可以分為三類:
情況一
從 \(s\) 出發到他的子樹中乙個點 \(t\) 所形成的路徑, 如圖中的黃色路徑:。
這個很好統計,可以直接由 \(s\)
\(dfs\) 一遍即可, 複雜度 \(o(m)\) 。( \(m\) 為路徑個數)
情況二:
不在 $s $ 的同乙個子樹中的兩個點 \(u,v\) 所形成的路徑,如圖。
顯然 \(u-v\) 的路徑是肯定要經過點 \(s\) 的,那麼 \(u-v\) 的路徑也就可以拆成 \(u-s\) 和 \(s-v\) 的兩條路徑。
這兩條路徑我們在解決情況 \(1\) 的時候已經求出來了,剩下的就是考慮怎麼把他們拼接起來。
設 \(d_u\) 表示 \(u\) 到 \(s\) 的距離,我們現在要解決的是 \(d_u + d_v = k\) 且 \(u\) 和 \(v\) 不在 \(s\) 的同一顆子樹中的情況。
我們可以把 \(d_u\) 按從小到大排一下序,根據單調性,利用雙指標就可以很好的解決這個問題。
但我們還要注意的是,需要排除兩條在同一顆子樹中的路徑的干擾。
具體的做法就是對每一條路徑記錄他位於 \(s\) 的那一顆子樹中。
這個可以和 \(d_u\) 一起在情況 \(1\) 的 \(dfs\) 中一併求出來。
假設路徑條數為 \(m\) ,那麼排序的複雜度為 \(o(mlogm)\), 雙指標的複雜度為 \(o(m)\), 所以總的複雜度為 \(o(mlogm)\)
那麼第二種情況我們就解決出來了。
情況三:
位於 \(s\) 的同一顆子樹中的 \(u,v\) 兩點形成的路徑,如圖:
這個顯然是當前問題的子問題,遞迴繼續求解即可。
三種情況我們已經考慮完了,現在分析一下時間複雜度的問題。
假設遞迴的深度為 \(k\), 每做一次的複雜度最壞為 \(o(nlogn)\) (主要來自於情況2的排序)。
那麼總的時間複雜度就是 \(o(knlogn)\), 所以為了保證複雜度,我們要使遞迴深度盡可能的小。
根據我們上面提到的關於重心的知識,每次分治的時候選取子樹的重心,這樣可以保證遞迴深度為 \(logn\)。
所以總的複雜度最壞為 \(o(nlog^2n)\) (實際上是很難達到這個上界的)
在做點分治的時候,我們需要把兩個子樹的資訊合併,我們暴力合併的複雜度過高,有的情況下會使用啟發式合併的方法來合併。
一般點分治的題套路都是一樣的,都是分治遞迴求解,唯一的不同點就在於怎麼合併子樹的資訊。
解決了合併子樹資訊這個問題,剩下的就是套模板的事情了。
例題**:
#include#include#includeusing namespace std;
const int n = 1e4+10;
int n,m,tot,cnt,sum_siz,root,u,v,w;
int head[n],siz[n],max_siz[n],dis[n],k[n];
bool vis[n],ans[n];
struct bian
e[n<<1];
struct node
; node(int x, int y)
}a[n];
inline int read()
while(ch >= '0' && ch <= '9')
return s * w;
}void add(int x,int y,int w)
bool comp(node a,node b)
void get_root(int x,int fa)//找重心
max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}void get_dis(int x,int fa,int who)//找到重心的距離, who 記錄他是誰的子樹
}int search(int d)
else l = mid + 1;
}return res;
}void calc(int x,int d)
a[++cnt] = node(0,0);
sort(a + 1, a + cnt + 1, comp);//排一下序
for(int i = 1; i <= m; i++)
}}void slove(int x)
}int main()
for(int i = 1; i <= m; i++) k[i] = read();
max_siz[0] = sum_siz = n; root = 0;
get_root(1,0); slove(root);//找到一開始整顆樹的重心
for(int i = 1; i <= m; i++)
return 0;
}
題目描述
給定一棵 \(n\) 個節點的樹,每條邊有邊權,求出樹上兩點距離小於等於 \(k\) 的點對數量。
和模板題差不多,只要在合併子樹的時候稍微改一下即可。
code:
#pragma gcc optimize(2)
#include#include#includeusing namespace std;
const int n = 4e4+10;
int n,m,tot,cnt,root,sum_siz,u,v,w,k,ans;
int head[n],siz[n],max_siz[n],dis[n],a[100010];
bool vis[n];
struct node
e[n<<1];
inline int read()
while(ch >= '0' && ch <= '9')
return s * w;
}void add(int x,int y,int w)
void get_root(int x,int fa)
max_siz[x] = max(max_siz[x],sum_siz-siz[x]);
if(max_siz[x] < max_siz[root]) root = x;
}void get_dis(int x,int fa)
}int calc(int x,int d)
sort(a+1,a+cnt+1);
int l = 1, r = cnt;
while(l <= r)
else r--;
}return res;
}void slove(int x)
}int main()
k = read();
max_siz[0] = sum_siz = n; root = 0;
get_root(1,0); slove(root);
printf("%d\n",ans);
return 0;
}
動態點分治又叫點分樹(個人覺得點分樹更形象一些),主要解決的樹上的一下帶修改問題。
動態點分治還是基於點分治的那套理論,每次選重心分治。
但考慮到修改操作,我們不可能每次修改都做一遍。
我們可以建點分樹來解決這個問題,具體來說就是:
設當前的分治中心為 \(x\), 由他子樹中的重心 \(y\) 向 \(x\) 連邊,不難發現這樣會構成一棵樹,這棵樹也被叫做點分樹。
不難發現當我們要修改 \(x\) 這個節點的資訊的時候,發現他會影響到的是 \(x\) 到根節點路徑上的點的資訊。
查詢的話同樣會用到 \(x\) 到根節點路徑上點的資訊。
由於我們樹的高度不超過 \(logn\), 所以直接暴力修改查詢即可。
我們就可以拿資料結構來維護每個點的資訊,巴拉巴拉。
**,咕咕咕
點分治 動態點分治
實在拖得太久了。先扔掉資料 分治的核心是盡量把乙個整體分成接近的兩個部分,這樣遞迴處理可以讓複雜度從n 變成nlogn。兩個問題,如何區分和如何算答案。對於第乙個問題,重心,然後就是找重心的方法,兩個dfs,對於第二個問題,對於每個重心算當前塊中每個點到重心的答案,然後由重心分開的塊要把多餘的資訊去...
動態點分治
由於蒟蒻太遜,現在才開始學動態點分治,寫乙個 blog 吧。動態點分治是利用點分治的過程,建成一顆由子樹重心連線而成的點分樹,這棵樹的高度為 log n 級別的,因此可以通過暴力跳父親完成修改操作。建立點分樹的過程,就是按照點分治的流程,記錄上級重心並連線,即可獲得一棵點分樹。點分樹的結構與原樹不相...
動態點分治
首先你要先會點分治,然後動態點分治就是把點分治可持久化一下,讓其不用再每次詢問時都重新做一遍 具體就是,你考慮做點分的時候,你在每個分治重心上統計它所管轄的所有點的資訊,並計算答案。那麼每個點的資訊只會出現在它上面的分治重心中,所以我們把每層分治重心向上一層的連邊,這樣每個點的資訊只會出現都在它所有...