1. 程式人生 > >【點分治】的學習筆記和眾多例題

【點分治】的學習筆記和眾多例題

【前言】

  最近一段時間變成了通過題目學習演算法,似乎整個人都亂套了(反思ing)

  不過還好,現在又調整為了學演算法後做題。(唉,最近一段時間有點急躁,要記住萬事不能速成啊)

【正題】點分治

  一句話:點分治主要用於樹上路徑點權統計問題。

一、【具體流程】

1,選取一個點,將無根樹變成有根樹
 為了使每次的處理最優,我們通常要選取樹的重心。
 何為“重心”,就是要保證與此點連線的子樹的節點數最大值最小,可以防止被卡。
 重心求法:
  1。dfs一次,算出以每個點為根的子樹大小。
  2。記錄以每個節點為根的最大子樹大小
  3。判斷:如果以當前節點為根更優,就更新當前根。

void getroot(int v,int fa)
{
    son[v] = 1; f[v] = 0;//f記錄以v為根的最大子樹的大小 
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            getroot(e[i].to,v);//遞迴更新 
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);//比較每個子樹 
        }
    f[v] = max
(f[v],sum-son[v]);//別忘了以v父節點為根的子樹 if(f[v] < f[root]) root = v;//更新當前根 }

2、處理連通塊中通過根節點的路徑。
  (注意,是通過根節點的路徑,所以後面要去掉同一子樹內部的路徑,即去重)
3、標記根節點(相當於處理後,將根節點從子樹中刪除)。
4、遞迴處理當前點為根的每棵子樹。

int solve(int v)
{
    vis[v] = 1;//標記 
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            root = 0
; sum = son[e[i].to]; getroot(e[i].to,v); solve(root);//遞迴處理下一個連通塊 } } int main() { sum = f[0] = n;//初始化 root = 0; getroot(1,0);//找重心 solve(root);//點分治 }

【註釋】:作者是用 son[] 來表示節點x為根的子樹大小,可能他人更多地是用size[]來表示,二者同意。

二、【POJ 1741 & BZOJ 1468 & BZOJ 3365】

給你一棵TREE,以及這棵樹上邊的距離.問有多少對點它們兩者間的距離小於等於K。
【題解】:
  我們找到樹的重心,然後dfs,求出每個點到root的距離deep,然後對deep排序,掃描哪些點對是符合的。
  但是,點分治要求處理的路徑是經過root,所以如果一條路徑是在同一個子樹之內的就不符合要求,所以還要對子樹dfs一下,然後去重。
  接下來處理好root後,就可以處理其他連通塊了,即遞迴其子樹。
【程式碼】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 10010
#define inf 1e9+10
struct node{int to,c,next;}g[N*2];
int head[N],m;
int son[N],f[N];
bool vis[N];
int d[N],deep[N];
int n,sum,root,k,ans;

void add_edge(int from,int to,int cost)
{
    g[++m].next = head[from];
    head[from] = m;
    g[m].to = to; g[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1; f[v] = 0;
    for(int i = head[v];i;i=g[i].next)
        if(g[i].to != fa && !vis[g[i].to])
        {
            getroot(g[i].to,v);
            son[v] += son[g[i].to];
            f[v] = max(f[v],son[g[i].to]);
        }
    f[v] = max(f[v],sum - son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    deep[++deep[0]] = d[v];
    for(int i = head[v];i;i=g[i].next)
        if(g[i].to != fa && !vis[g[i].to])
        {
            d[g[i].to] = d[v] + g[i].c;
            getdeep(g[i].to,v);
        }
}

int cal(int v,int cost)
{
    d[v] = cost; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int l = 1,r = deep[0],sum = 0;
    while(l < r) {
        if(deep[l]+deep[r] <= k) {
            sum += r-l;
            l++;
        } else r--;
    }
    return sum;
}

void solve(int v)
{
    ans += cal(v,0);
    vis[v] = 1;
    for(int i = head[v];i;i=g[i].next)
        if(!vis[g[i].to])
        {
            ans -= cal(g[i].to,g[i].c);
            sum = son[g[i].to];
            root = 0;
            getroot(g[i].to,0);
            solve(root);
        }
}

int main()
{
    int u,v,w;
    while(scanf("%d%d",&n,&k) && n && k)
    {
        ans = root = m = 0;
        memset(vis,0,sizeof(vis));
        memset(head,0,sizeof(head));
        for(int i = 1;i < n;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            add_edge(u,v,w);
            add_edge(v,u,w);
        }
        f[0] = inf;
        sum = n;
        getroot(1,0);
        solve(root);
        printf("%d\n",ans);
    }
    return 0;
}

【補】:若是距離等於k,cal可以改成:

int cal(int v,int cost)
{
    d[v] = cost; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int r = deep[0],res = 0;
    for(int l = 1;l < r;l++)
        while(deep[l]+deep[r] >= k) {
            if(deep[l] + deep[r] == k) res++;
            r--;
        }
    return res;
}

三、【BZOJ 2152】

由爸爸在紙上畫n個“點”,並用n-1條“邊”把這n個“點”恰好連通(其實這就是一棵樹)。並且每條“邊”上都有一個數。接下來由聰聰和可可分別隨即選一個點(當然他們選點時是看不到這棵樹的),如果兩個點之間所有邊上數的和加起來恰好是3的倍數,則判聰聰贏,否則可可贏。聰聰非常愛思考問題,在每次遊戲後都會仔細研究這棵樹,希望知道對於這張圖自己的獲勝概率是多少。現請你幫忙求出這個值以驗證聰聰的答案是否正確。
【題解】:
  感覺這道更好處理,不用快排,也不用去重。我們對於當前的樹,直接找到重心V,然後從V出發,搜尋與V相鄰的點,計算邊長的餘數分別是是0,1,2的情況數,用t[0],t[1],t[2]分別表示。
  顯然答案就是 t[1]*t[2]*2+t[0]*t[0]。
【程式碼】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 20010
struct node{int to,c,next;}e[N*2];
int head[N],m;
int ans,root,t[4],d[N],son[N],f[N],sum;
bool vis[N];

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] = m;
    e[m].to = to; e[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i = e[i].next)
        if(!vis[e[i].to] && e[i].to != fa)
        {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    t[d[v]]++;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to] && e[i].to != fa)
        {
            d[e[i].to] = (d[v] + e[i].c)%3;
            getdeep(e[i].to,v);
        }
}

