樹上差分算法筆記


原文鏈接

樹上差分

算法詳解

算法范圍

樹上差分算法,是一個適用於樹上區間操作的算法.

它是差分數組,前綴和求解的樹上拓展.

眾所周知,樹這類特殊的結構,往往具有很多性質,而樹上差分往往就是結合這些性質,進行高效率的處理.

我們還需要知道一點,樹上差分基本上不會出裸題,往往會和大量的算法結伴出行.

其中,樹上差分通常,99%的可能性與LCA最近公共祖先算法,一起出現在題目.就像熱戀情人一樣

樹上差分,准確來說不是算法,而是一種優秀的思想

算法概念

樹上差分,捕捉關鍵字.

  1. 樹上(體現了適用結構)
  2. 差分(體現了思想本質)

說了跟沒有講一樣,差評

正經臉嬉皮笑臉的我,跟大家好好胡說八道說一下樹上差分,這個有趣變態的算法.

趣聞來也

Acwing是一個大集體,通過網絡空間,聯絡了千萬家庭.

現在有大量的網友們,他們手上有大量的學習資源,想要共享給其他人,賺取一個具有神秘力量的東西.(強勢為接下來的Acwing積分打廣告)

那就是Acwing積分,它無所不能,可以兌換字體,稱號,榮譽,權限,甚至AcwingT恤,雖然現在木有等等,等等更加人性化的東西

我們知道,兩個人他們相互認識,可能隔着很多人.

因此我們不妨認為,Acwing的網友們,他們組成了一棵樹.

二叉樹.png

小A同學,想要把自己的資源傳輸給小B同學,他們需要經過很多人.

本着資源共享的目的,因此路徑上的每一個人,都可以得到一份資源.

小A同學,希望自己手上的資源,越早地提供給小B同學,那么他們顯然要走一條最短路.

也就是我們的Lca路徑.


小A同學是一個資源大戶,他總是時不時給小B同學,傳輸資源.

第一天,小A傳給小B,15yxc老師精品資源,包括算法基礎課,算法競賽進階指南,Leetcode打卡.

第二天,小A傳給小B,7個秦淮岸同學的算法大禮包精品資源.

第三天,小A傳給小B,1Chicago大佬搜索精品資源.

第四天,小A傳給小B,1Corner小仙女的精品並查集資源.

第五天,小A傳給小B,1個林同學的精品貪心資源.

小A同學,小B同學手上的資源數量就是以上總和.

綜上所述,我們發現[a,b]節點,路徑上的所有節點,他們的值都會增加同樣一個值.

顯然每天,肯定不止小A和小B同學傳資源,肯定有很多人,很多次傳輸資源.

假如說,我們的Acwing服務器,使用最普通的算法,那么面對茫茫人海,統計每一個節點的資源數量,增加一個區間的資源數量,肯定會崩潰的.

所以,我們的首席技術官,yxc老師想到了樹上差分算法.


算法流程

我們發現,差分和前綴和在一起,是可以統計每一個節點的數量的.

但是此時我們面臨的是一棵樹,我們該怎么辦呢?

先來定義概念

\[f[i]表示i到i的父親的邊權 \\\\ w[i]表示i的子樹權值之和 \]

樹上差分1.png

觀察這張圖,然后思考一下,我們發現了什么.

樹上差分,竟然就是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 行以同樣的格式給出附加邊。

輸出格式

輸出一個整數表示答案。

數據范圍

\[N \le 100000\\\\ M \le 200000,數據保證答案不超過2^{31}-1 \]

輸入樣例:

4 1 
1 2 
2 3 
1 4 
3 4 

輸出樣例:

3

解題報告

題意理解

這道題目題意比較繞,我們來一步步剖解這道題目.題目解剖學,人體解剖學

  1. 一顆\(n-1\)主要邊,然后增加了\(m\)條附加邊.

  2. 我們只能刪除一條主要邊,一條附加邊,一種邊叫做主要邊,一種邊叫做附加邊.

  3. 要求刪除兩條邊后,這棵樹不再是連通的.

  4. 我們需要統計,有多少種方案可以使得不連通,輸出方案數.


算法解析

附加邊到底有什么用處?

\[對於每一條連接x,y節點的(x,y),其實我們都可以認為這條邊,連接了(x,y)這條路徑上的所有點. \]

當沒有了主要邊的時候,其實附加邊就是我們的主要邊.

\[所以說,附加邊(x,y),就是將樹上x,y之間的路徑上的每條主要邊,都覆蓋了一次. \]

因為當\((x,y)\)路徑上的任意一條主要邊消失后,他都可以成為主要邊,去維護連通性.

因此現在我們的問題模型轉化了.

給定一個\(n-1\)條邊的樹,求每一條樹邊,被非樹邊覆蓋了多少次

  1. 樹邊也就是主要邊
  2. 非樹邊也就是附加邊

那么這就是一個樹上差分統計覆蓋次數問題了.

\[每一條附加邊,使得(x,y)節點的路徑上,每一個節點的權值+1. \]


此時我們的問題,變成了如何統計方案數.

我們來好好地分類討論一下主要邊,身上的附加邊.

\[1.主要邊被覆蓋了0次,即上面只有0條附加邊. \]

我們發現刪除完這條主要邊后,隨意刪除一條附加邊,我們都可以讓樹不連通.也就是\(m\)種方案.

樹上差分2.png

只要刪除\((2,4)\)這條紅邊,那么隨意一條附加邊,都可以滿足條件.

\[2.主要邊覆蓋1次,即上面只有一條附加邊 \]

我們發現刪除完這條主要邊后,我們只能刪除這條主要邊的附加邊.也就是\(1\)種方案.

也就是刪除咱們圖上面的\((3,7)\)紅邊,然后我們只能刪除那條上面的紫色邊.

\[3.主要邊覆蓋大於1次,即上面有多條附加邊 \]

我們發現,怎么刪除,總能連通.於是\(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;
}


免責聲明!

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



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