點分治詳解


點分治

以下講解都以Luogu P4178 Tree為例

點分治,是一個很簡單非常常見的數據結構

她是一種處理樹上路徑問題的工具,舉個栗子:

給定一棵樹和一個整數k,求樹上邊數等於k的路徑有多少條

當樹的節點數比較多的時候,就不能使用暴力了,我該怎么辦

就要用點分治

原理

dianfenzhi2.png

如圖,我們在這棵樹上選出一個root,那路徑一共有三種情況:

1.在紅子樹中

2.在黑子樹中

3.一半在紅子樹,一半在黑子樹,要過root,拼成一條完整的路徑

分類討論,不存在的qaq,或許這輩子我也不會分類討論

仔細想一下,實際情況1,2都珂以看做情況3,如圖將答案中一點變成root,就成了情況3

dianfenzhi6.png

好的上面只是思想,好像很虛空

我們需要實現

選根

選根的過程實際就是一個樹形dp

選root是非常重要的,選不好會使復雜度爆炸

想想會發現這個根最好是樹的重心

所以一個簡單的樹形dp就能搞定

inline void getroot(register int u,register int fa)
{
    size[u]=1;
    int num=0;
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa||vis[v])
            continue;
        getroot(v,u);
        size[u]+=size[v];
        num=Max(num,size[v]);
    }
    num=Max(num,sizee-size[u]);
    if(mx>num)
        mx=num,rt=u;
}

因為之后的分治過程還需要對子樹單獨找重心,所以代碼中有vis,但是開始對整棵樹無影響

分治

這才是點分治的精華qaq

根據代碼來理解

inline ll devide(register int u)
{
    ll res=solve(u,0); //把當前節點的答案加上去 
    vis[u]=true; //把節點標記,防止陷入死循環 
    int totsize=sizee;
    for(register int i=head[u];i;i=e[i].next)
    {
        //分別處理每一棵子樹 
        int v=e[i].to;
        if(vis[v])
            continue;
        res-=solve(v,e[i].v); //容斥原理,下面說 
        rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
        //把所有信息更新,遞歸進子樹找重心,並繼續分治 
        getroot(v,0);
        res+=devide(rt);
    }
    return res;
}

大部分都應該比較好理解,除了ans-=slove(v,1)這句

我們先看一種情況:

dianfenzhi4.png

在路徑A->Root和B->Root合並時,這種情況顯然是不合法的

所以要減去一些路徑

完整代碼

#include <bits/stdc++.h>
#define N 50005
#define ll long long
#define inf 0x3f3f3f3f
using namespace std;
inline int read()
{
    register int x=0,f=1;register char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9')x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
    return x*f;
}
inline void write(register ll x)
{
    if(!x)putchar('0');if(x<0)x=-x,putchar('-');
    static int sta[36];int tot=0;
    while(x)sta[tot++]=x%10,x/=10;
    while(tot)putchar(sta[--tot]+48);
}
inline int Max(register int a,register int b)
{
    return a>b?a:b;
}
struct node{
    int to,next,v;
}e[N<<1];
int head[N],tot=0;
inline void add(register int u,register int v,register int k)
{
    e[++tot]=(node){v,head[u],k};
    head[u]=tot;
}
int n,k;
int size[N];
int rt,sizee,mx;
bool vis[N];
ll ans=0;
ll d[N],q[N],l,r;
inline void getroot(register int u,register int fa)
{
    size[u]=1;
    int num=0;
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa||vis[v])
            continue;
        getroot(v,u);
        size[u]+=size[v];
        num=Max(num,size[v]);
    }
    num=Max(num,sizee-size[u]);
    if(mx>num)
        mx=num,rt=u;
}
inline void getdis(register int u,register int fa)
{
    q[++r]=d[u];
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa||vis[v])
            continue;
        d[v]=d[u]+e[i].v;
        getdis(v,u);
    }
}
inline ll solve(register int u,register int val)
{
    r=0;
    d[u]=val;
    getdis(u,0);
    ll sum=0;
    l=1;
    sort(q+1,q+r+1);
    while(l<r)
    {
        if(q[l]+q[r]<=k)
            sum+=r-l,++l;
        else
            --r;
    }
    return sum;
 } 
inline ll devide(register int u)
{
    ll res=solve(u,0); 
    vis[u]=true; 
    int totsize=sizee;
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(vis[v])
            continue;
        res-=solve(v,e[i].v);
        rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
        getroot(v,0);
        res+=devide(rt);
    }
    return res;
}
int main()
{
    n=read();
    for(register int i=1;i<n;++i)
    {
        int u=read(),v=read(),w=read();
        add(u,v,w),add(v,u,w);
    }
    k=read();
    sizee=n,mx=inf;
    getroot(1,rt=0);
    ans=devide(rt);
    write(ans);
    return 0;
}

動態點分治

一般,點分治只能處理靜態的問題

但是,毒瘤的出題人加上了修改操作該怎么辦qaq?

每修改一次做一次點分治?復雜度直接飛天

考慮一下,修改操作修改的是點權(不是樹的結構,樹的結構的話請找lct同學)

樹的重心不會改變

先給出點分樹的定義qaq:

點分治時每一層的重心連出的一個深度為logn的樹

2018-12-0616-58-10-879000.png

我們假設已經處理完了所有經過點1的路徑,然后遞歸進子樹繼續點分,那么實際上原樹被拆成了這么兩棵樹,兩個重心分別為2和6

2018-12-0616-58-45-672000.png

那么把第一層的重心和第二層的重心給連接起來(用紅色表示)

2018-12-0616-59-30-885000.png

然后我們繼續進行點分,我們已經把經過點2和點6的所有路徑都已經處理完了,那么子樹又會繼續拆分

2018-12-0616-59-55-752000.png

然后因為子樹大小只有1,重心就是他們自己,繼續和上一層的重心連邊

2018-12-0617-00-42-833000.png

一棵點分樹就這樣建好了

在代碼實現上實際只有一點點小的變動

inline void devide(register int u)
{
    vis[u]=true; 
    int totsize=sizee;
    for(register int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(vis[v])
            continue;
        rt=0,sizee=size[v]>size[u]?totsize-size[u]:size[v],mx=inf;
        getroot(v,0);
        fa[rt]=u;
        devide(rt);
    }
    return res;
}

那么每一次修改,只要在點分樹里不斷往上跳,就能夠維護整棵樹的信息了

好像十分抽象,結合一道例題來講或許會更好

我們以P2056 [ZJOI2007]捉迷藏作為模板來說一下動態點分治

題意簡介:

給你了一棵樹,每個節點有個權值(0/1),一開始所有點全是0

有一下兩種操作:

C(Change)i,把i這個節點的權值取反

G(Game)查詢距離最遠的兩個權值為0的點的距離

題解先咕咕咕


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM