1. 程式人生 > 其它 >P3806 【模板】點分治1題解

P3806 【模板】點分治1題解

題幹

給定一棵有n個點的樹,詢問樹上距離為k的點對是否存在。

分析

Part1:點分治的意義

分治都懂吧,將問題劃分為若干個子問題,最後合併答案

可以類比歸併排序,氣泡排序的複雜度為O(n^2),而歸併則是穩定的O(n log2 n)

點分治同理,我們將每個樹劃分為若干個子樹,然後分開來處理,這樣可以優化時間複雜度

Part2:如何點分治

觀察若有這樣一棵樹,若我們把1當做根節點,則點分治就毫無意義

因為這時候我們的遞迴層數為n層

時間複雜度仍然是O(n^2)

但如果看做這樣一棵樹呢?

顯然,若按照根節點分割開後,剩下的子樹為12,45每一顆樹的大小縮小為原來的一半

所以遞迴的層數直接降為 log2 n 層

可以得到現在的複雜度僅有O(n log2 n)

那麼我們怎麼找到要切割的這個點呢?

顯然是樹的重心,這樣分割完後剩下的子樹會盡量地平衡(size的最大會最小),從而達到最優的時間複雜度

Part 3:如何操作

對於一棵樹,該樹上的所有路徑必能分為:

1.經過根節點的路徑

2.沒經過根節點的路徑

情況1:

先考慮第一種情況:我們對每一個根(也就是重心),我們求取他所有的dis[k]表示k到根的距離

那麼任意兩點的距離(經過根)distance(x,y)=dis[x]+dis[y]

對於一個詢問k,若存在dis[x]和dis[y]=k-dis[x]則說明這個條件可以滿足

——————————————————————————————————————————————

但仍有一個例外:

假定我們以5為根節點(只畫出了左子樹嗷),設每一條邊長為1

若k=4,那麼4和1到5的距離加起來正好為4

但顯然1到4這條鏈上不會有5,所以我們還要統計該點是從那個(根節點的親兒子)來的

若兩個點從同一個兒子過來的,那麼就忽略這種情況

——————————————————————————————————————————————---

回到如何檢驗該詢問能否成立

我們將所有的dis從小到大排一個序,設定兩個座標1和dis的長度分別為l,r

若dis[l]+dis[r]>k,就將r減一,這樣總和就會變小

反之,將l加一,這樣總和就會變大

情況2:

如果該條鏈不經過根節點,那麼必然在他的子樹裡,我們就要進行分治

先將根節點斷開,對於每一棵子樹,我們先求得他的根節點也就是重心

之後我們仍然考慮兩種情況:

1.經過根節點的路徑

2.沒經過根節點的路徑

懂了嗎,情況一就和上面一樣,情況二則繼續分治

從而有了這樣一個divide函式:

void divide(int x) {
    vis[x]=1;
    calc(x);
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        root=0;
        getrt(y,0,size[y]);
        divide(root);
    }
}

vis陣列表示這個點已經被隔開來,calc函式用於解決在這個根節點上的情況一

之後對於他的每一個孩子,我們都要找到他的重心,然後繼續搜尋

下面貼一下calc函式,getrt函式,以及其附屬函式

calc:

void calc(int x) {
    tot=1;
    a[tot]=x;
    dis[x]=0;
    father[x]=x;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        get(y,x,val[i],y);
    }
    sort(a+1,a+tot+1,cmp);
    for(int i=1; i<=m; i++) {
        int l=1,r=tot;
        if(ans[i]) continue;
        while(l<r) {
            if(dis[a[l]]+dis[a[r]]>query[i]) r--;
            else 
            if(dis[a[l]]+dis[a[r]]<query[i]) l++;
            else 
            if(father[a[l]]==father[a[r]])
            {
                if(dis[a[r]]==dis[a[r-1]])r--;
                else l++;
            }
            else {
                ans[i]=1;
                break;
            }
        }
    }
}

dis[i]表示所有點到i的距離

father[i]表示i對應的根的親兒子

get函式用於得到所有的dis和father

之後就按照我們上面的思路,排序,查詢,如果第i個詢問可行,則ans[i]=1

然後是get函式以及cmp函式:

bool cmp(int x,int y) {return dis[x]<dis[y];}
void get(int x,int fa,int now,int rt) {
    tot++;
    a[tot]=x;
    dis[x]=now;
    father[x]=rt;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        get(y,x,now+val[i],rt);
    }
}

很明顯,不用過多的分析。

然後是getrt函式

void getrt(int x,int fa,int total) {
    size[x]=1;
    maxsum[x]=0;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        getrt(y,x,total);
        size[x]+=size[y];
        maxsum[x]=max(size[y],maxsum[x]);
    }
    maxsum[x]=max(maxsum[x],total-size[x]);
    if(!root||maxsum[x]<maxsum[root]) root=x;
}

依舊是很常規,maxsum[i]記錄i的最大子樹的大小,size記錄他子節點以及他本身的個數

然後就完美的解決遼~

程式碼:

void add(int a,int b,int c) {
    cnt++;
    nxt[cnt]=head[a];
    to[cnt]=b;
    val[cnt]=c;
    head[a]=cnt;
}
void getrt(int x,int fa,int total) {
    size[x]=1;
    maxsum[x]=0;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        getrt(y,x,total);
        size[x]+=size[y];
        maxsum[x]=max(size[y],maxsum[x]);
    }
    maxsum[x]=max(maxsum[x],total-size[x]);
    if(!root||maxsum[x]<maxsum[root]) root=x;
}
bool cmp(int x,int y) {
    return dis[x]<dis[y];
}
void get(int x,int fa,int now,int rt) {
    tot++;
    a[tot]=x;
    dis[x]=now;
    father[x]=rt;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(y==fa||vis[y]) continue;
        get(y,x,now+val[i],rt);
    }
}
void calc(int x) {
    tot=1;
    a[tot]=x;
    dis[x]=0;
    father[x]=x;
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        get(y,x,val[i],y);
    }
    sort(a+1,a+tot+1,cmp);
    for(int i=1; i<=m; i++) {
        int l=1,r=tot;
        if(ans[i]) continue;
        while(l<r) {
            if(dis[a[l]]+dis[a[r]]>query[i]) r--;
            else 
            if(dis[a[l]]+dis[a[r]]<query[i]) l++;
            else 
            if(father[a[l]]==father[a[r]])
            {
                if(dis[a[r]]==dis[a[r-1]])r--;
                else l++;
            }
            else {
                ans[i]=1;
                break;
            }
        }
    }
}
void divide(int x) {
    vis[x]=1;
    calc(x);
    for(int i=head[x]; i; i=nxt[i]) {
        int y=to[i];
        if(vis[y]) continue;
        root=0;
        getrt(y,0,size[y]);
        divide(root);
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin>>n>>m;
    for(int i=1; i<n; i++) {
        int x,y,w;
        cin>>x>>y>>w;
        add(x,y,w);
        add(y,x,w);
    }
    for(int i=1; i<=m; i++) {
        cin>>query[i];
        if(!query[i]) ans[i]=1;
    }
    maxsum[0]=n;
    getrt(1,0,n);
    divide(root);
    for(int i=1; i<=m; i++)
        if(ans[i]) cout<<"AYE"<<endl;
        else cout<<"NAY" <<endl;
    return 0;
}
View Code

query儲存讀入的k

ans用於記錄該詢問可不可行

在進入搜尋前先找一遍重心

AC~