【點分治】的學習筆記和眾多例題
【前言】
最近一段時間變成了通過題目學習演算法,似乎整個人都亂套了(反思ing)
不過還好,現在又調整為了學演算法後做題。(唉,最近一段時間有點急躁,要記住萬事不能速成啊)
【正題】點分治
一句話:點分治主要用於樹上路徑點權統計問題。
一、【具體流程】
1,選取一個點,將無根樹變成有根樹
為了使每次的處理最優,我們通常要選取樹的重心。
何為“重心”,就是要保證與此點連線的子樹的節點數最大值最小,可以防止被卡。
重心求法:
1。dfs一次,算出以每個點為根的子樹大小。
2。記錄以每個節點為根的最大子樹大小
3。判斷:如果以當前節點為根更優,就更新當前根。
void getroot(int v,int fa)
{
son[v] = 1; f[v] = 0;//f記錄以v為根的最大子樹的大小
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
getroot(e[i].to,v);//遞迴更新
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);//比較每個子樹
}
f[v] = max (f[v],sum-son[v]);//別忘了以v父節點為根的子樹
if(f[v] < f[root]) root = v;//更新當前根
}
2、處理連通塊中通過根節點的路徑。
(注意,是通過根節點的路徑,所以後面要去掉同一子樹內部的路徑,即去重)
3、標記根節點(相當於處理後,將根節點從子樹中刪除)。
4、遞迴處理當前點為根的每棵子樹。
int solve(int v)
{
vis[v] = 1;//標記
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
root = 0 ;
sum = son[e[i].to];
getroot(e[i].to,v);
solve(root);//遞迴處理下一個連通塊
}
}
int main()
{
sum = f[0] = n;//初始化
root = 0;
getroot(1,0);//找重心
solve(root);//點分治
}
【註釋】:作者是用 son[] 來表示節點x為根的子樹大小,可能他人更多地是用size[]來表示,二者同意。
二、【POJ 1741 & BZOJ 1468 & BZOJ 3365】
給你一棵TREE,以及這棵樹上邊的距離.問有多少對點它們兩者間的距離小於等於K。
【題解】:
我們找到樹的重心,然後dfs,求出每個點到root的距離deep,然後對deep排序,掃描哪些點對是符合的。
但是,點分治要求處理的路徑是經過root,所以如果一條路徑是在同一個子樹之內的就不符合要求,所以還要對子樹dfs一下,然後去重。
接下來處理好root後,就可以處理其他連通塊了,即遞迴其子樹。
【程式碼】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 10010
#define inf 1e9+10
struct node{int to,c,next;}g[N*2];
int head[N],m;
int son[N],f[N];
bool vis[N];
int d[N],deep[N];
int n,sum,root,k,ans;
void add_edge(int from,int to,int cost)
{
g[++m].next = head[from];
head[from] = m;
g[m].to = to; g[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1; f[v] = 0;
for(int i = head[v];i;i=g[i].next)
if(g[i].to != fa && !vis[g[i].to])
{
getroot(g[i].to,v);
son[v] += son[g[i].to];
f[v] = max(f[v],son[g[i].to]);
}
f[v] = max(f[v],sum - son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
deep[++deep[0]] = d[v];
for(int i = head[v];i;i=g[i].next)
if(g[i].to != fa && !vis[g[i].to])
{
d[g[i].to] = d[v] + g[i].c;
getdeep(g[i].to,v);
}
}
int cal(int v,int cost)
{
d[v] = cost; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int l = 1,r = deep[0],sum = 0;
while(l < r) {
if(deep[l]+deep[r] <= k) {
sum += r-l;
l++;
} else r--;
}
return sum;
}
void solve(int v)
{
ans += cal(v,0);
vis[v] = 1;
for(int i = head[v];i;i=g[i].next)
if(!vis[g[i].to])
{
ans -= cal(g[i].to,g[i].c);
sum = son[g[i].to];
root = 0;
getroot(g[i].to,0);
solve(root);
}
}
int main()
{
int u,v,w;
while(scanf("%d%d",&n,&k) && n && k)
{
ans = root = m = 0;
memset(vis,0,sizeof(vis));
memset(head,0,sizeof(head));
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w);
add_edge(v,u,w);
}
f[0] = inf;
sum = n;
getroot(1,0);
solve(root);
printf("%d\n",ans);
}
return 0;
}
【補】:若是距離等於k,cal可以改成:
int cal(int v,int cost)
{
d[v] = cost; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int r = deep[0],res = 0;
for(int l = 1;l < r;l++)
while(deep[l]+deep[r] >= k) {
if(deep[l] + deep[r] == k) res++;
r--;
}
return res;
}
三、【BZOJ 2152】
由爸爸在紙上畫n個“點”,並用n-1條“邊”把這n個“點”恰好連通(其實這就是一棵樹)。並且每條“邊”上都有一個數。接下來由聰聰和可可分別隨即選一個點(當然他們選點時是看不到這棵樹的),如果兩個點之間所有邊上數的和加起來恰好是3的倍數,則判聰聰贏,否則可可贏。聰聰非常愛思考問題,在每次遊戲後都會仔細研究這棵樹,希望知道對於這張圖自己的獲勝概率是多少。現請你幫忙求出這個值以驗證聰聰的答案是否正確。
【題解】:
感覺這道更好處理,不用快排,也不用去重。我們對於當前的樹,直接找到重心V,然後從V出發,搜尋與V相鄰的點,計算邊長的餘數分別是是0,1,2的情況數,用t[0],t[1],t[2]分別表示。
顯然答案就是 t[1]*t[2]*2+t[0]*t[0]。
【程式碼】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 20010
struct node{int to,c,next;}e[N*2];
int head[N],m;
int ans,root,t[4],d[N],son[N],f[N],sum;
bool vis[N];
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] = m;
e[m].to = to; e[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i = e[i].next)
if(!vis[e[i].to] && e[i].to != fa)
{
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
t[d[v]]++;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to] && e[i].to != fa)
{
d[e[i].to] = (d[v] + e[i].c)%3;
getdeep(e[i].to,v);
}
}
int cal(int v,int w)
{
t[0] = t[1] = t[2] = 0;
d[v] = w;
getdeep(v,0);
return t[1]*t[2]*2+t[0]*t[0];
}
void solve(int v)
{
ans += cal(v,0); vis[v] = 1;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to])
{
ans -= cal(e[i].to,e[i].c);
root = 0; sum = son[e[i].to];
getroot(e[i].to,0);
solve(root);
}
}
inline int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}
int main()
{
int n,u,v,w;
scanf("%d",&n);
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
w %= 3;
add_edge(u,v,w); add_edge(v,u,w);
}
sum = n;f[0] = n;
root = ans = 0;
getroot(1,0);
solve(root);
int x = gcd(ans,n*n);
printf("%d/%d\n",ans/x,n*n/x);
return 0;
}
先到這裡,我下去逛逛。未完待續……
好了,我又回來了
四、【BZOJ 2599】
給一棵樹,每條邊有權.求一條簡單路徑,權值和等於K,且邊的數量最小.N <= 200000, K <= 1000000
【題解】:
參考黃學長的題解啊。
開一個100W的陣列t,t[i]表示權值為i的路徑最少邊數
找到重心分成若干子樹後, 得出一棵子樹的所有點到根的權值和x,到根a條邊,用t[k-x]+a更新答案,全部查詢完後,再用所有a更新t[x],這樣可以保證不出現點分治中的不合法情況。
把一棵樹的所有子樹搞完後再遍歷所有子樹恢復T陣列,如果用memset應該會比較慢
看的稀裡糊塗的,但還是好像懂了一點啊。
d陣列 表示已經有幾條邊
dis陣列 表示子樹中的點到根的距離
add函式用於更新和初始化(好像有這個功能吧)
【程式碼】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 200010
#define inf 1000000000
struct node{int to,c,next;}e[N*2];
int head[N],m,k;
bool vis[N];
int t[1000010];
int sum,f[N],dis[N],d[N],son[N],root,ans;
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] = m;
e[m].to = to;e[m].c = cost;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void cal(int v,int fa)
{
if(dis[v] <= k) ans = min(ans,d[v]+t[k-dis[v]]);
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
d[e[i].to] = d[v] + 1;
dis[e[i].to] = dis[v] + e[i].c;
cal(e[i].to,v);
}
}
void add(int v,int fa,bool flag)
{
if(dis[v] <= k) {
if(flag) t[dis[v]] = min(t[dis[v]],d[v]);
else t[dis[v]] = inf;
}
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to])
add(e[i].to,v,flag);
}
void solve(int v)
{
vis[v] = 1;t[0] = 0;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
d[e[i].to] = 1;
dis[e[i].to] = e[i].c;
cal(e[i].to,0);
add(e[i].to,0,1);
}
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) add(e[i].to,0,0);
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
root = 0;
sum = son[e[i].to];
getroot(e[i].to,0);
solve(root);
}
}
int main()
{
int n,u,v,w;;
scanf("%d%d",&n,&k);
for(int i = 1;i <= k;i++) t[i] = n;
for(int i = 1;i < n;i++) {
scanf("%d%d%d",&u,&v,&w);
u++; v++;
add_edge(u,v,w); add_edge(v,u,w);
}
ans = sum = f[0] = n;
root = 0;
getroot(1,0);
solve(root);
if(ans != n) printf("%d\n",ans);
else puts("-1");
return 0;
}
啊!
今天對點分治的學習差不多就到這裡了。
筆記結束,開始刷水題玩嘍。
補:
五、【BZOJ 1316】
一棵n個點的帶權有根樹,有p個詢問,每次詢問樹中是否存在一條長度為Len的路徑,如果是,輸出Yes否輸出No.
【題解】:
運用點分治統計點到重心的距離,再兩次二分查詢距離,判斷有多少條路徑長度為k(這樣為了方便去重)。
聽說點分治的常數比較大,所以將所有詢問在一次點分治中一起做。
【程式碼】:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define N 10010
struct node{int to,w,next;}e[N*2];
int head[N],m,p;
int q[110],f[N],son[N],sum,root,d[N],deep[N];
bool vis[N];
void add_edge(int from,int to,int cost)
{
e[++m].next = head[from];
head[from] =m;
e[m].w = cost; e[m].to = to;
}
void getroot(int v,int fa)
{
son[v] = 1;f[v] = 0;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to] && e[i].to != fa) {
getroot(e[i].to,v);
son[v] += son[e[i].to];
f[v] = max(f[v],son[e[i].to]);
}
f[v] = max(f[v],sum-son[v]);
if(f[v] < f[root]) root = v;
}
void getdeep(int v,int fa)
{
deep[++deep[0]] = d[v];
for(int i = head[v];i;i=e[i].next)
if(e[i].to != fa && !vis[e[i].to]) {
d[e[i].to] = d[v] + e[i].w;
getdeep(e[i].to,v);
}
}
int findl(int L,int R,int k)
{
int ans = 0;
while(L <= R){
int mid = (L+R)>>1;
if(deep[mid] == k){ans = mid;R = mid-1;}
else if(deep[mid] < k) L = mid + 1;
else R = mid - 1;
}
return ans;
}
int findr(int L,int R,int k)
{
int ans = -1;
while(L <= R) {
int mid = (L+R)>>1;
if(deep[mid] == k){ans = mid;L = mid+1;}
else if(deep[mid] < k) L = mid+1;
else R = mid - 1;
}
return ans;
}
int cal(int v,int now,int k)
{
d[v] = now; deep[0] = 0;
getdeep(v,0);
sort(deep+1,deep+deep[0]+1);
int t = 0;
for(int i = 1;i <= deep[0];i++) {
if(deep[i] + deep[i] > k) break;
int l = findl(i,deep[0],k-deep[i]);
int r = findr(i,deep[0],k-deep[i]);
t += r-l+1;
}
return t;
}
int ans[110];
void solve(int v)
{
for(int i = 1;i <= p;i++) ans[i] += cal(v,0,q[i]);
vis[v] = 1;
for(int i = head[v];i;i=e[i].next)
if(!vis[e[i].to]) {
for(int j = 1;j <= p;j++)
ans[j] -= cal(e[i].to,e[i].w,q[j]);
sum = son[e[i].to];
root = 0;
getroot(e[i].to,0);
solve(root);
}
}
int main()
{
int n,u,v,w;
scanf("%d%d",&n,&p);
m = 0;
for(int i = 1;i < n;i++)
{
scanf("%d%d%d",&u,&v,&w);
add_edge(u,v,w); add_edge(v,u,w);
}
for(int i = 1;i <= p;i++) scanf("%d",&q[i]);
sum = f[0] = n;
root = 0;
getroot(1,0);
solve(root);
for(int i = 1;i <= p;i++)
if(ans[i]) puts("Yes"); else puts("No");
return 0;
}
吾 點分治 之道路大概結束於此。
PS:4月3日,第四次更新。