點分治學習筆記

點分治

關於點分治,其實思想是很是好理解的,類比在數列上或是在平面上的分治算法(如歸併排序,平面最近點對等),咱們能夠從字面上理解該算法:c++

以一個點爲界限,將一棵樹分紅若干個子樹,當劃分到必定規模,就對每一個子樹分別進行求解算法

感性理解就行了數組

感覺一個算法最直觀的辦法,就是來看一道模板題。函數

【模板】 點分治

給定一棵有$n$個點的樹,詢問樹上長度爲$k$的鏈是否存在。post


首先能夠很直觀的知道,對於樹上的任意一個點,有不少條通過它的鏈url

那麼,對於本題,咱們是否能夠在可以接受的時間內對這些通過該點的鏈進行求解呢?spa

答案是確定的,只須要以該節點爲根節點,對整顆樹進行一遍$\text{DFS}$,求出各個點到該點的距離,而後就能夠用桶排等方法解決該問題。.net

那麼對於剩下的沒有被處理到的鏈呢?指針

天然,咱們能夠以這個點,將整棵樹斷掉,將它的子樹分開遞歸分治求解,這樣這道題目就解決啦!code

咳咳,真的這麼簡單嗎?

咱們來看一張圖

graph.png

多麼優雅的一條鏈!

若是咱們一開始以$1$爲根節點,按照這個思路,咱們須要進行$n$次操做,這樣確定是不行的。

也就是說,咱們須要找到一個節點,使得在將其斷掉以後,剩下的各個子樹的大小相對均勻,這樣在進行分治求解的時候就可讓時間複雜度最優。

因此這裏須要引入一個新的概念:

樹的重心

定義:樹的重心也叫樹的質心。找到一個點,其全部的子樹中最大的子樹節點數最少,那麼這個點就是這棵樹的重心,刪去重心後,生成的多棵子樹儘量平衡。(摘自百度百科)


那麼如何求樹的重心呢?

咱們能夠採起一種相似於$DP$的算法,由於咱們要使最大的子樹節點數最少,因而咱們能夠任選一個點進行$DFS$,在搜索過程當中,記錄每個點的最大的子樹大小,而後進行操做,即 $$ dp[u]=max(siz[son[u]],sum-siz[u]) $$ $sum$表示這顆子樹一共有多少個節點,$siz[i]$即子樹大小

這樣的話,咱們就只須要在該子樹中找到最小的$dp[i]$,這樣$i$就是咱們要找的重心了。

是否是很簡單?

貼一小段代碼

//root默認爲0,dp[0]=inf
void get_root(int u,int fa,int sum)
{
    dp[u]=0,siz[u]=1;//初始化
    for(int i=head[u];i;i=e[i].nex)
    {
        int v=e[i].to;
        if(v==fa||vis[u]) continue;//vis[u]表示該節點是否被看成根節點操做過,同時保證該函數只在本子樹內操做
        get_root(v,u,sum);
        siz[u]+=siz[v];
        dp[u]=max(dp[u],siz[v]);
    }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[root]) root=u;	
}

那麼,如何統計答案呢?

對於本題,提供$3$種方法供君選擇

說明:$dis[u]$表示$u$節點到重心的距離,$siz[u]$表示以$u$爲根的子樹大小,$root$表示當前子樹重心

$1.$暴力枚舉法

咱們將全部的節點到重心的距離$dis[u]$經過一遍$DFS$記錄下來,而後開一個桶,兩兩組合,統計答案。這樣的話,會有一個問題,就是在同一條路徑上的節點的答案也會被統計,好比$dis[u]+dis[son[u]]=k$,可是這兩個節點並無到重心的一條鏈,因此須要刪去。

那麼如何作呢?

簡單容斥一下就行了 $$ Ans=Ans(以重心爲根的子樹)-\sum Ans(以重心的孩子爲根的子樹) $$ 時間複雜度爲單次$O(siz[root]^2)$,且有必定侷限性——$k$太大時沒法使用

$ 2.$配對法

這是一個在本題跑得飛起的計算方法

假設一共有$son_1,son_2,son_3,...,son_n$這些多棵子樹

令$vis[j]$數組表示在求解到第$i$棵子樹的答案時,前$i-1$棵子樹是否存在到重心長度爲$j$的路徑

這樣一來,咱們就只須要在每棵子樹當中對於每個詢問,枚舉找到能夠湊成答案的路徑便可

時間複雜度爲單次$O(m*siz[root])$,因爲詢問較少,跑的飛起

