點分治教程:
例題
給定一棵帶權樹,顯然共有N*(N-1)/2條邊,問:第k小的邊邊長多長?
N<=10000.
題解:
這道題直接上手做實在是太難了,需要逐步拆分。
首先,問題是第k小的邊的邊長,這個問題不好解,但是轉換一下問題,二分第k小的邊長T,然後判斷這棵樹中<=T的路徑有多少條,這個能稍微好做一下,至少變成了一個統計性問題。
二分後題目變成:
給定一棵帶權樹,顯然共有N*(N-1)/2條邊,問:<=T的邊有多少條(POJ1741)。
顯然的,對於整個樹的根來說,路徑只有兩種:
1.經過根的路徑
2.不經過根的路徑
顯然,不經過根的所有路徑,都單獨屬於某個根的子樹中。
因此,我們完全可以這麼做此題:
void solve(int i)//表示統計以i爲根的這棵樹中的滿足題目要求的路徑個數。
{
work(i); //統計所有經過根節點i的滿足要求的路徑個數。
delete(i); //把節點i從整棵樹中刪掉,這樣i的所有兒子就形成了不同的子樹
for (i的任何一個兒子j)
{
if (j屬於i的某個子樹中)
solve(j); //統計以j爲根節點的子樹中滿足要求的路徑個數
}
}
例如:共有一顆樹,聯通情況如下:
1
2————|————3
4——|——5 6——|——7
那麼,首先調用solve(1),處理所有經過1的路徑,然後把1刪掉,調用solve(2),solve(3)分別處理,solve(2)處理完後調用了solve(4),solve(5),solve(3)以此類推。
這麼做顯然是明智的,把一個規模爲n的問題,通過計算一個比較簡單的子問題,轉換成了兩個規模爲2n的問題。
如果計算這個比較簡單的子問題的複雜度是nlogn的話,那麼總的複雜度就是nlognlogn。
然而,這麼做有一個反例:
1——2——3——4——5——6——7
首先調用solve(1),然後調用solve(2),然後調用solve(3)…
如此一來複雜度退化成了n*nlogn
爲了避免這一種退化,我們可以人爲的規定每棵子樹的根,對於上述問題:
首先調用solve(4),然後兩顆子樹就是1——2——3,5——6——7,再分別調用solve(2),solve(6)。
這樣,最多經過logn層,勢必把所有的節點都處理完畢了。
而每棵子樹的根很好選擇,顯然就是當前子樹的重心!用兩遍dfs可以求得。
solve的代碼非常好寫,如下:
void solve(int now) //表示solve以now爲根的子樹,此時now已經是重心
{
int u;
ans += work(now); //work(now)返回的是經過now的路徑中,滿足題意的數量。
done[now] = true; //把now從樹中刪除
for (int i=0; i<g[now].size(); i++) //枚舉和now相鄰的每一個點u
if (!done[u = g[now][i].v]) //如果u沒有被刪除,說明在某一棵子樹中。
{
f[0] = size = s[u];
getroot(u, root=0); //找到u所在子樹的重心root
solve(root); //遞歸處理root
}
}
不會求樹重心的移步這裏
:http://blog.csdn.net/xdu_truth/article/details/9104629
現在的問題變成了如何寫work(now),即在now所在的子樹中,有多少條經過now的邊長度<=T。
爲了求這個問題,我們可以求出子樹中now到所有點j的
路徑長度dis[j],
從誰來 from[j](表示j是從now編號爲from[j]的兒子走來的,例如1-2-3-4,from[4]=from[3]=from[2]=2)
那麼,問題就變成了,對於每一個dis[j],有多少個dis[t]+dis[j]<=T,且from[j]!=from[t],方法非常多,此處不再贅述。
作業:
poj1741,hdu4812,codeforces161D,bzoj3697,bzoj2152。
1.求數的重心:
//sz[x]-->x的樹大小,f[x]-->x最大子樹的節點數;
void getrt(int x,int fa)//利用*sz,*f求重心
{
sz[x]=1;
f[x]=0;
for(int i=0;i<lin[x].size();i++)
{
int u=lin[x][i].x;
if(vis[u]||u==fa) continue;
getrt(u,x);
sz[x]+=sz[u];
f[x]=max(sz[u],f[x]);
}
f[x]=max(f[x],size-sz[x]);//! x最大子樹的節點數=max(與此子樹大小-f[x],f[x])
if(f[x]<f[rt]) rt=x;
}
2.solve函數
void solve(int x)
{
vis[x]=1;
cal(x);//每到題不同的地方。。。。
for(int i=0;i<lin[x].size();i++)
{
int u=lin[x][i].x;
if(vis[u]) continue;
f[0]=size=sz[u];
getrt(u,rt=0);
solve(rt);
}
}
3**.接下來的cal函數**
//一般的兩個輔助數組
//pre[x] 代表到當前根的子樹時,當前子樹之前路徑上值爲x的方案數
//now[x] 代表到當前根的子樹時,當前子樹上路徑上值爲x的方案數
//dis[x]
void cal(int x)
{
for(int i=0;i<lin[x].size();i++)
{
int u=lin[x][i].x;
dis[u]=lin[x][i].y;
if(vis[u]) continue;
dfs(u,x);//dfs更新子樹的now【】值
//ans1=。。。。。。。g[j]*ff[rev(j)];
//統計數答案數;
}
for(int i=0;i<3;i++) g[i]=0;
}
4.cal中的dfs
void getdis(int x,int fa)//一遍dfs求值+求重心的一點預處理sz[x],求子樹大小size
{
sz[x]=1;
d.push_back(dis[x]);
for(int i=0;i<lin[x].size();i++)
{
int u=lin[x][i].x;
if(vis[u]||u==fa) continue;
// dis[u]=dis[x]+lin[x][i].y;
getdis(u,x);
sz[x]+=sz[u];
}
}
例1:poj 1741
http://blog.csdn.net/alps233/article/details/51392495
nlog n 計算對於每個子樹的經過該子樹的根的方案數;
即 求出子樹中now到所有點j的
路徑長度dis[j],*logn 的排序統計;.cal(x)統計答案,x子樹中滿足dis[x]+dix[y]<=K 的方案數;
**例2:**codeforces 161D] Distance in Tree
http://blog.csdn.net/alps233/article/details/51393322
在cal函數中引入 tmp[],cnt[],代表作到x子樹時,當前層solve中之前子樹《=k的dis個數,因爲k<=500;O(n)計算,且不重複
例3: [bzoj 2152] 聰聰可可
http://blog.csdn.net/alps233/article/details/51396309
明顯的樹上的點分治,利用兩個數組g[]表示搜當前根的子樹時,當前子樹之前的路徑長x的方案數,ff表示當前子樹路徑長x方案數
*ans+=g[j]*ff[(3-j)%3]*2; //注意(1,1)合法,(1,2)(2,1)算兩種;
例4:hdu4812
預處理逆元后cal中處理flag【ni【i】】的位置進行更新
if (path[j]*val[rt]%mod==K) getans(rt,id[j]);
int tmp=K*ni[path[j]*val[rt]%mod]%mod;
if(flag[tmp]!=ca) continue;
getans(F[tmp],id[j]);
例5
bzoj3697: 採藥人的路徑
枚舉根節點的每個子樹。用f[i][0…1],g[i][0…1]分別表示前面幾個子樹以及當前子樹和爲i的路徑數目,0和1用於區分路徑上是否存在前綴和爲i的節點。
ans+=f[0][0] * g[0][0] + f [i][0] * g [-i][1] + f[i][1] * g[-i][0] + f[i][1] * g[-i][1]
//g[x][]代表到當前根的子樹時,當前子樹之前路徑上值爲x的方案數 0代表路徑上有休息點,1代表無休息點的個數;
//f[x][] 代表到當前根的子樹時,當前子樹上路徑上值爲x的方案數 0代表路徑上有休息點,1代表無休息點的個數;
其中i的範圍[-d,d],d爲當前子樹的深度。
在每個子樹 處更新並統計經過rt的方案數即可