int cal(int v,int w)
{
    t[0] = t[1] = t[2] = 0;
    d[v] = w;
    getdeep(v,0);
    return t[1]*t[2]*2+t[0]*t[0];
}

void solve(int v)
{
    ans += cal(v,0); vis[v] = 1;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to])
        {
            ans -= cal(e[i].to,e[i].c);
            root = 0; sum = son[e[i].to];
            getroot(e[i].to,0);
            solve(root);
        }
}

inline int gcd(int a,int b){return b == 0 ? a : gcd(b,a%b);}
int main()
{
    int n,u,v,w;
    scanf("%d",&n);
    for(int i = 1;i < n;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        w %= 3;
        add_edge(u,v,w); add_edge(v,u,w);
    }
    sum = n;f[0] = n;
    root = ans = 0;
    getroot(1,0);
    solve(root);
    int x = gcd(ans,n*n);
    printf("%d/%d\n",ans/x,n*n/x);
    return 0;
}

先到這裡,我下去逛逛。未完待續……

好了,我又回來了

四、【BZOJ 2599】

給一棵樹,每條邊有權.求一條簡單路徑,權值和等於K,且邊的數量最小.N <= 200000, K <= 1000000
【題解】:
參考黃學長的題解啊。
  開一個100W的陣列t,t[i]表示權值為i的路徑最少邊數
  找到重心分成若干子樹後, 得出一棵子樹的所有點到根的權值和x,到根a條邊,用t[k-x]+a更新答案,全部查詢完後,再用所有a更新t[x],這樣可以保證不出現點分治中的不合法情況。
  把一棵樹的所有子樹搞完後再遍歷所有子樹恢復T陣列,如果用memset應該會比較慢
  
看的稀裡糊塗的,但還是好像懂了一點啊。
  d陣列 表示已經有幾條邊
  dis陣列 表示子樹中的點到根的距離
  add函式用於更新和初始化(好像有這個功能吧)
【程式碼】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 200010
#define inf 1000000000
struct node{int to,c,next;}e[N*2];
int head[N],m,k;
bool vis[N];
int t[1000010];
int sum,f[N],dis[N],d[N],son[N],root,ans;

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] = m;
    e[m].to = to;e[m].c = cost;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void cal(int v,int fa)
{
    if(dis[v] <= k) ans = min(ans,d[v]+t[k-dis[v]]);
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            d[e[i].to] = d[v] + 1;
            dis[e[i].to] = dis[v] + e[i].c;
            cal(e[i].to,v);
        }
}

void add(int v,int fa,bool flag)
{
    if(dis[v] <= k) {
        if(flag) t[dis[v]] = min(t[dis[v]],d[v]);
        else t[dis[v]] = inf;
    }
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to])
            add(e[i].to,v,flag);
}