但注意,在還原數組的時候,須要將

一樣,也有必定的侷限性——$k$太大時一樣沒法使用

$3.$two pointers

維護$l,r$兩個指針,將全部獲得的$dis[i]$從小到大排序,這樣的話,就能夠保證$dis$數組單調遞增,有兩個思路供君選擇:

$1)$直接標記(僅針對本題)

在$DFS$求解$dis[i]$時,能夠記錄每個節點對應來自哪一棵子樹,記爲$tag[i]$而後能夠按照這樣的思路:

令$l=0,r=siz[root]$

若是當前點已有答案,跳過

若是$dis[l]+dis[r]>k$,就$--r$,這樣纔有可能有解

若是$dis[l]+dis[r]<k$,就$++l$,同理

若是$dis[l]+dis[r]=k \quad且\quad tag[l]==tag[r]$,就看$dis[r-1]$的大小,並進行相應調整

若是上述條件都不知足,則對於這個$k$有解


$2)$前綴統計

咱們能夠化等爲不等,記錄$\le k$和$\le k-1$的路徑條數

一樣令$l=0,r=siz[root]$

若$dis[l]+dis[r]<=k$,則說明在$[l+1,r]$的$dis$均可以組成答案,此時$++l$;

不然$--r$;

這種方法一樣須要容斥。

兩種方法的時間複雜度均爲單次$O(m*siz[root])$,且不受$k$的限制,同時這種思想也在很是多的題目上有所運用,如$NOI2019Day1T3$


大致思路就是這樣,共$3$步:

$1.$找樹的重心

$2.$求解通過重心的鏈對答案的貢獻

$3.$在各個子樹內求解

因而這個題目就完結辣OWO~

貼代碼(上面講的很清楚了因而沒有註釋QWQ)

#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int n,m;
struct cc{
    int to,nex,w;
}e[maxn<<2];
int head[maxn],cnt;
int siz[maxn],dp[maxn],vis[maxn],q[maxn],ans[maxn];
void add(int x,int y,int z)
{
    ++cnt;
    e[cnt].to=y;
    e[cnt].nex=head[x];
    e[cnt].w=z;
    head[x]=cnt;
}
int root=0;
void get_root(int u,int fa,int sum)
{
    dp[u]=0,siz[u]=1;
    for(int i=head[u];i;i=e[i].nex)
    {
        int v=e[i].to;
        if(v==fa||vis[v]) continue;
        get_root(v,u,sum);
        siz[u]+=siz[v];
        dp[u]=max(dp[u],siz[v]);
    }
    dp[u]=max(dp[u],sum-siz[u]);
    if(dp[u]<dp[root]) root=u;	
}
int dep[maxn],dis[maxn],tot;
void get_dis(int u,int fa)
{
    dep[++tot]=dis[u];
    for(int i=head[u];i;i=e[i].nex)
    {
        int v=e[i].to;
        if(v==fa||vis[v]) continue;
        dis[v]=dis[u]+e[i].w,get_dis(v,u);
    }
}
void get_ans(int u,int now,int val)
{
    dis[u]=now,tot=0;
    get_dis(u,0);
    stable_sort(dep+1,dep+tot+1);
    for(int i=1;i<=m;++i)
    {
        int s1=0,s2=0,l=1,r=tot;
        while(l<r)
            if(dep[l]+dep[r]<=q[i]) s1+=r-l,++l;
            else --r;
        l=1,r=tot;
        while(l<r)
            if(dep[l]+dep[r]<q[i]) s2+=r-l,++l;
            else --r;
        ans[i]+=(s1-s2)*val;
    }
}
void solve(int u)
{
    get_ans(u,0,1);
    vis[u]=1;
    for(int i=head[u];i;i=e[i].nex)
    {
        int v=e[i].to;
        if(vis[v]) continue;
        get_ans(v,e[i].w,-1);
        root=0;
        get_root(v,u,siz[v]),solve(root);
    }
}
int main()
{
    int a,b,c;
    scanf("%d%d",&n,&m);
    dp[0]=n;
    for(int i=1;i<n;++i)
        scanf("%d%d%d",&a,&b,&c),add(a,b,c),add(b,a,c);
    for(int i=1;i<=m;++i) scanf("%d",&q[i]);
    get_root(1,0,n);solve(root);
    for(int i=1;i<=m;++i)
        printf("%s\n",ans[i]?"AYE":"NAY");
    return 0;
}

Thanks for reading.

相關文章
相關標籤/搜索