淺析差分及其推廣(樹上差分與廣義差分)

差分數組及樹上差分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的值(從形式上看就是求前綴)。而這種差距不僅限於減法的差,還有異或等等。不過通常這種關係應可交換(即對順序的要求不嚴格)且對於運算來講有單位元(麼元)(或通常化的話就是要能有互相抵消的方法)

    見:洛谷P3943 星空——題解

  樹上差分:將差分搬到了樹上。能夠有兩個差分方向:

    一、記錄當前節點與父節點的邏輯關係,查詢時從上往下求前綴。(不經常使用,由於在每次路徑修改時都要修改一下當前節點的全部子節點,時間、程序複雜度都很高,沒有靈魂的差分(不能O(1)實現路徑修改))

    二、記錄當前節點與它全部子節點總和的邏輯關係,查詢時dfs求子樹和(或是說以向上爲正方向的求前綴)。(路徑修改時只要修改一下路徑起始點和lca(有時還有lca的父親),有了靈魂的差分(可O(1)實現路徑修改),很經常使用)

  樹上差分分爲點差分和邊差分,不論哪一種差分,差分數組的意義都是當前節點與它兒子節點總和的差距(這裏爲當前點(或點上的邊)被路徑通過次數與它的兒子節點(或其上的邊)被路徑通過次數總和的差,每次新增一個路徑,即要求實現路徑修改時,起始點與兒子們的差會多一,路徑中中間的點與兒子們的差不變。點差分時,lca會比兒子們少1,lca的父親會比兒子們少1;邊差分時,lca會比兒子們少二。用這些邏輯關係從葉子向上推時,若當前點的兒子們的值都是對的,那它也是對的。邊界狀況就是葉子結點,顯然是它的值對的,故可經過回溯推出整個樹的值。這樣對差分概念的理解有深刻了:差分的結構不知限於線性的數組)

    (這裏的基礎講解引用自大佬的博客)

    前置知識:

      須要知道的樹的性質:

        一、樹上任意兩個點的路徑惟一.

        二、任何子節點的父親節點惟一.(能夠認爲根節點是沒有父親的)

      樹上差分的兩種基本操做用到了LCA,不瞭解LCA的話能夠去這裏面學一下

    思想

      類比於差分數組,樹上差分利用的思想也是前綴和思想.(在這裏應該是子樹和思想.

      當咱們記錄樹上節點被通過的次數,記錄某條邊被通過的次數的時候.

      若是每次強制dfs去標記的話,時間複雜度將高到爆炸!

      所以咱們引入了樹上差分!

      與樹上差分在一塊兒的使用的是 DFS ,由於在回溯的時候,咱們能夠計算出子樹的大小.

      (這個應該不用過多解釋

    定義數組

      cnti 爲節點i被通過的次數.

    基本操做

      1.點的差分

      這個比較簡單,因此先講這個qwq

      例如,咱們從s>t ,求這條路徑上的點被通過的次數.

      很明顯的,咱們須要找到他們的LCA,(由於這個點是中轉點啊qwq.

      咱們須要讓cns++ ,讓 cnt++,而讓他們的cnlca−,cnfaher(lca)− ;

      可能讀着會有些難理解,因此我準備了一個圖qwq。綠色的數字表明通過次數.

      

      直接去標記的話,可能會T到不行,可是咱們如今在講啥?樹上差分啊!

      根據剛剛所講,咱們的標記應該是這樣的↓

    

      考慮:咱們搜索到s,向上回溯.

      下面以 u 表示當前節點, soni 表明i的兒子節點.(若是一些 son 不給出下標,即表明當前節點 u 的兒子

      每一個 u 統計它的子樹大小,順着路徑標起來.(即cnu+=cnson )

      咱們會發現第一次從s回溯到它們的LCA時候,cnLCA+=cnt[sonLCA]

      cntLCA=0 ! "不是LCA會被通過一次嘛,爲何是0!"

      別急,咱們繼續搜另外一邊.

      繼續:咱們搜索到t,向上回溯.

      依舊統計每一個u的子樹大小 cnu+=cnson

      再度回到 LCA 依舊 是 cntLCA+=cnt[sonLCA]

      這個時候cntLCA=1 這就達到了咱們要的效果 (是否是特別優秀 ( • ̀ω•́ )✧

      擔心: 萬一咱們再從 LCA 向上回溯的時候使得其父親節點的子樹和爲1怎麼辦?

      這樣咱們不就使得其父親節點被通過了一次? 所以咱們須要在cnfaher(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,且樹上差分經常用於求通過某點或邊路徑的條數。

相關文章
相關標籤/搜索