日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

【用学校抄作业带你走进可持久化线段树(主席树)】可持久化线段树概念+全套模板+例题入门:[福利]可持久化线段树)

發布時間:2023/12/3 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 【用学校抄作业带你走进可持久化线段树(主席树)】可持久化线段树概念+全套模板+例题入门:[福利]可持久化线段树) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

我似乎很少寫這種算法博客

  • 可持久化線段樹概念
    • 概念介紹(類比幫助理解)
    • 簡單分析一下時間和空間復雜度(內容池)
  • 模板
    • 結構體變量
    • 建樹模板
    • 單點修改模板
    • 單點查詢模板
    • 區間修改模板(pushup)
    • 區間修改模板(比較特別)
    • 區間查詢模板
  • 入門題:可持久化線段樹
    • 題目
    • 簡單題解
    • 代碼實現

我以這種字體提醒大家是類比概念的理解

可持久化線段樹概念

概念介紹(類比幫助理解)

概念:可持久化線段樹也叫函數式線段樹,它的主體是線段樹,準確的說,是很多棵線段樹。線段樹表示的區間,是輸入數據的整個權值域,如果值域空間太大,要先離散化可能出現的元素值,壓縮值域空間。
線段樹存儲的信息是權值,維護不同區間的權值分布情況。
對于有n個元素的序列S,構造出n棵線段樹T[1…n],每棵樹Ti儲存序列的每一個前綴S[1…i]中每類數的出現個數,即權值分布情況。
陳立杰、Seter、Fotile等人稱這種以值域作為區間長度的線段樹為權值線段樹。因為每棵線段樹Ti維護的是前綴S[1…i]中每類數的出現個數。

一句話概括:每次修改線段樹上的值都新建一棵樹,保證原來的線段樹未被覆蓋


給幾張圖片幫助認識

對于可持久化線段樹的操作,保證每一次的線段樹都不要被覆蓋
可能會想到每一次都copy一棵完整的新樹,然后再新樹上進行更改
空間會變成(nm)(nm)(nm),顯然是不允許的

其實我們發現每一次都最多只修改log(n)log(n)log(n)個節點(走到了葉子節點)
其余的節點沒經過的都跟原來的線段樹狀態是一樣的,
那么我們就沒有必要去新建一些多余的節點儲存同樣的內容
所以可持久化線段樹的修改每一次都只新建需要更改的新點,不動的就直接賦成原來的狀態
這樣就保證了每一個狀態下的線段樹都被保存下來了,且未對其發生更改


我們可以類比抄作業,
A同學做完了一份作業(樹的最初始狀態) B同學就開始抄A,發現這其中有幾處錯誤, 那么他就在這幾個題目上面趕緊寫下自己的正確答案(新建線段樹), 然后其它一樣的地方就直接鏈接A同學的答案(先不急著把答案寫下來,只用鏈接地址不變,在后面要用到的時候就直接找到A的答案地址抄就可以了)

假設這個時候C又開始抄B的,又發現B有一些錯誤, 他就悄悄更改這幾處的答案(新建點), 與B一樣的就鏈接地址(雖然鏈接的是B的地址,但是B鏈接的是A的地址,所以就相當于C直接鏈接了A的地址) 以此類推。。。

可持久化就意味著老師檢查作業(查詢之時)能找到每一位同學各自的答案,并且每位同學都很不要臉
發現了別人的錯誤都不告訴別人,自己偷偷摸摸的改,生成了自己新的一份答案

不然你想如果A發現自己有個錯誤,改了的話,所有的同學都要更改那一道題,所以要按住A不讓他更改自己的答案,保證后面的答案順利傳下去

我jio得這個類比非常貼近生活實際,淺顯易懂啊!!相信很多親故都已經明白了吧!!


簡單分析一下時間和空間復雜度(內容池)

因為是新建的點,并未對以前的先單數進行深度增加或多幾個葉子結點
查詢和更改都應該是與線段樹一樣的時間復雜度O(logN)O(logN)O(logN)
m次操作就是O(MlogN)O(MlogN)O(MlogN)是肯定可以接受的


