點分治(樹分治)

樹上點分治

思想

兩個點之間的距離無非就是兩種關係:我們約定dis[i]dis[i]表示這個點到當前根節點的距離

  • dis[u]+dis[v]dis[u] + dis[v],在同一個根節點的不同子樹上。
  • dis[u]+dis[v]dis[u] + dis[v],在同一個根節點的同一個節點上。

樹上點分治的思想就是通過改變根節點從而轉化任意兩點的距離爲在同一個根節點下的情況。

舉個例子

當我們選定1號節點作爲我們的根節點時,我們可以簡單的得到(三號節點的子樹上的點到節點1, 4, 2, 7的距離,也就是不在三號節點子樹上的點的距離)(4, 2子樹同理)。

通過這一步轉換我們只需要得到三號節點子樹上的點之間的距離即可,這就是分治思想的體現,我們可以不斷地遞歸最後只剩一個節點,這個節點的子樹上的點到其子樹上的點的距離就是確定的了,就是0嘛,只可能是它自己到它自己。

所以簡而言之,點分治就是去不斷地遞歸某個節點地子樹,知道沒有子樹。

假如我們的點是連接成一串的,我們能任選一個點去當初始節點的子樹嗎?

這裏顯然是不能的,當我們選定的節點剛好是端點的時候,這個時候複雜度將會變成n2n^2,這完全違背了我們優化其的初衷。

於是這裏有一個簡單的優化方法,就是每次我們選取每顆子樹的重心去充當根節點,這樣的分治效果顯然是最優的。

於是我們的樹上點分治算法好像已近逐漸可以寫出來了,我們通過下面這個例子來更加理解一下實現過程吧。

P3806 【模板】點分治1 + 代碼

/*
    樹上點分治
*/

#include <bits/stdc++.h>

using namespace std;

const int INF = 0x3f3f3f3f;
const int N = 1e5 + 10;

int head[N], to[N << 1], nex[N << 1], value[N << 1], cnt = 1;
int sz[N], maxsz[N], dis[N], pre[N], vis[N], judge[10000010], is_true[110], query[110], q[N];
int n, m, sum, root;

inline int read() {
    int f = 1, x = 0;
    char c = getchar();
    while(c < '0' || c > '9') {
        if(c == '-')    f = -1;
        c = getchar();
    }
    while(c >= '0' && c <= '9') {
        x = (x << 1) + (x << 3) + (c ^ 48);
        c = getchar();
    }
    return f * x;
}

void get_root(int rt, int fa) {//簡單的找重心
    sz[rt] = 1, maxsz[rt] = 0;
    for(int i = head[rt]; i; i = nex[i]) {
        if(vis[to[i]] || to[i] == fa)   continue;//加了一個vis判斷,防止跑到已經訪問過的根節點給上去。
        get_root(to[i], rt);
        maxsz[rt] = max(maxsz[rt], sz[to[i]]);
        sz[rt] += sz[to[i]];
    }
    maxsz[rt] = max(maxsz[rt], sum - sz[rt]);
    if(maxsz[rt] < maxsz[root]) root = rt;
}

void get_dis(int rt, int fa) {//就是dfs樹上最短路的實現過程。
    pre[++pre[0]] = dis[rt];//記錄其子樹的每個節點到根節點的距離。
    for(int i = head[rt]; i; i = nex[i]) {
        if(to[i] == fa || vis[to[i]]) continue;
        dis[to[i]] = dis[rt] + value[i];
        get_dis(to[i], rt);
    }
}

void calc(int rt) {//核心。
    int p = 0;
    for(int i = head[rt]; i; i = nex[i]) {
        if(vis[to[i]])  continue;//同樣的也是訪問子樹。
        dis[to[i]] = value[i];//這裏一定要記得重置。
        pre[0] = 0;
        get_dis(to[i], rt);
        for(int j = 1; j <= pre[0]; j++)//查詢有沒有點到當前子樹的點的距離是符合query中的要求的。
            for(int k = 1; k <= m; k++)
                if(query[k] >= pre[j])
                    is_true[k] |= judge[query[k] - pre[j]];
        for(int j = 1; j <= pre[0]; j++)//記錄我們judge中被標記的點,方便在下一次分治之前重置。
            if(pre[j] <= 1e7 + 5)//特判一下吧,題目的dis可能會到1e8,爲了防止數組越界,
            q[++p] = pre[j], judge[pre[j]] = 1;
    }
    for(int i = 1; i <= p; i++)//不用memset重置,防止變成n^2的算法。
        judge[q[i]] = 0;
}

void solve(int rt) {
    vis[rt] = judge[0] = 1;//置這個點被訪問過,防止其子樹上的點再次訪問這個點。
    calc(rt);
    for(int i = head[rt]; i; i = nex[i]) {
        if(vis[to[i]])    continue;//我們肯定是找一個沒有訪問的子樹上的點去進行下一次分治遞歸。
        sum = sz[to[i]], root = 0;
        maxsz[root] = INF;
        get_root(to[i], 0);
        solve(root);
    }
}

void add(int x, int y, int w) {
    to[cnt] = y;
    nex[cnt] = head[x];
    value[cnt] = w;
    head[x] = cnt++;
}

int main() {
    // freopen("in.txt", "r", stdin);
    n = read(), m = read();
    int x, y, w;
    for(int i = 1; i < n; i++) {//雙向建邊。
        x = read(), y = read(), w = read();
        add(x, y, w);
        add(y, x, w);
    }
    for(int i = 1; i <= m; i++)
        query[i] = read();
    root = 0;//尋找初始的遞歸根節點。
    maxsz[root] = INF;
    get_root(1, 0);
    solve(root);
    for(int i = 1; i <= m; i++)
        puts(is_true[i] ? "AYE" : "NAY");
    return 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章