1. 程式人生 > >比較簡單的線段樹入門

比較簡單的線段樹入門

define 需要 時間 char 什麽 struct style 重要 輸出格式

線段樹是一種十分方便的數據結構,可以解決多段連續區間的查詢問題

對比其他一些數據結構,線段樹能夠解決的問題是動態的,這也是線段樹的特性

線段樹的性質還有每個節點保存一個線段,以及左節點保存的線段是父節點保存的線段的左半段,右子節點反之

(即當父節點保存的線段為[1,n],左子節點保存的線段為[1,(1+n)/2],而右子節點保存的線段為[(1+n)/2+1,n])

由於線段樹是一顆完全二叉樹,所以每個操作的復雜度大概保持在O(log n)

線段樹可以做到的操作有如下:

1)構建線段樹;

2)區間查詢;

3)區間或單點修改;

(單點查詢即為左右端點相等的特殊區間查詢,單點修改也是同理)

那話不多說(已經不少了),接下來就對以上幾種操作舉例說明(這裏統一以維護區間的和為例)

1)線段樹的構建

就是遞歸構造一個樹結構

 1 int a[200010];//a[i]記錄數組
 2 struct node{int val;}tree[400010];//tree用於構造線段樹
 3 //root 當前父節點
 4 //l 當前節點保存區間的左端點
 5 //r 當前節點保存區間的右端點
 6 void bdt(int root,int l,int r)
 7 {
 8     if(l==r) //當節點保存的區間是一個點時,記錄該元素
 9     {tree[root].val=a[l]; return;}
10     //遞歸構建子樹
11     bdt(rt*2,l,(l+r)/2);
12     bdt(rt*2
+1,(l+r)/2+1,r); 13 //將左右子節點的值回溯到當前節點上 14 tr[rt].val=tr[rt*2].val+tr[rt*2+1].val; 15 }

2)區間查詢

區間的查詢就是將要查詢的區間劃分為線段樹上節點保存的區間,然後通過節點的合成得到目的區間的值

 1 //root,l,r同上
 2 //f 查詢目標區間的左端點
 3 //t 查詢目標區間的右端點
 4 int query(int root,int l,int r,int f,int t)
 5 {
 6     //如果查詢區間與節點區間沒有交集則返回0(0對求和沒有影響)
 7     if(t<l || f>r)
8 return 0; 9 //如果節點區間包含於查詢區間則返回當前區間的求和值 10 if(f<=l && t>=r) 11 return tree[root].val; 12 //左右子樹遞歸將求和保存到父節點 13 return query(root*2,l,(l+r)/2,f,t)+query(root*2+1,(l+r)/2+1,r,f,t); 14 }

可以見得,由於所選的區間盡量少,所以很大程度上節省了時間復雜度,大概節省到了O(log n)

還要記得使用線段樹解決問題有一個條件——問題是可以分解解決的

3)單點及區間的修改

首先是單點修改

 1 //root,l,r仍然同上
 2 //k 要修改的點
 3 //add 要增加的值
 4 void update(int root,int l,int r,int k,int add)
 5 {
 6     //當節點是葉子節點執行修改
 7     if(l==r)
 8     {if(k==l) tree[root]+=ad; return;}
 9     //遞歸左右字數尋找目標節點
10     int mid=(l+r)/2;
11     if(k<=mid)
12     update(root*2,l,mid,k,add);
13     else update(root*2+1,mid+1,r,k,add);
14     //回溯更新父節點(可見回溯在線段樹中還是很重要的)
15     tree[root]=tree[root*2]+tree[root*2+1];
16 }

然後是區間修改

