OI樹上問題 簡單學習筆記



判斷鏈

  • 每個點的度數不超過2

判斷樹

  • n個點,n-1條邊
  • 每兩個點之間的路徑唯一

多叉樹轉換成二叉樹

第一個孩子作為左孩子,第一個孩子的兄弟作為它的右孩子。


最小生成樹

  • 切割性質 假定所有邊權均不相同。設S為既非空集也非全集的V的子集,邊e是滿足一個端點在S內,另一個端點不再S內的所有邊中權值權值最小的一個,則圖G的所有生成樹均包含e。
  • 回路性質。假定所有邊權均不相同。設C為圖G的任意回路,邊e是C上權值最大的邊,則圖G的所有生成樹均不包含e。

增量最小生成樹

從包含的n個點的空圖開始,依次加入m條帶權邊。每加入一條邊,輸出當前圖中最小生成樹權值(如果當前圖不聯通,輸出無解)。
如果加入一條邊(u,v)后,圖中恰好包含一個環,根據回路性質,刪除該葫蘆上權值最大的邊即可,因此只需要在加邊之前的MST中找到u到v唯一路徑上權值最大的邊,再和e比較,刪除權值較大的一條。由於路徑唯一,可以用DFS或者BFS找到這條u到v的路徑,總時間復雜度為\(O(nm)\)

最小瓶頸生成樹

給出加權無向圖,求一個最小生成樹,使得最大邊權值盡量小。
每顆最小生成樹一定是最小瓶頸生成樹,每顆最小瓶頸生成樹卻不一定是最小生成樹

最小瓶頸路

給定加權無向圖的兩個結點u和v,求出從u到v的一條路徑,使得路徑上的最長邊盡量短。
我們直接求出這個圖的最小生成樹,則起點和終點在書上的唯一路徑就是我們要找的路徑,這條路經上的最長邊就是問題的答案。

每對結點間的最小瓶頸路

給出加權無向圖,求每兩個結點u和v之間的最小瓶頸路的最大邊長\(f(u,v)\)
我們先求出來最小生成樹,同時計算\(f(u,v)\),每訪問一個結點u時,考慮所有已經訪問過的老結點x,更新\(f(x,u)=max(f(x,v),w(u,v))\),其中v是u的父親結點。(使用dfs實現上述過程)

次小生成樹

戳我


樹的重心

樹上一點,滿足刪除該點時,樹內剩下的子樹最大節點數最小。

性質

1、樹的重心每棵子樹的大小一定小於等於\(n/2\)

2、每顆子樹的大小都小於等於\(n/2\)的點一定是這棵樹的重心(就是上一個的逆定理)

3、樹中所有點到某個點的距離和中,到重心的距離和最小(如果有兩個重心,他們的距離一樣)
證明:我們考慮使用調整法,設當前最優決策為u點,v為u的任意相鄰節點。記size(x)為當u為整棵樹的根時,以x為根的子樹的節點的大小。
u為全局最優決策當且僅當\(n-size(v)\ge size(v)\),否則最優策略一定在不滿足該條件的v的子樹中。
我們化簡這個式子,即\(size(v)\le n/2\)
由定理2得,該點為樹的重心。

4、兩棵樹通過一條邊相連成為一顆新的樹,新樹重心一定在原來兩棵樹得重心的路徑上。(注意中心不止一個的情況)
例題:cf civilization


怎么找重心?

方法1:處理出每個節點的𝑠𝑖𝑧𝑒,依次枚舉點,模擬刪除該點后各子樹大小,更新最優解。

方法2:采用“調整法”的思想,從一個點出發,調整過去。

兩種方法都是Ο(𝑛)的。


樹的直徑

樹(可帶權)上最長的簡單路徑。
1、一棵樹的直徑可能有若干條,但是有一點顯然——他們一定兩兩相交,不然我們就一定可以找出一條更長的。

2、所有直徑的交集一定非空。因為如果三條直徑兩兩相交。如果他們沒有共同交集,那么就會形成環。然后我們可以一直推廣到所有直徑的情況。

3、以樹上任意一個點作為起點的最長路徑,重點一定是直徑上的一個端點。
在此撈上棟棟小哥哥的證明:

4、對於兩條相交直徑,他們不相交的部分一定對稱。

5、兩棵樹用一條邊合並,新樹的直徑兩端一定是原本兩棵樹直徑四個端點中的兩個。
證明:(1)直徑不經過新邊 這個時候顯然是原本兩條直徑中的一條。否則就不滿足直徑的定義了。(2)直徑經過新邊。 新邊兩端分屬兩棵樹,那么這條直徑的新邊的兩端部分肯定是從這兩個點出發在各自樹中的最長路徑。根據性質3,端點還是四個端點的其中之二。


怎么找直徑

