1. 程式人生 > >[點分治系列] 靜態點分

[點分治系列] 靜態點分

沒錯...我就是要講點分治。這個東西原本學過的,當時學得不好...今天模擬賽又考這個東西結果寫不出來。

於是博主專門又去學了學這個東西,這次絕對要搞懂了...【複賽倒計時:11天】

——正片開始——

點分是一個用來解決樹上路徑問題、距離問題的演算法。說直接點其實就是分治思想在樹上的體現和應用。

首先是分治的方法,既然是樹上問題,自然就是對樹進行分治。

我們對於一棵樹,選定一個點作為它的根,將這個點與其子樹拆開,然後對它的子樹也進行相同的操作,然後利用子樹統計答案。一般來說,點分更像一種思想,而不是一個演算法,當然你也可以把它當演算法來學。

關於怎麼選根來分樹,其他dalao的部落格已經講得非常清楚仔細了,每次選定一棵樹的重心,然後分它,這樣可以做到

O(nlogn)的優秀時間複雜度。

關於求重心,做法就是一個size統計。

這裡還是介紹一下吧(很多部落格都只貼一個程式碼...):
對於一個節點x,若我們把其刪除,原來的樹就會變成若干部分,我們設maxsubtree(x)表示刪除x後剩下的最大子樹的大小,若我們找到一個x,使得maxsubtree(x)最小,x就是這棵樹的重心。

給出求重心的程式碼:

void getroot(int x,int fa){
    siz[x]=1;son[x]=0;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==fa||vis[y])continue;
        getroot(y,x);
        siz[x]+=siz[y];
        if(siz[y]>son[x])son[x]=siz[y];
    }
    if(Siz-siz[x]>son[x])son[x]=Siz-siz[x];
    if(son[x]<maxx)maxx=son[x],root=x;
}

於是我們就知道怎麼拆樹了,後面的東西就不難了。

update: 19.11.5 對於上面的程式碼加一些解釋,siz表示當前在求重心的這一棵樹的大小,root用來記錄重心

我們來講如何統計資訊

首先我們要統計與每一次找到的重心有關的路徑,我們用dis[x]表示目標點(重心)到x的距離。

給出getdis的程式碼:

void getdis(int x,int fa,int d){//d表示x到目標點的距離
    dis[++top]=d;//我們只需要知道每一個點到目標點的距離就行,不用知道這個點是哪個
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==fa||vis[y])continue;
        getdis(y,x,d+val(i));
    }
}

有了dis陣列後,我們就可以很輕鬆的獲得路徑的長度了。

比如,我們已知x到重心的距離是m,y到重心的距離是n,那x到y的距離就是m+n,可能細心的讀者已經發現鍋了。

如果x到y的路徑都在一棵子樹內,我們就會有一段距離被重複計算了,這樣我們得到的路徑就是不對的。

給一張圖理解一下:
如圖,藍色的代表x到重心的路徑,紅色的代表y到重心的路徑,我們可以得到dis[x]=3,dis[y]=2。

如果按照前面說的計算方式,x到y的路徑長度應該是5了,但是並不是,我們的路徑長度是3。

原因就是,綠色的那一段,我們根本不走,我們不經過它。

於是我們要解決這種不合法路徑。知道所有路徑,又知道不合法路徑,利用容斥原理,我們可以得到:

合法路徑=所有路徑 - 不合法路徑。

這樣我們divide的程式碼就出來了:

void divide(int x){
    vis[x]=1;//保證我們每次divide的x都是當前這棵樹的重心,所以標記x已經divide過了
    solve(x,0,1);//計算這棵樹以x為重心的所有路徑答案
    int totsiz=Siz;//這棵樹的大小
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(vis[y])continue;
        solve(y,val(i),0);//求不合法路徑
        maxx=inf,root=0;//初始化
        Siz=siz[y]>siz[x]?totsiz-siz[x]:siz[y];//更新siz
        getroot(y,0);//求出以y為根的子樹
        divide(root);//以y為根的子樹分治下去
    }
}

我們看一看去除不合法路徑的程式碼:

solve(y,val(i),-1);

思考發現:

所有不合法路徑都是在同一棵子樹中的路徑,我們要減去它。

先往下看完solve再回來看這裡。

我們進入到solve中,首先是getdis,以x為目標點獲取dis。但是我們要獲取的是距離為val(i)的dis。

這就使得dis[y]=val(i),所以以y為根的子樹中的所有dis值就等於dis[y]+它們到y的距離,然後因為dis[y]是x到y的距離,

