差分數組及樹上差分html
所謂差分,就是記錄當前的元素與以前元素邏輯上的差距。算法
最基礎的用法是差的差分數組:數組
記錄當前位置的數與上一位置的數的差值。ide
即b[i]=a[i]-a[i-1] (b爲差分數組,a爲原數組)post
經過對差分數組求前綴和,能夠求出原數組,即:優化
甚至能夠求出前綴和:url
(s爲原數組,sum爲原數組的前綴和數組,b爲差分數組)spa
能夠O(1)優化區間加法:給原數組區間[l,r]的數都加上x,只要在b l處加x,b r+1 處減x。3d
有兩種理解角度:code
一、從差分定義出發,區間加x使區間左端點與它在原數組上一個數的差距加大了x、使區間右端點的後一個數與區間右端點的數的差距縮小了x,而沒有改變區間中相鄰2數的差距。
二、從差分數組的修改對原數組的影響入手:因爲差分數組求前綴和得出原數組,當b l加x以後求前綴和,那麼原數組自l及之後的數所有比b l加x以前多了x;同理當b r+1減x以後求前綴和,那麼原數組自r+1及之後的數所有比b r+1減x以前少了x。總的一看,發現原數組l~r的部分就多了x,其他部分沒有變化。
廣義差分:差分維護的是相鄰元素間的邏輯關係,從而使能從初始狀態(a[0])經過差分數組表達的邏輯關係推出某個位置上a的值(從形式上看就是求前綴)。而這種差距不僅限於減法的差,還有異或等等。不過通常這種關係應可交換(即對順序的要求不嚴格)且對於運算來講有單位元(麼元)(或通常化的話就是要能有互相抵消的方法)
樹上差分:將差分搬到了樹上。能夠有兩個差分方向:
一、記錄當前節點與父節點的邏輯關係,查詢時從上往下求前綴。(不經常使用,由於在每次路徑修改時都要修改一下當前節點的全部子節點,時間、程序複雜度都很高,沒有靈魂的差分(不能O(1)實現路徑修改))
二、記錄當前節點與它全部子節點總和的邏輯關係,查詢時dfs求子樹和(或是說以向上爲正方向的求前綴)。(路徑修改時只要修改一下路徑起始點和lca(有時還有lca的父親),有了靈魂的差分(可O(1)實現路徑修改),很經常使用)
樹上差分分爲點差分和邊差分,不論哪一種差分,差分數組的意義都是當前節點與它兒子節點總和的差距(這裏爲當前點(或點上的邊)被路徑通過次數與它的兒子節點(或其上的邊)被路徑通過次數總和的差,每次新增一個路徑,即要求實現路徑修改時,起始點與兒子們的差會多一,路徑中中間的點與兒子們的差不變。點差分時,lca會比兒子們少1,lca的父親會比兒子們少1;邊差分時,lca會比兒子們少二。用這些邏輯關係從葉子向上推時,若當前點的兒子們的值都是對的,那它也是對的。邊界狀況就是葉子結點,顯然是它的值對的,故可經過回溯推出整個樹的值。這樣對差分概念的理解有深刻了:差分的結構不知限於線性的數組)
(這裏的基礎講解引用自大佬的博客)
前置知識:
須要知道的樹的性質:
一、樹上任意兩個點的路徑惟一.
二、任何子節點的父親節點惟一.(能夠認爲根節點是沒有父親的)
樹上差分的兩種基本操做用到了LCA,不瞭解LCA的話能夠去這裏面學一下
思想
類比於差分數組,樹上差分利用的思想也是前綴和思想.(在這裏應該是子樹和思想.
當咱們記錄樹上節點被通過的次數,記錄某條邊被通過的次數的時候.
若是每次強制dfs去標記的話,時間複雜度將高到爆炸!
所以咱們引入了樹上差分!
與樹上差分在一塊兒的使用的是 DFS ,由於在回溯的時候,咱們能夠計算出子樹的大小.
(這個應該不用過多解釋
定義數組
cnti 爲節點i被通過的次數.
基本操做
1.點的差分
這個比較簡單,因此先講這個qwq
例如,咱們從s−−>t ,求這條路徑上的點被通過的次數.
很明顯的,咱們須要找到他們的LCA,(由於這個點是中轉點啊qwq.
咱們須要讓cnt s++ ,讓 cnt t++,而讓他們的cnt lca−−,cnt faher(lca)−− ;
可能讀着會有些難理解,因此我準備了一個圖qwq。綠色的數字表明通過次數.
直接去標記的話,可能會T到不行,可是咱們如今在講啥?樹上差分啊!
根據剛剛所講,咱們的標記應該是這樣的↓
考慮:咱們搜索到s,向上回溯.
下面以 u 表示當前節點, soni 表明i的兒子節點.(若是一些 son 不給出下標,即表明當前節點 u 的兒子
每一個 u 統計它的子樹大小,順着路徑標起來.(即cnt u+=cnt son )
咱們會發現第一次從s回溯到它們的LCA時候,cnt LCA+=cnt[sonLCA]
cntLCA=0 ! "不是LCA會被通過一次嘛,爲何是0!"
別急,咱們繼續搜另外一邊.
繼續:咱們搜索到t,向上回溯.
依舊統計每一個u的子樹大小 cnt u+=cnt son
再度回到 LCA 依舊 是 cntLCA+=cnt[sonLCA]
這個時候cntLCA=1 這就達到了咱們要的效果 (是否是特別優秀 ( • ̀ω•́ )✧
擔心: 萬一咱們再從 LCA 向上回溯的時候使得其父親節點的子樹和爲1怎麼辦?
這樣咱們不就使得其父親節點被通過了一次? 所以咱們須要在cnt faher(lca)−−
這樣就達到了標記咱們路徑上的點的要求! 厲不厲害 (o゚▽゚)o tql!!
2.邊的差分
既然咱們已經get到了點的差分,那麼咱們邊的差分也是很簡單啦!
機房某dalao:"這不和點差分標記方式同樣嗎?不就是把邊塞給點嗎? 看我切了它!"
爲這位大佬默哀一下 qwq.
的確,咱們對邊進行差分須要把邊塞給點,可是,這裏的標記並非同點差分同樣.
PS: 把邊塞給點的話,是塞給這條邊所連的深度較深的節點. (即塞給兒子節點
先請你們思考 5s ……
好,時間到,有沒有想到如何標記?(只要畫圖模擬一下就能夠啦! 上圖! 紅色邊爲須要通過的邊,綠色的數字表明通過次數
正常的話,咱們的圖是這樣的.↓
可是因爲咱們把邊塞給了點,所以咱們的圖應該是這樣的↓
可是根據咱們點差分的標記方式來看的話顯然是行不通的,
不然atherLCA−−>LCA 這一路徑也會被標記爲通過了1次
所以考慮如何標記咱們的點,來達到通過紅色邊的狀況
聰明的你必定想到了,這樣來標記
cnts++ ,cntt++ ,cntLCA−=2
這樣回溯的話,咱們便可只通過圖中紅色邊啦!(這裏就不詳細解釋啦,原理其實相同 qwq
把邊塞入點中的代碼這樣寫.qwq(順便在搜索的時候處理便可
1 前置知識 2 須要知道的樹的性質: 3
4 樹上任意兩個點的路徑惟一. 5
6 任何子節點的父親節點惟一.(能夠認爲根節點是沒有父親的) 7
8 若是你認爲你知道了這些你就能秒切這些樹上差分的題,那你就過低估這個東西了!
9
10 樹上差分的兩種基本操做用到了LCA,不瞭解LCA的話能夠去這裏面學一下 11
12 思想 13 類比於差分數組,樹上差分利用的思想也是前綴和思想.(在這裏應該是子樹和思想. 14
15 當咱們記錄樹上節點被通過的次數,記錄某條邊被通過的次數的時候. 16
17 若是每次強制dfs去標記的話,時間複雜度將高到爆炸!
18
19 所以咱們引入了樹上差分!
20
21 與樹上差分在一塊兒的使用的是 DFSDFS ,由於在回溯的時候,咱們能夠計算出子樹的大小. 22
23 (這個應該不用過多解釋 24
25 定義數組 26 cnt_icnt 27 i 28 爲節點i被通過的次數. 29
30 基本操做 31 1.點的差分 32 這個比較簡單,因此先講這個qwq 33
34 例如,咱們從 s-->ts−−>t ,求這條路徑上的點被通過的次數. 35
36 很明顯的,咱們須要找到他們的LCA,(由於這個點是中轉點啊qwq. 37
38 咱們須要讓 cnt_s++cnt 39 s 40 ++ ,讓 cnt_t++cnt 41 t 42 ++ ,而讓他們的 cnt_{lca}--cnt 43 lca 44 −− , cnt_{faher(lca)}--cnt 45 faher(lca) 46 −− ; 47
48 可能讀着會有些難理解,因此我準備了一個圖qwq 49
50 綠色的數字表明通過次數. 51
52
53
54 直接去標記的話,可能會T到不行,可是咱們如今在講啥?樹上差分啊!
55
56 根據剛剛所講,咱們的標記應該是這樣的↓ 57
58
59
60 考慮:咱們搜索到s,向上回溯. 61
62 下面以 uu 表示當前節點, son_ison 63 i 64 表明i的兒子節點.(若是一些 sonson 不給出下標,即表明當前節點 uu 的兒子 65
66 每一個 uu 統計它的子樹大小,順着路徑標起來.(即 cnt_u+=cnt_{son}cnt 67 u 68 +=cnt 69 son 70 ) 71
72 咱們會發現第一次從s回溯到它們的LCA時候, cnt_{LCA}+=cnt[son_{LCA}]cnt 73 LCA 74 +=cnt[son 75 LCA 76 ] 77
78 cnt_{LCA}=0cnt 79 LCA 80 =0 ! "不是LCA會被通過一次嘛,爲何是0!"
81
82 別急,咱們繼續搜另外一邊. 83
84 繼續:咱們搜索到t,向上回溯. 85
86 依舊統計每一個u的子樹大小 cnt_u+=cnt_{son}cnt 87 u 88 +=cnt 89 son 90 91
92 再度回到 LCALCA 依舊 是 cnt_{LCA}+=cnt[son_{LCA}]cnt 93 LCA 94 +=cnt[son 95 LCA 96 ] 97
98 這個時候 cnt_{LCA}=1cnt 99 LCA 100 =1 這就達到了咱們要的效果 (是否是特別優秀 ( • ̀ω•́ )✧ 101
102 擔心: 萬一咱們再從 LCALCA 向上回溯的時候使得其父親節點的子樹和爲1怎麼辦?
103
104 這樣咱們不就使得其父親節點被通過了一次? 所以咱們須要在 cnt_{faher(lca)}--cnt 105 faher(lca) 106 −− 107
108 這樣就達到了標記咱們路徑上的點的要求! 厲不厲害 (o゚▽゚)o tql!!
109
110 這樣點的差分應該沒什麼問題了吧 ,有問題能夠問個人哦 qwq (若是我會的話.) 111
112 2.邊的差分 113 既然咱們已經get到了點的差分,那麼咱們邊的差分也是很簡單啦!
114
115 機房某dalao:"這不和點差分標記方式同樣嗎?不就是把邊塞給點嗎? 看我切了它!"
116
117 爲這位大佬默哀一下 qwq. 118
119 的確,咱們對邊進行差分須要把邊塞給點,可是,這裏的標記並非同點差分同樣. 120
121 PS: 把邊塞給點的話,是塞給這條邊所連的深度較深的節點. (即塞給兒子節點 122
123 先請你們思考 5s5s 124
125 \vdots⋮ 126
127 \vdots⋮ 128
129 \vdots⋮ 130
131 好,時間到,有沒有想到如何標記?(只要畫圖模擬一下就能夠啦! 上圖!
132
133 紅色邊爲須要通過的邊,綠色的數字表明通過次數 134
135 正常的話,咱們的圖是這樣的.↓ 136
137
138
139 可是因爲咱們把邊塞給了點,所以咱們的圖應該是這樣的↓ 140
141
142
143 可是根據咱們點差分的標記方式來看的話顯然是行不通的, 144
145 這樣的話咱們會通過 father_{LCA}--> LCAfather 146 LCA 147 −−>LCA 這一路徑. 148
149 所以考慮如何標記咱們的點,來達到通過紅色邊的狀況 150
151 聰明的你必定想到了,這樣來標記 152
153 cnt_s++cnt 154 s 155 ++ , cnt_t ++cnt 156 t 157 ++ , cnt_{LCA}-=2cnt 158 LCA 159 −=2
160
161 這樣回溯的話,咱們便可只通過圖中紅色邊啦!(這裏就不詳細解釋啦,原理其實相同 qwq 162
163 把邊塞入點中的代碼這樣寫.qwq(順便在搜索的時候處理便可 164
165 void dfs(int u,int fa,int dis) 166 { 167 //u爲當前節點,fa爲當前節點的父親節點,dis爲從fa通向u的邊的邊權.
168 depth[u]=depth[fa]+1; 169 f[u][0]=fa;//相信寫過倍增LCA的人都能看懂.
170 init[u]=dis;//這裏是將邊權賦給點.
171 for(int i=1;(1<<i)<=depth[u];i++)f[u][i]=f[f[u][i-1]][i-1];//預處理倍增數組.
172 for(int i=head[u];i;i=edge[i].u) 173 { 174 if(edge[i].v==fa)continue; 175 dfs(edge[i].v,u,edge[i].w); 176 } 177 //這個每一個人的寫法不同吧. 178 //因此根據每一個人的代碼風格不同,碼出來的也不同
179 }
最後總結一下:
差分維護元素與它前面緊鄰的一個或多個元素的邏輯關係,並且通常均可從邊界由差分維護的邏輯關係推出每個元素。(結構不僅侷限於線性,邏輯關係不僅侷限於減法的差關係、異或等)
(應用)差分常常用於優化修改相鄰元素的操做,並且每每優化的效果很贊(直接到O(1)),但要O(n)處理出差分的前綴和後才能查詢。適用於優化一批大量的全是修改連續元素的修改操做。離線算法。常搭配前綴和,對於先修改再詢問的題來講,差分O(1)處理修改,O(n)處理出前綴和,再用前綴和O(1)處理詢問。
樹上差分基本都會有LCA,且樹上差分經常用於求通過某點或邊路徑的條數。