由於性質3,我們可以通過兩次bfs或者dfs來確定直徑——任選一點,通過搜索找到從該點出發的最長路,由性質得到,終點為直徑的一個端點。從該端點出發,再通過搜索找到最長路,由定理得,此終點一定是直徑的另外一個端點。
代碼這樣寫:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define MAXN 300010
using namespace std;
int n,m,k,t;
int head[MAXN<<1],done[MAXN],dis[MAXN];
struct Edge{int nxt,to,dis;}edge[MAXN<<1];
inline void add(int from,int to,int dis)
    {edge[++t].nxt=head[from],edge[t].to=to,edge[t].dis=dis,head[from]=t;}
inline void solve(int x)
{
    done[x]=1;
    for(int i=head[x];i;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(!done[v])
        {
            dis[v]=dis[x]+edge[i].dis;
            solve(v);
        }
    }
}
int main()
{
    #ifndef ONLINE_JUDGE
    freopen("ce.in","r",stdin);
    #endif
    scanf("%d%d",&n,&k);
    for(int i=1;i<n;i++)
    {
        int x,y,w;
        scanf("%d%d%d",&x,&y,&w);
        add(x,y,w),add(y,x,w);
    }
    solve(41);
    int maxx=-0x3f3f3f3f,pos;
    for(int i=1;i<=n;i++) 
        if(dis[i]>maxx)
            maxx=dis[i],pos=i;
    memset(dis,0,sizeof(dis));
    memset(done,0,sizeof(done));
    solve(pos);
    maxx=-0x3f3f3f3f,pos;
    for(int i=1;i<=n;i++)
        if(dis[i]>maxx)
            maxx=dis[i],pos=i;
    printf("%d\n",maxx);
    return 0;
}

但是要注意!這種方法對於有負權的樹,求直徑是錯的!!

不過還有一種肯定是正確的算法——樹形DP

inline void dp(int x)
{
    done[x]=1;
    for(int i=head[x];i;i=edge[i].nxt)
    {
        int v=edge[i].to;
        if(done[v]) continue;
        dp(v);
        ans=max(ans,d[x]+d[v]+edge[i].dis);
        d[x]=max(d[x],d[v]+edge[i].dis);
    }
}

遍歷序列

歐拉序

從根節點開始dfs遍歷樹——在點x時,走到一個未遍歷過的兒子,或者兒子已經全部遍歷過從x返回到父親時,以此法得到的遍歷序列是歐拉序。

DFS序

從根節點開始dfs遍歷樹,一個節點第一次被遍歷到時加入到序列中,以此法得到的遍歷序列是dfs序。

括號序

從根節點開始dfs遍歷樹,一個節點第一次被遍歷或者遍歷完兒子要退出時將其加入到序列中,以此法得到的遍歷序列是括號序。

應用

1、將樹中子樹表示為遍歷序列的一段區間。(括號序)

