樹上差分
算法詳解
算法范圍
樹上差分算法,是一個適用於樹上區間操作的算法.
它是差分數組,前綴和求解的樹上拓展.
眾所周知,樹這類特殊的結構,往往具有很多性質,而樹上差分往往就是結合這些性質,進行高效率的處理.
我們還需要知道一點,樹上差分基本上不會出裸題,往往會和大量的算法結伴出行.
其中,樹上差分通常,99%的可能性與LCA最近公共祖先算法,一起出現在題目.就像熱戀情人一樣
樹上差分,准確來說不是算法,而是一種優秀的思想
算法概念
樹上差分,捕捉關鍵字.
- 樹上(體現了適用結構)
- 差分(體現了思想本質)
說了跟沒有講一樣,差評
正經臉嬉皮笑臉的我,跟大家好好胡說八道說一下樹上差分,這個有趣變態的算法.
趣聞來也
Acwing是一個大集體,通過網絡空間,聯絡了千萬家庭.
現在有大量的網友們,他們手上有大量的學習資源,想要共享給其他人,賺取一個具有神秘力量的東西.(強勢為接下來的Acwing積分打廣告)
那就是Acwing積分,它無所不能,可以兌換字體,稱號,榮譽,權限,甚至AcwingT恤,雖然現在木有等等,等等更加人性化的東西
我們知道,兩個人他們相互認識,可能隔着很多人.
因此我們不妨認為,Acwing的網友們,他們組成了一棵樹.
小A同學,想要把自己的資源傳輸給小B同學,他們需要經過很多人.
本着資源共享的目的,因此路徑上的每一個人,都可以得到一份資源.
小A同學,希望自己手上的資源,越早地提供給小B同學,那么他們顯然要走一條最短路.
也就是我們的Lca路徑.
小A同學是一個資源大戶,他總是時不時給小B同學,傳輸資源.
第一天,小A傳給小B,15個yxc老師精品資源,包括算法基礎課,算法競賽進階指南,Leetcode打卡.
第二天,小A傳給小B,7個秦淮岸同學的算法大禮包精品資源.
第三天,小A傳給小B,1個Chicago大佬的搜索精品資源.
第四天,小A傳給小B,1個Corner小仙女的精品並查集資源.
第五天,小A傳給小B,1個林同學的精品貪心資源.
小A同學,小B同學手上的資源數量就是以上總和.
綜上所述,我們發現[a,b]節點,路徑上的所有節點,他們的值都會增加同樣一個值.
顯然每天,肯定不止小A和小B同學傳資源,肯定有很多人,很多次傳輸資源.
假如說,我們的Acwing服務器,使用最普通的算法,那么面對茫茫人海,統計每一個節點的資源數量,增加一個區間的資源數量,肯定會崩潰的.
所以,我們的首席技術官,yxc老師想到了樹上差分算法.
算法流程
我們發現,差分和前綴和在一起,是可以統計出每一個節點的數量的.
但是此時我們面臨的是一棵樹,我們該怎么辦呢?
先來定義概念
觀察這張圖,然后思考一下,我們發現了什么.
樹上差分,竟然就是DFS序列構成區間的一種差分體現.
我們知道DFS序列,其實就是子樹區間.如果不懂,歡迎看秦淮岸的搜索專題講解,里面有DFS序的講解
那么樹上差分,其實就這么巧妙地轉化為了區間差分一樣的套路.
在這里,我們只討論邊上差分,暫時不討論點上差分.
好題選講
題目描述
傳說中的暗之連鎖被人們稱為 Dark。
Dark 是人類內心的黑暗的產物,古今中外的勇者們都試圖打倒它。
經過研究,你發現 Dark 呈現無向圖的結構,圖中有 N 個節點和兩類邊,一類邊被稱為主要邊,而另一類被稱為附加邊。
Dark 有 N – 1 條主要邊,並且 Dark 的任意兩個節點之間都存在一條只由主要邊構成的路徑。
另外,Dark 還有 M 條附加邊。
你的任務是把 Dark 斬為不連通的兩部分。
一開始 Dark 的附加邊都處於無敵狀態,你只能選擇一條主要邊切斷。
一旦你切斷了一條主要邊,Dark 就會進入防御模式,主要邊會變為無敵的而附加邊可以被切斷。
但是你的能力只能再切斷 Dark 的一條附加邊。
現在你想要知道,一共有多少種方案可以擊敗 Dark。
注意,就算你第一步切斷主要邊之后就已經把 Dark 斬為兩截,你也需要切斷一條附加邊才算擊敗了 Dark。
輸入格式
第一行包含兩個整數 N 和 M。
之后 N – 1 行,每行包括兩個整數 A 和 B,表示 A 和 B 之間有一條主要邊。
之后 M 行以同樣的格式給出附加邊。
輸出格式
輸出一個整數表示答案。
數據范圍
輸入樣例:
4 1
1 2
2 3
1 4
3 4
輸出樣例:
3
解題報告
題意理解
這道題目題意比較繞,我們來一步步剖解這道題目.題目解剖學,人體解剖學
-
一顆\(n-1\)條主要邊的樹,然后增加了\(m\)條附加邊.
-
我們只能刪除一條主要邊,一條附加邊,一種邊叫做主要邊,一種邊叫做附加邊.
-
要求刪除兩條邊后,這棵樹不再是連通的.
-
我們需要統計,有多少種方案可以使得不連通,輸出方案數.
算法解析
附加邊到底有什么用處?
當沒有了主要邊的時候,其實附加邊就是我們的主要邊.
因為當\((x,y)\)路徑上的任意一條主要邊消失后,他都可以成為主要邊,去維護連通性.
因此現在我們的問題模型轉化了.
給定一個\(n-1\)條邊的樹,求每一條樹邊,被非樹邊覆蓋了多少次
- 樹邊也就是主要邊
- 非樹邊也就是附加邊
那么這就是一個樹上差分統計覆蓋次數問題了.
此時我們的問題,變成了如何統計方案數.
我們來好好地分類討論一下主要邊,身上的附加邊.
我們發現刪除完這條主要邊后,隨意刪除一條附加邊,我們都可以讓樹不連通.也就是\(m\)種方案.
只要刪除\((2,4)\)這條紅邊,那么隨意一條附加邊,都可以滿足條件.
我們發現刪除完這條主要邊后,我們只能刪除這條主要邊的附加邊.也就是\(1\)種方案.
也就是刪除咱們圖上面的\((3,7)\)紅邊,然后我們只能刪除那條上面的紫色邊.
我們發現,怎么刪除,總能連通.於是\(0\)種方案.
代碼解析
#include <bits/stdc++.h>
using namespace std;
const int N=100000+200;
int n,m,ans;
struct LCA
{
int head[N<<1],Next[N<<1],edge[N<<1],ver[N<<1],tot;
int deep[N],fa[N][22],lg[N],date[N];
inline void init()
{
memset(head,0,sizeof(head));
memset(deep,0,sizeof(deep));
tot=0;
}
inline void add_edge(int a,int b,int c)
{
edge[++tot]=b;
ver[tot]=a;
Next[tot]=head[a];
head[a]=tot;
}
inline void dfs(int x,int y)
{
deep[x]=deep[y]+1;//深度是父親節點+1
fa[x][0]=y;//2^0=1,也就是父親節點
for(int i=1; (1<<i)<=deep[x]; i++) //2^i<=deep[x]也就是別跳出根節點了
fa[x][i]=fa[fa[x][i-1]][i-1];
for(int i=head[x]; i; i=Next[i]) //遍歷所有的出邊
if (edge[i]!=y)//避免回到父親節點
dfs(edge[i],x);//自己的兒子節點, 自己是父親節點
return ;
}
inline int Lca(int x,int y)//Lca過程
{
if (deep[x]<deep[y])//x節點默認深度深一些,在下面
swap(x,y);//交換
while(deep[x]>deep[y])//還沒有同一高度
x=fa[x][lg[deep[x]-deep[y]]-1];//往上面跳躍,deep[x]-deep[y]是高度差.-1是為了防止deep[x]<deep[y]
if(x==y)//意外發現,y就是(x,y)的Lca
return x;
for(int i=lg[deep[x]]; i>=0; i--)
if (fa[x][i]!=fa[y][i])//沒有跳到Lca
{
x=fa[x][i];//旋轉跳躍
y=fa[y][i];//我閉着眼
}
return fa[x][0];//父親節點就是Lca,因為本身是離着Lca節點最近的節點,也就是Lca的兒子節點.
}
inline int query(int x,int f)//f是x節點的父親節點
{
for(int i=head[x]; i; i=Next[i]) //所有出邊
{
int j=edge[i];//出邊
if (j!=f)//不是父親節點
{
query(j,x);//訪問兒子節點
date[x]+=date[j];//累加子樹節點的值
if(date[j]==0)
ans+=m;
else if(date[j]==1)
ans++;
}
}
}
inline int update(int x,int y)//修改操作,x,y節點構成的路徑統一+1
{
date[x]++;
date[y]++;
date[Lca(x,y)]-=2;
}
} g1;
int main()
{
scanf("%d%d",&n,&m);
g1.init();
for(int i=1; i<n; i++)
{
int a,b;
scanf("%d%d",&a,&b);
g1.add_edge(a,b,0);//加邊
g1.add_edge(b,a,0);//無向圖
}
for(int i=1; i<=n; i++)
g1.lg[i]=g1.lg[i-1]+(1<<g1.lg[i-1]==i);//處理log數組的關系
g1.dfs(1,0);
for(int i=1; i<=m; i++)
{
int a,b;
scanf("%d%d",&a,&b);
g1.update(a,b);//附加邊
}
g1.query(1,0);
printf("%d\n",ans);
return 0;
}