[點分治系列] 靜態點分

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

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

——正片開始——數組

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

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

咱們對於一棵樹,選定一個點做爲它的根,將這個點與其子樹拆開,而後對它的子樹也進行相同的操做,而後利用子樹統計答案。通常來講,點分更像一種思想,而不是一個算法,固然你也能夠把它當算法來學。函數

關於怎麼選根來分樹,其餘dalao的博客已經講得很是清楚仔細了,每次選定一棵樹的重心,而後分它,這樣能夠作到學習

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

關於求重心,作法就是一個size統計。3d

這裏仍是介紹一下吧(不少博客都只貼一個代碼...):
對於一個節點x,若咱們把其刪除,原來的樹就會變成若干部分,咱們設maxsubtree(x)表示刪除x後剩下的最大子樹的大小,若咱們找到一個x,使得maxsubtree(x)最小,x就是這棵樹的重心。
code

給出求重心的代碼:

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,x);//求出以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,x);
        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最短路算法,不就是貪心嗎?再好比線段樹,不就是分治嗎?

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

相關文章
相關標籤/搜索