所以我們就求出了以y為根的子樹中所有點到x的距離。

實際一點的理解就是,y到目標點的距離是dis[y]=val(i),y的子樹中的點到x的距離就是它們到y的距離+dis[y],所以跑一遍可以求出y的子樹中所有點到x的距離。

然後就是solve函數了:

我們這裡的solve以模板題:【模板】點分治1 為例,不同題目我們solve的東西不同。

首先肯定可以想到一個O(n^2)的做法的,確實可以水過去這一題。

但是我畢竟是在寫部落格...所以,O(nlogn)做法奉上...

我們這麼想,我們需要統計路徑長為k的點對個數,那我們只要確定了一個點x,另一個點的dis[y]就應該是k-dis[x],我們只需要二分找k-dis[x]就行了。

void solve(int x,int d,int avl){//avl(available)表示這次計算得到的答案是否合法 
    top=0;//清空dis陣列 
    getdis(x,0,d);//獲取到當前這棵樹到x的距離為d的所有dis 
    int cnt=0;
    sort(dis+1,dis+top+1);//排好序準備二分
    dis[0]=-1;//第一個dis設定為奇怪的數方便下面比較 
    for(int i=1;i<=top;i++){//把所有距離相同的點放進一個桶裡面方便操作 
        if(dis[i]==dis[i-1])
            bucket[cnt].amount++;//原來桶的個數+1 
        else
            bucket[++cnt].dis=dis[i],bucket[cnt].amount=1;//新開一個桶 
    }
    for(int i=1;i<=m;i++){
        if(query[i]%2==0)//如果k是偶數的話,我們單獨考慮一下距離為k/2那些點,它們可以互相配對形成長為k的路徑 
            for(int j=1;j<=cnt;j++)
                if(bucket[j].dis==query[i]/2)//如果距離是k/2 
                    ans[i]+=(bucket[j].amount-1)*bucket[j].amount/2*avl;
                    //組合計數,假設我們有x個距離為k/2的點,就有(x-1)*x/2個點對距離為k,也就是我們可以配出這麼多個不同點對
                    //其實就是C(x,2)->x!/((x-2)!*2)->(x-1)*x/2
        for(int j=1;j<=cnt&&bucket[j].dis<query[i]/2;j++){
        //接著列舉<k/2的距離,然後我們二分找>2的距離配對,避免重複(點對(u,v)和(v,u)是等價的),等於k/2的我們前面算過了,所以所有情況都考慮到了 int l=j+1,r=cnt; while(l<=r){ int mid=(l++r)>>1; if(bucket[j].dis+bucket[mid].dis==query[i]){ ans[i]+=bucket[j].amount*bucket[mid].amount*avl; //組合計數記錄答案,假設我們有x個距離為m的點,y個距離為k-m的點,我們就有x*y個不同的點對(分類相乘) break;//這一輪二分完了,下一輪 } if(bucket[j].dis+bucket[mid].dis>query[i])r=mid-1;//大了,往小的二分 else l=mid+1;//小了,往大的二分 } } } }

這麼詳細都看不懂我就教不了了...

接下來就給出所有程式碼吧...(我知道你們只想看這個/doge)

#include<bits/stdc++.h>
#define N 100010
#define lint long long
#define inf 0x7fffffff
using namespace std;
int vis[N],son[N],Siz,maxx,siz[N];
int root,head[N],tot,n,m,dis[N],top,query[N],ans[N];
lint k;
struct Bucket{
    int dis,amount;
}bucket[N];
struct Edge{
    int nxt,to,val;
    #define nxt(x) e[x].nxt
    #define to(x) e[x].to
    #define val(x) e[x].val
}e[N<<1];
inline int read(){
    int data=0,w=1;char ch=0;
    while(ch!='-' && (ch<'0'||ch>'9'))ch=getchar();
    if(ch=='-')w=-1,ch=getchar();
    while(ch>='0' && ch<='9')data=data*10+ch-'0',ch=getchar();
    return data*w;
}
inline void addedge(int f,int t,int val){
    nxt(++tot)=head[f];to(tot)=t;val(tot)=val;head[f]=tot;
}
void getroot(int x,int fa){
    siz[x]=1;son[x]=0;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==fa||vis[y])continue;
        getroot(y,x);
        siz[x]+=siz[y];
        if(siz[y]>son[x])son[x]=siz[y];
    }
    if(Siz-siz[x]>son[x])son[x]=Siz-siz[x];
    if(son[x]<maxx)maxx=son[x],root=x;
}
void getdis(int x,int fa,int d){
    dis[++top]=d;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(y==fa||vis[y])continue;
        getdis(y,x,d+val(i));
    }
}
void solve(int rt,int d,int avl){//avl(available)表示這次計算得到的答案是否合法 
    top=0;//清空dis陣列 
    getdis(rt,0,d);//獲取到當前這棵樹的rt的距離為d的所有dis 
    int cnt=0;
    sort(dis+1,dis+top+1);//排好序準備二分
    dis[0]=-1;//第一個dis設定為奇怪的數方便下面比較 
    for(int i=1;i<=top;i++){//把所有距離相同的點放進一個桶裡面方便操作 
        if(dis[i]==dis[i-1])
            bucket[cnt].amount++;//原來桶的個數+1 
        else
            bucket[++cnt].dis=dis[i],bucket[cnt].amount=1;//新開一個桶 
    }
    for(int i=1;i<=m;i++){
        if(query[i]%2==0)//如果k是偶數的話,我們單獨考慮一下距離為k/2那些點,它們可以互相配對形成長為k的路徑 
            for(int j=1;j<=cnt;j++)
                if(bucket[j].dis==query[i]/2)//如果距離是k/2 
                    ans[i]+=(bucket[j].amount-1)*bucket[j].amount/2*avl;
                    //組合計數,假設我們有x個距離為k/2的點,就有(x-1)*x/2個點對距離為k,也就是我們可以配出這麼多個不同點對
                    //其實就是C(x,2)->x!/((x-2)!*2)->(x-1)*x/2
        for(int j=1;j<=cnt&&bucket[j].dis<query[i]/2;j++){//接著列舉<k/2的距離,然後我們二分找>2的距離配對 
            int l=j+1,r=cnt;
            while(l<=r){
                int mid=(l+r)>>1;
                if(bucket[j].dis+bucket[mid].dis==query[i]){
                    ans[i]+=bucket[j].amount*bucket[mid].amount*avl;
                    //組合計數記錄答案,假設我們有x個距離為m的點,y個距離為k-m的點,我們就有x*y個不同的點對(分類相乘)
                    break;//這一輪二分完了,下一輪 
                }
                if(bucket[j].dis+bucket[mid].dis>query[i])r=mid-1;//大了,往小的二分
                else l=mid+1;//小了,往大的二分
            }
        }
    }    
}
void divide(int x){
    vis[x]=1;
    solve(x,0,1);//合法的算進去 
    int totsiz=Siz;
    for(int i=head[x];i;i=nxt(i)){
        int y=to(i);
        if(vis[y])continue;
        solve(y,val(i),-1);//不合法的算出來減掉 
        maxx=inf,root=0;
        Siz=siz[y]>siz[x]?totsiz-siz[x]:siz[y];
        getroot(y,0);
        divide(root);
    }
}
int main(){
    n=read();m=read();
    for(int i=1;i<n;i++){
        int x=read(),y=read(),z=read();
        addedge(x,y,z);addedge(y,x,z);
    }
    for(int i=1;i<=m;i++)query[i]=read();
    maxx=inf;root=0;Siz=n;
    getroot(1,0);
    divide(root);
    for(int i=1;i<=m;i++){
        if(ans[i]>0)puts("AYE");
        else puts("NAY");
    }
    return 0;
}

這份程式碼不僅求出了是否有路徑長為k的答案存在,還求出了路徑長為k的點對個數。

不需要求個數的話你就改一改solve,這樣常數會小一點,跑得更快,但是這一題我更想給你們講講思路,學會舉一反三...最後還是那句話,我個人並不認為點分是一種演算法,它更多體現的是分治的思想在樹上的應用。

其實你會發現,很多高階的資料結構、演算法之類的,都是由基礎的演算法思想衍生出來的泛用性更強的東西。

懂了基礎的演算法思想,你不但可以輕鬆的學會各種高階演算法,甚至可以自己造出解題的演算法。

比如圖論裡面的dijkstra最短路演算法,不就是貪心嗎?再比如線段樹,不就是分治嗎?

一些看起來很高階,聽起來很難的東西,只要你弄明白了其中的本質,你在感嘆發明者的智慧同時,自己也就收穫到了其中蘊含的知識和基礎思想的應用方法。學習不是死學...只有弄明白了它的工作原理和方式,你才算是掌握了它,僅僅是可以用它來做題,那題目變變形,你就一臉懵