區間修改要是也和上面一樣那就失去了線段樹的意義了(笑

於是就需要一個很有意思的操作——對每個點維護一個延遲標記

這個標記可以記錄節點受到了什麽樣的修改,而這個修改會影響他的子節點

當我們找到一個節點並且判斷需要考慮其子節點,就將這個節點的延遲標記向子節點傳遞,並將這個節點的標記清零

恩...由於這個操作新加入了一個元素,就破壞了以上代碼的連續性,這裏決定直接上例題(cogs上的一道模板題)

1317. 數列操作c

★★☆ 輸入文件:shuliec.in 輸出文件:shuliec.out 簡單對比
時間限制:1 s 內存限制:128 MB

所有答案小於4611686018427387904

【問題描述】

假設有一列數 {Ai }(1 ≤ i ≤ n),n<=100000 ,支持如下兩種操作:

(1)將 A i至A j 的值均增加 D 。( i,j,D 是輸入的數)

(2) 輸出 A s +A s+1 +…+A t 。( s, t 都是輸入的數, S ≤ T )

根據操作要求進行正確操作並輸出結果。

【輸入格式】

輸入文件第一行一個整數 n ,

第二行為 n 個整數,表示 {A i } 的初始值。

第三行為一個整數 m ,表示操作數

v 下接 m 行,每行描述一個操作,有如下兩種情況:

ADD i j d ( 表示將 A i至A j 的值均增加 D , 1<=i,j<=n , d 為整數 )

SUM s t (表示輸出 A s +…+A t )

【輸出格式】

對於每一個 SUM 提問,輸出結果

【輸入輸出樣例】

輸入:

shuliec.in

4

1 4 2 3

3

SUM 1 3

ADD 2 2 50

SUM 2 3

輸出:

shuliec.out

7

56

思路:就是一道區間修改區間查詢的線段樹模板,以下代碼

 1 #include<cstdio>
 2 #define LL long long
 3 using namespace std;
 4 int n,m,a[200010];
 5 struct node{LL val,mark;}tree[400010];//這裏的mark即為延遲標記
 6 char s[10];
 7 void buildtree(int root,int l,int r)
 8 {
 9     //構建時要先把所有點的延遲標記清零
10     tree[root].mark=0;
11     if(l==r)
12     {tree[root].val=a[l]; return;}
13     buildtree(root*2,l,(l+r)/2);
14     buildtree(root*2+1,(l+r)/2+1,r);
15     tree[root].val=tree[root*2].val+tree[root*2+1].val;
16 }
17 //這就是將延遲標記向下傳遞的操作
18 void pushdown(int root,int x)
19 {
20     if(tree[root].mark!=0)
21     {
22         //考慮到可能有些節點並不需要繼續向下查詢,應該是"+="
23         tree[root*2].mark+=tree[root].mark;
24         tree[root*2+1].mark+=tree[root].mark;
25         //這可是個區間啊,每個節點的權值只增加標記值怎麽能行
26         tree[root*2].val+=tree[root].mark*(x-(x>>1));
27         tree[root*2+1].val+=tree[root].mark*(x>>1);
28     }tree[root].mark=0;
29 }
30 LL query(int root,int l,int r,int f,int t)
31 {
32     if(t<l || f>r)
33     return 0;
34     if(f<=l && t>=r)
35     return tree[root].val;
36     pushdown(root,r-l+1);
37     return query(root*2,l,(l+r)/2,f,t)+query(root*2+1,(l+r)/2+1,r,f,t);
38 }
39 //備受期待的區間修改,前三個元素仍然是同上
40 //f,t分別是修改區間的左右端點,v則是需要增加的值
41 void add(int root,int l,int r,int f,int t,int v)
42 {
43     int mid=(l+r)/2;
44     //修改區間和節點區間沒有交集,自然不考慮
45     if(f>r || t<l) return;
46     //節點區間包含於修改區間,更改區間權值,記錄延遲標記
47     if(f<=l && t>=r)
48     {
49         tree[root].mark+=v;
50         tree[root].val+=(LL)v*(r-l+1);
51         return;
52     }
53     //將延遲標記向下傳遞
54     pushdown(root,(r-l+1));
55     //考慮和左子,右子是否有交集的情況
56     add(root*2,l,mid,f,t,v);
57     add(root*2+1,mid+1,r,f,t,v);
58     //重要的回溯
59     tree[root].val=tree[root*2].val+tree[root*2+1].val;
60 }
61 int main()
62 {
63     int x,y,w,i,j;
64     LL ans;
65     scanf("%d",&n);
66     for(i=1;i<=n;++i)
67     scanf("%d",&a[i]);
68     buildtree(1,1,n);
69     scanf("%d",&m);
70     for(i=1;i<=m;++i)
71     {
72         scanf("%s",s);
73         if(s[0]==S)
74         {
75             scanf("%d%d",&x,&y);
76             ans=query(1,1,n,x,y);
77             printf("%lld\n",ans);
78         }
79         if(s[0]==A)
80         {
81             scanf("%d%d%d",&x,&y,&w);
82             add(1,1,n,x,y,w);
83         }
84     }
85 }

比較簡單的線段樹入門