唯一蜜汁迷人的就是這個空間問題,
建初始樹的空間與普通線段樹是不一樣的,
因為普通線段樹是num<<1num<<1num<<1num<<1∣1num<<1|1num<<11,葉子結點也會生成多的空兒子
所以空間會變成4N4N4N,而主席樹就不一樣了,用了內存池再加上寫法的原因
葉子結點是不會多生成空兒子的,空間自然就是2N2N2N,盡管本蒟蒻還是按著4N4N4N在開
有m次操作,就有MlogNMlogNMlogN個新點,就會多這么多空間
線段樹總空間也就是(2N+MlogN)(2N+MlogN)(2N+MlogN)

不建議大家卡著點開,可以盡可能的多開一些,保證不MLE就ok

模板

學習任何算法都有一定的模板,剛開始多半都是靠背,打多了過后就自然而然就理解了
沒辦法
接下來的多數模板我們以找最大值為例,對于不同的情況適當更改部分代碼即可

結構體變量

#define MAXN 1000005 struct node {int l, r, Max; //依情況而決定里面的變量是Max,Min,Sum... }tree[MAXN << 2];

建樹模板

我都是寫的動態建樹,挺不錯的,推薦,嘻嘻
這里要注意必須把每一層的節點編號提前存下來,
不然回溯的時候cnt早已多加了很多,這樣我們就對不上了
作業亂了,名字跟真實試卷會對不上

int build ( int l, int r ) {int t = ++ cnt;if ( l == r ) {tree[t].l = tree[t].r = 0;tree[t].Max = a[l];return t;}int mid = ( l + r ) >> 1;tree[t].l = build ( l, mid );tree[t].r = build ( mid + 1, r );tree[t].Max = max ( tree[tree[t].l].Max, tree[tree[t].r].Max );return t; }

單點修改模板

把p這個點修改為v發現這份作業的p題錯了,偷偷摸摸不告訴別人自己改

int update ( int num, int p, int v, int l, int r ) {int t = ++ cnt;if ( l == r ) {tree[t].l = tree[t].r = 0;tree[t].Max = v;return t;}int mid = ( l + r ) >> 1;if ( p <= mid ) {tree[t].l = update ( tree[num].l, p, v, l ,mid );tree[t].r = tree[num].r;}else {tree[t].r = update ( tree[num].r, p, v, mid + 1, r );tree[t].l = tree[num].l;}tree[t].Max = max ( tree[tree[t].l].Max, tree[tree[t].r].Max );return t; }

單點查詢模板

假設我們要找p的值老師開始檢查作業,抽查到我們的p題答案,馬上找鏈接的答案

int query ( int p, int num, int l, int r ) {if ( l == r )return tree[num].Max;int mid = ( l + r ) >> 1;if ( p <= mid )return query ( p, tree[num].l, l, mid );elsereturn query ( p, tree[num].r, mid + 1, r ); }

區間修改模板(pushup)

首先面對線段樹的區間修改,大多數人都會采取lazy,主席樹也同樣適用
但不同的是各棵樹是彼此獨立的,如果我們lazy下放,對于i這個狀態是對的
但有可能到j狀態時的樹是不能加上這個lazy的,這個lazy并不是j打上去的
所以不下放,我們就回溯上放回來即可
我們之前說了每個人都小賤小賤的,只會改自己的答案,但是我們有些題貼的是別人的鏈接,無法幫助別人改答案,就只能等著別人改了答案更新了鏈接,自己這份才能跟著改,這個鏈接鏈上了甩都甩不掉

void pushup ( int root,int len ) {tree[root].sum = tree[tree[root].l].sum + tree[tree[root].r].sum + tree[tree[root].l].lazy * ( len - ( len >> 1 ) ) + tree[tree[root].r].lazy * ( len >> 1 ); }int update ( int root, int l, int r, int L, int R, int val ) {int t = ++ cnt;tree[t].lazy = tree[root].lazy;tree[t].l = tree[root].l;tree[t].r = tree[root].r;tree[t].sum = tree[root].sum;if ( L <= l && r <= R ) {tree[t].lazy = tree[root].lazy + val;return t;}int mid = ( l + r ) >> 1;if ( L <= mid )tree[t].l = update ( tree[root].l, l, mid, L, R, val );if ( mid < R )tree[t].r = update ( tree[root].r, mid + 1, r, L, R, val );pushup ( t, r - l + 1 );return t; }