void solve(int v)
{
    vis[v] = 1;t[0] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            d[e[i].to] = 1;
            dis[e[i].to] = e[i].c;
            cal(e[i].to,0);
            add(e[i].to,0,1);
        }
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) add(e[i].to,0,0);
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            root = 0;
            sum = son[e[i].to];
            getroot(e[i].to,0);
            solve(root);
        }
}

int main()
{
    int n,u,v,w;;
    scanf("%d%d",&n,&k);
    for(int i = 1;i <= k;i++) t[i] = n;
    for(int i = 1;i < n;i++) {
        scanf("%d%d%d",&u,&v,&w);
        u++; v++;
        add_edge(u,v,w); add_edge(v,u,w);
    }
    ans = sum = f[0] = n;
    root = 0;
    getroot(1,0);
    solve(root);
    if(ans != n) printf("%d\n",ans);
        else puts("-1");
    return 0;
}

啊!
今天對點分治的學習差不多就到這裡了。
筆記結束,開始刷水題玩嘍。

補:

五、【BZOJ 1316】

一棵n個點的帶權有根樹,有p個詢問,每次詢問樹中是否存在一條長度為Len的路徑,如果是,輸出Yes否輸出No.
【題解】:
  運用點分治統計點到重心的距離,再兩次二分查詢距離,判斷有多少條路徑長度為k(這樣為了方便去重)
  聽說點分治的常數比較大,所以將所有詢問在一次點分治中一起做。
【程式碼】:

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

#define N 10010
struct node{int to,w,next;}e[N*2];
int head[N],m,p;
int q[110],f[N],son[N],sum,root,d[N],deep[N];
bool vis[N];

void add_edge(int from,int to,int cost)
{
    e[++m].next = head[from];
    head[from] =m;
    e[m].w = cost; e[m].to = to;
}

void getroot(int v,int fa)
{
    son[v] = 1;f[v] = 0;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to] && e[i].to != fa) {
            getroot(e[i].to,v);
            son[v] += son[e[i].to];
            f[v] = max(f[v],son[e[i].to]);
        }
    f[v] = max(f[v],sum-son[v]);
    if(f[v] < f[root]) root = v;
}

void getdeep(int v,int fa)
{
    deep[++deep[0]] = d[v];
    for(int i = head[v];i;i=e[i].next)
        if(e[i].to != fa && !vis[e[i].to]) {
            d[e[i].to] = d[v] + e[i].w;
            getdeep(e[i].to,v);
        }
}

int findl(int L,int R,int k)
{
    int ans = 0;
    while(L <= R){
        int mid = (L+R)>>1;
        if(deep[mid] == k){ans = mid;R = mid-1;}
        else if(deep[mid] < k) L = mid + 1;
                else R = mid - 1;
    }
    return ans;
}

int findr(int L,int R,int k)
{
    int ans = -1;
    while(L <= R) {
        int mid = (L+R)>>1;
        if(deep[mid] == k){ans = mid;L = mid+1;}
        else if(deep[mid] < k) L = mid+1;
                else R = mid - 1;
    }
    return ans;
}

int cal(int v,int now,int k)
{
    d[v] = now; deep[0] = 0;
    getdeep(v,0);
    sort(deep+1,deep+deep[0]+1);
    int t = 0;
    for(int i = 1;i <= deep[0];i++) {
        if(deep[i] + deep[i] > k) break;
        int l = findl(i,deep[0],k-deep[i]);
        int r = findr(i,deep[0],k-deep[i]);
        t += r-l+1;
    }
    return t;
}

int ans[110];
void solve(int v)
{
    for(int i = 1;i <= p;i++) ans[i] += cal(v,0,q[i]);
    vis[v] = 1;
    for(int i = head[v];i;i=e[i].next)
        if(!vis[e[i].to]) {
            for(int j = 1;j <= p;j++)
                ans[j] -= cal(e[i].to,e[i].w,q[j]);
            sum = son[e[i].to];
            root = 0;
            getroot(e[i].to,0);
            solve(root);
        }
}

int main()
{
    int n,u,v,w;
    scanf("%d%d",&n,&p);
    m = 0;
    for(int i = 1;i < n;i++)
    {
        scanf("%d%d%d",&u,&v,&w);
        add_edge(u,v,w); add_edge(v,u,w);
    }
    for(int i = 1;i <= p;i++) scanf("%d",&q[i]);
    sum = f[0] = n;
    root = 0;
    getroot(1,0);
    solve(root);
    for(int i = 1;i <= p;i++)
        if(ans[i]) puts("Yes"); else puts("No");
    return 0;
}

吾 點分治 之道路大概結束於此。

PS:4月3日,第四次更新。