2、判斷一個點是否在以另外一個點為根的子樹里。
(設一個點x在dfs序列中位置為dfn(x),那么如果點y在以x為根的子樹離=>\(dfn(x)\le dfn(y)<dfn(x)+size(x)\)

3、和2等價的,還可以推出判斷點x是否在點y到根節點的路徑上(只要判斷不在子樹里就可以了)

4、再考慮這樣的一類詢問,求點x到根路徑上所有點的權值和,且存在修改點權操作。
顯然,每個點的權值,對以該點為根的子樹的所有點都有貢獻,所以每次修改操作(初始化時給點賦值也看成修改)就可以將以該點為根的子樹所有點答案加上修改的值。
處理時我們可以開一顆全局線段樹,每個點在其中的下標就是該點的dfs序,值為該點的權值。
求答案時就是單點查詢該點在線段樹中的權值,修改時就是對整棵子樹進行區間修改。
但是還有更簡單的方法!我們考慮括號序列。
我們開一個大小為2n的數組,點x在位置\(arr_x\)插入權值,從位置\(lea_x\)插入權值的相反數,做一次前綴和后,設\(sum_i\)為前i項的和,顯然點x到根節點的路徑權值就是\(sum_{arr_x}\)
維護這個可以修改的前綴和數組可以用樹狀數組qwq


樹鏈剖分(輕重鏈剖分)

定義:

1.𝑠𝑖𝑧𝑒(𝑢)為節點𝑢為根的子樹的節點大小。
2.令𝑣是𝑢的兒子中𝑠𝑖𝑧𝑒(𝑣)最大的兒子(如果有多個,則任選一個),則稱邊(𝑢,𝑣)為重邊,𝑣為𝑢的重兒子。
3.重路徑:一條路徑為重路徑,當且僅當這條路徑全由重邊組成。
4.輕邊:令𝑤是𝑢的兒子(𝑤≠𝑢),那么稱邊(𝑢,𝑤)為輕邊(即使𝑠𝑖𝑧𝑒(𝑣)=𝑠𝑖𝑧𝑒(𝑤)),𝑤為𝑢的輕兒子。

算法步驟

第一步,dfs出每個點的size。
第二步,找出每個點的重邊,給點重新編號(DFS序),並將重邊連為重鏈。
第三步,建立一個以新編號為下標的數據結構(一般都是線段樹吧),維護樹上的信息。

啊。。。。其他的東西去參考專門的博文吧qwq,我這里又不是講樹鏈剖分的。我的初衷是整理給自己看的

時間復雜度

單點修改和查詢\(O(log(n))\),路徑修改和查詢為\(O(log(n)^2)\)

性質

1、如果v是u的兒子,且\((u,v)\)是一條輕邊,那么\(size(v)<\frac{size(u)}{2}\)

2、令𝑙𝑖𝑔ℎ𝑡𝑑𝑒𝑝𝑡ℎ(𝑢)表示從𝑢到根經過的輕邊個數
則有\(𝑙𝑖𝑔ℎ𝑡𝑑𝑒𝑝𝑡ℎ(𝑢)≤𝑙𝑜𝑔_2^𝑛\)
由定理8,如果經過了一條輕邊,當前子樹點的個數至多變為原來的一半。最初子樹的點為𝑛,𝑢的子樹的點的個數至少為1,所以至多經過logn條輕邊。
由這個引理,我們也可以得知,每個點到根的路徑上的輕邊和重路徑條數都不超過logn。


dsu on tree

本質是樹上啟發式合並,可以解決多數無修改的子樹查詢問題。
例如:每個點有一個顏色,詢問每個子樹中顏色種類數。

做法

首先,對每個點求出重兒子和輕兒子,維護一個顏色為下標的桶,開始dfs,假設當前到點x我們先將x的輕兒子都遞歸,每次退出遞歸時把桶都清空。
最后在遞歸x的重兒子,返回時無需清空。 然后再將除重兒子的部分加進來,即可得到x子樹的桶。

時間復雜度?

考慮一個點暴力加入和暴力清空的次數,顯然和它到輕邊的數量有關,那么一個點只會有log次。


kruskal重構樹

參考自niick dalao的博客 傳送門
類似kruskal算法,先將邊權排序,然后對於兩個不在一個並查集內的節點,我們新建節點,該點點權為這條邊的邊權,並把這兩個點向它連。之后更新它們的父親。
這棵樹是以最后建立的節點為根的有根樹,如果原圖不連通,那么就遍歷一遍,找到每個並查集的根作為這個森林中對應樹的根。

性質(由開始對邊的排序決定)

  • 是一個大/小根堆
  • 兩個節點的lca的權值是原圖中其之間路徑上的最大邊權的最小值(或者最小邊權的最大值)

長鏈剖分

選擇最深的子樹進行剖分。
與重鏈剖分類似,對於樹高最高的兒子子樹,稱為長兒子,多個仍選擇一個,其余都是短兒子。

經典應用是求K級祖先和一些和樹上的深度有關的題目;

博客參考:zzq dalao的博客 戳我


點分治

咕咕咕qwq不想寫總結了qwq


LCT

性質(摘自flashhu dalao)博客鏈接:戳我

  • 每一個Splay維護的是一條從上到下按在原樹中深度嚴格遞增的路徑,且中序遍歷Splay得到的每個點的深度序列嚴格遞增。
  • 每個節點包含且僅包含於一個Splay中
  • 邊分為實邊和虛邊,實邊包含在Splay中,而虛邊總是由一棵Splay指向另一個節點(指向該Splay中中序遍歷最靠前的點在原樹中的父親)。
    因為性質2,當某點在原樹中有多個兒子時,只能向其中一個兒子拉一條實鏈(只認一個兒子),而其它兒子是不能在這個Splay中的。
    那么為了保持樹的形狀,我們要讓到其它兒子的邊變為虛邊,由對應兒子所屬的Splay的根節點的父親指向該點,而從該點並不能直接訪問該兒子(認父不認子)。

LCT解決動態DP


虛樹

其實就是對樹上的信息進行了簡化的一種方法。對於一棵樹,如果我們提前知道一些詢問點,那么我們可以考慮只保留根節點,詢問點,以及他們之間的LCA。
先貼兩個感覺講的不錯的鏈接:1 2
例題:SDOI2011消耗戰
yyb神犇的題解


prufer序列

請看這個

我們定義葉子結點為度數為1的節點。

將無根樹轉換成prufer序列的方法:

每次尋找一個最小的葉子結點,把與它相連的點放入prufer序列里,然后從樹上刪掉這個點以及它相連的邊。直到剩下兩個點為止。

將prufer序列轉換成無根樹的做法:

弄一個序列A:{1,2,...,n}(全排列),然后我們每次在A中尋找編號最小且沒有在prufer序列中出現的點,將它與prufer序列中的第一個點連邊,然后將這兩個點分別從A和prufer序列中刪掉。最后A中會剩下兩個點,將它們連邊即可。


最小樹形圖(最小有向生成樹)

給定一個有向帶權圖G和其中一個節點u,找出一個以u為根節點,權和最小的有向生成樹。這個生成樹滿足:

  • 恰好有一個入度為0的點,稱為根節點。
  • 其他節點的入度均為1.
  • 可以從根節點到達所有其他節點

算法:朱劉算法
就是先找出來前n-1條最小的彼岸,然后如果沒有環就結束,有環就縮點繼續重復上述過程。

inline bool solve(int n,int m,int root)
{
    ans=0;
    while(233)
    {
        int cnt=0;
        for(int i=1;i<=n;i++) id[i]=top[i]=fa[i]=0,minn[i]=INF;
        for(int i=1;i<=m;i++)
        {
            int u=edge[i].u,v=edge[i].v;
            if(u!=v&&edge[i].dis<minn[v])
                fa[v]=u,minn[v]=edge[i].dis; 
        }
        minn[root]=0;
        for(int i=1;i<=n;i++)
        {
            if(minn[i]==INF) return false;
            ans+=minn[i];
            int u;
            for(u=i;u!=root&&top[u]!=i&&!id[u];u=fa[u]) top[u]=i;
            if(u!=root&&!id[u])
            {
                id[u]=++cnt;
                for(int v=fa[u];v!=u;v=fa[v]) id[v]=cnt;
            }
        }
        if(!cnt) return true;
        for(int i=1;i<=n;i++) 
            if(!id[i])
                id[i]=++cnt;
        for(int i=1;i<=m;i++)
        {
            int u=edge[i].u,v=edge[i].v;
            int last=minn[v];
            if((edge[i].u=id[u])!=(edge[i].v=id[v]))
                edge[i].dis-=last;
        }
        n=cnt,root=id[root];
    }
}

矩陣樹定理:

外向樹:邊的方向為根->葉子
內向樹:邊的方向為葉子->根

上三角矩陣:只有主對角線及其上方的位置有值的行列式,主對角線以下部分都是0;
行列式的求值:用高斯消元把這個行列式小成一個上三角矩陣的形式,然后直接把對角線上面的數乘起來就是這個行列式的值.
余子式:一個行列式的余子式就是這個行列式去掉一行一列后剩下的那個少了一維的行列式.

基爾霍夫矩陣:度數矩陣-鄰接矩陣

無向圖的生成樹計數:該圖的基爾霍夫矩陣的任意一個余子式的行列式的值.
有向圖的外向樹計數:基爾霍夫矩陣換成入度矩陣-鄰接矩陣
有向圖的內向樹計數:基爾霍夫矩陣換成出度矩陣-鄰接矩陣
有向圖中的計數,余子式去掉的行列不能任意了,應該的根節點對應的那一行一列
注意如果有重邊的時候,鄰接矩陣可是記錄的是兩個點之間邊的條數,而不是0/1(其實這是變元矩陣樹定理)

變元矩陣樹定理:對於有重邊+邊的權值有可能不為1的,我們把鄰接矩陣換成邊權值之和,然后按照上面的方法求出來的是是所有矩陣樹的邊權積之和.(所以說如果計算有重邊的,我們可以直接設邊為1,然后來達到計數的目的)


哈夫曼樹

相關題目推薦 荷馬史詩

就是一個最優檢索的二叉樹(一般都是二叉的),滿足它的葉子節點*深度總和最小.

然后還有哈夫曼編碼:構造的方式是在哈夫曼樹上,連接左節點的邊賦成0,連接右節點的邊賦成,然后從根到葉子節點的所有數連起來,就是該葉子節點的哈夫曼編碼.哈夫曼編碼有一個特性,就是兩兩之間一定不會出現前綴關系

如何構造哈夫曼樹?其實就是一個貪心的思想.把所有葉子節點都放進堆里,權值為出現次數(檢索次數).我們每次選取兩個權值最小的點,然后將它們合並(合並意為新開一個節點做它們的父親,然后權值為它們的和).然后一路合並上去,直到只剩下一個為止.

為什么這樣子是最優的呢?因為我們的貪心策略,保證了次數小的一定深度最低.

K叉哈夫曼樹:就是每次選取K個,然后合並.但是需要注意一點的是,最后一次合並的時候可能不足K個,這樣的話,如果根的子節點有空的話,顯然不是最優結果,所以我們要計算一下,在合並開始前往隊列里面添加值為0的節點,補夠空缺

對於同一個問題,可能有很多種哈夫曼樹的形態.如果要求深度最小,合並的時候還需要按照權值為第一關鍵字,深度為第二關鍵字(從小到大)選取.



免責聲明!

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



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