區間修改模板(比較特別)

就用求區間和為例吧!!
對于有些奇怪的題,區間是不能pushup和pushdown的,那么就要另辟蹊徑代替lazy上下放

我們采取另一種手段來實現修改查詢操作,就是每次在區間直接對區間答案進行修改,
然后如果被劃分到剛剛好的區間的時候打上lazy即可。
保證了如果區間有lazy那么這整個區間一定是被完全覆蓋的

對于一次的修改L,R,lazy的標記只存在與L,R之間劃分到的整個區間,
并且所有不是整區間的區間答案在找區間經過的途中都整體增加了該有的值,包括整區間,
然后每次查詢答案其答案的初始化都是當前區間的lazy值乘上其區間大小,
因為如果當前區間有lazy那么一定是整體覆蓋的,并且查詢和修改
要把目標區間進行劃分,這樣我每次查詢和修改都能維護出合理的答案

int update( int & suf, int pre, int l, int r, int L, int R, int val ){suf = ++ cnt;tree[suf].sum = tree[pre].sum;tree[suf].l = tree[pre].l;tree[suf].r = tree[pre].r;tree[suf].lazy = tree[pre].lazy;tree[suf].sum += 1ll * ( R - L + 1 ) * val;if( L <= l && r <= R ) {tree[suf].lazy += val;return suf;}int mid = ( l + r ) >> 1;if( R <= mid ) //全部都在左兒子區間 tree[suf].l = update ( tree[suf].l, tree[pre].l, l, mid, L, R, val );else if( mid < L ) //全部都在右兒子區間 tree[suf].r = update ( tree[suf].r, tree[pre].r, mid + 1, r, L, R, val );else { //左右皆有 tree[suf].l = update ( tree[suf].l, tree[pre].l, l, mid, L, R, val );tree[suf].r = update ( tree[suf].r, tree[pre].r, mid + 1, r, L, R, val );}return suf; }

區間查詢模板

其實親故們都知道區間查詢也可以完成單點的,無非就是多傳一個不用的變量罷了
這里假設的是找[L,R][L,R][L,R]之間的最大值

int query ( int L, int R, int num, int l, int r ) {if ( L <= l && r <= R )return tree[num].Max;int mid = ( l + r ) >> 1;int ans1 = - INF, ans2 = - INF;if ( L <= mid )ans1 = query ( L, R, tree[num].l, l, mid );if ( mid < R )ans2 = query ( L, R, tree[num].r, mid + 1, r );return max ( ans1, ans2 ); }


說了這么多,也該是期末學習成果的展示了!!!
只有做題才能更好的掌握運用主席樹模板!

入門題:可持久化線段樹

題目

題目描述
為什么說本題是福利呢?因為這是一道非常直白的可持久化線段樹的練習題,目的并不是虐人,而是指導你入門可持久化數據結構。

線段樹有個非常經典的應用是處理RMQ問題,即區間最大/最小值詢問問題。現在我們把這個問題可持久化一下:

Q k l r 查詢數列在第k個版本時,區間[l, r]上的最大值
M k p v 把數列在第k個版本時的第p個數修改為v,并產生一個新的數列版本

最開始會給你一個數列,作為第1個版本。每次M操作會導致產生一個新的版本。
修改操作可能會很多呢,如果每次都記錄一個新的數列,空間和時間上都是令人無法承受的。
所以我們需要可持久化數據結構
對于最開始的版本1,我們直接建立一顆線段樹,維護區間最大值。

修改操作呢?我們發現,修改只會涉及從線段樹樹根到目標點上一條樹鏈上logn個節點而已,其余的節點并不會受到影響。所以對于每次修改操作,我們可以只重建修改涉及的節點即可。
需要查詢第k個版本的最大值,那就從第k個版本的樹根開始,像查詢普通的線段樹一樣查詢即可。
要計算好所需空間哦

輸入格式
第一行兩個整數N, Q。N是數列的長度,Q表示詢問數
第二行N個整數,是這個數列
之后Q行,每行以0或者1開頭,0表示查詢操作Q,1表示修改操作M,
格式為
0 k l r 查詢數列在第k個版本時,區間[l, r]上的最大值 或者
1 k p v 把數列在第k個版本時的第p個數修改為v,并產生一個新的數列版本

輸出格式
對于每個M詢問,輸出正確答案

樣例
input
4 5
1 2 3 4
0 1 1 4
1 1 3 5
0 2 1 3
0 2 4 4
0 1 2 4
output
4
5
4
4
解釋
序列版本1: 1 2 3 4
查詢版本1的[1, 4]最大值為4
修改產生版本2: 1 2 5 4
查詢版本2的[1, 3]最大值為5
查詢版本1的[4, 4]最大值為4
查詢版本1的[2, 4]最大值為4

數據范圍與提示
N <= 10000
Q <= 100000
對于每次詢問操作的版本號k保證合法,區間[l, r]一定滿足1 <= l <= r <= N

簡單題解

說過了這道題是一個入門版題,照著模板打就可以了
對于每一個更改線段樹的操作,我們找到操作途中所經過的點,重新建點,
不覆蓋原來的線段樹模樣,對于不變的我們就直接把原來樹上的地址直接甩過去就可以了
記錄下第i個操作的根節點,每一個線段樹都是不相互影響沖突的,只是有可能會共用一些點罷了。

代碼實現

#include <cstdio> #include <iostream> using namespace std; #define MAXN 1000005 #define INF 0x7f7f7f7f struct node {int l, r, Max; }tree[MAXN << 2]; int cnt, tot, n, q; int a[MAXN / 10], root[MAXN << 2]; int build ( int l, int r ) {int t = ++ cnt;if ( l == r ) {tree[t].l = tree[t].r = 0;tree[t].Max = a[l];return t;}int mid = ( l + r ) >> 1;tree[t].l = build ( l, mid );tree[t].r = build ( mid + 1, r );tree[t].Max = max ( tree[tree[t].l].Max, tree[tree[t].r].Max );return t; } int update ( int num, int p, int v, int l, int r ) {int t = ++ cnt;if ( l == r ) {tree[t].l = tree[t].r = 0;tree[t].Max = v;return t;}int mid = ( l + r ) >> 1;if ( p <= mid ) {tree[t].l = update ( tree[num].l, p, v, l ,mid );tree[t].r = tree[num].r;}else {tree[t].r = update ( tree[num].r, p, v, mid + 1, r );tree[t].l = tree[num].l;}tree[t].Max = max ( tree[tree[t].l].Max, tree[tree[t].r].Max );return t; } int query ( int L, int R, int num, int l, int r ) {if ( L <= l && r <= R )return tree[num].Max;int mid = ( l + r ) >> 1;int ans1 = - INF, ans2 = - INF;if ( L <= mid )ans1 = query ( L, R, tree[num].l, l, mid );if ( mid < R )ans2 = query ( L, R, tree[num].r, mid + 1, r );return max ( ans1, ans2 ); } int main() {scanf ( "%d %d", &n, &q );for ( int i = 1;i <= n;i ++ )scanf ( "%d", &a[i] );root[++ tot] = build ( 1, n );for ( int i = 1;i <= q;i ++ ) {int opt, k, p, v;scanf ( "%d %d %d %d", &opt, &k, &p, &v );if ( opt == 0 )printf ( "%d\n", query ( p, v, root[k], 1, n ) );elseroot[++ tot] = update ( root[k], p, v, 1, n );}return 0; }

相信這篇博客會幫助大家更好地理解可持久化線段樹,如果有錯誤的地方歡迎大家指出改正,謝謝,聯系方式:139紅酒白酒葡萄酒+評論
ヾ(ToT)ByeBye
講得好的話點個贊,親(づ ̄3 ̄)づ╭?~

總結

以上是生活随笔為你收集整理的【用学校抄作业带你走进可持久化线段树(主席树)】可持久化线段树概念+全套模板+例题入门:[福利]可持久化线段树)的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。