線段樹用於對數據的更新和查詢,主要優勢體現在對段的處理上。如將數組a[]從a[i]-a[j]的元素均加上b,要做j-i+1次,而有一個段表示a[i]-a[j]的話,就直接將這個段的和sum加上b*(j-i+1)即可。同理,查詢段的和也只需一次。
線段樹的原理就是將[1,n]分解成若干子區間,通過對其少量子區間進行修改統計實現對該區間快速的修改統計,用於統計的對象需符合區間加法,如求和,求最值,公因數。
時間複雜度:預處理O(n),查詢、更新操作O(logn);空間複雜度:O(n)。
線段樹的父節點區間平均分割到左右子樹中,對於包含n個葉子節點的二叉樹,它一定有n-1個非葉節點,總共2n-1個節點,因此存儲線段是需要的空間複雜度是O(n)。
結點定義:
struct node {
int l; //線段左端點
int r; //線段右端點
int value; //線段的值,這裏指線段中各元素和
int add; //lazy_tag
}tree[4*N];
利用數組存儲線段樹(有效空間爲2*N-1,實際空間爲滿二叉樹的結點數,即2^(log(2*N-1)+1)-1=4*N-3),tree數組大小爲4*N。其中tree[1]爲根結點,tree[2*i]、tree[2*i+1]分別爲tree[i]的左右孩子結點。
創建線段樹:
void build(int v, int l, int r)
{
tree[v].l = l;
tree[v].r = r;
if (l == r) //葉子結點
{
tree[v].value = a[r];
return;
}
int mid = (l + r) / 2;
build(v << 1, l, mid);
build(v << 1 | 1, mid + 1, r);
Push_up(v) //由子結點更新父節點
}
void Push_up(int v)
{
tree[v].value = tree[v << 1].value + tree[v << 1 | 1].value;
}
Push_up函數根據左右孩子結點的值更新該結點的值,在求最值問題中可寫爲tree[v].value = max(tree[v << 1].value , tree[v << 1 | 1].value)。
區間更新:
區間更新是指更新某個區間內的葉子節點的值,因爲涉及到的葉子節點不止一個,而葉子節點會影響其相應的非葉父節點,那麼回溯需要更新的非葉子節點也會有很多,如果一次性更新完,操作的時間複雜度肯定不是O(logn)。於是線段樹引入延遲標記,記錄這個節點是否進行了某種修改(這種修改操作會影響其子節點),對於任意區間的修改,我們先按照區間查詢的方式將其劃分成線段樹中的節點,然後修改這些節點的信息,並給這些節點標記上代表這種修改操作的標記。在修改和查詢的時候,如果我們到了一個節點v,並且決定考慮其子節點,那麼我們就要看節點v是否被標記,如果有,就要按照標記修改其子節點的信息,並且給子節點都標上相同的標記,同時消掉節點p的標記。
void Push_down(int v)
{
tree[v << 1].add += tree[v].add;//可能多次標記沒有傳遞,需+=
tree[v << 1 | 1].add += tree[v].add;
tree[v << 1].value += tree[v].add*(tree[v << 1].r - tree[v << 1].l + 1);
tree[v << 1|1].value += tree[v].add*(tree[v << 1|1].r - tree[v << 1|1].l + 1);
tree[v].add = 0;
}
void update(int v, int L, int R, int delta)
{//L:左端點,R:右端點 delta:增量
if (tree[v].l == L&&tree[v].r == R) //找到需要更新的線段,記錄增加的值add
{
tree[v].value += delta*(R - L + 1);
tree[v].add += delta;
return;
}
if (tree[v].add) Push_down(v); //延遲標記向下傳遞給子結點
int mid = (tree[v].l + tree[v].r) / 2;
if (R <= mid) update(v << 1, L, R, delta);//所找範圍在該結點的左半段
else
{
if (L > mid) update(v << 1 | 1, L, R, delta);//在右半段
else
{//橫跨左右兩段
update(v << 1, L, mid, delta);
update(v << 1 | 1, mid + 1, R, delta);
}
}
Push_up(v); //回溯時修改父節點
}
區間查詢:
int query(int v, int L, int R)
{
if (tree[v].l == L&&tree[v].r == R)
{//找到該段分區
return tree[v].value;
}
if (tree[v].add) Push_down(v);//延遲標記向下傳遞給子節點
int mid = (tree[v].l + tree[v].r) / 2,ans;
if (R <= mid) ans=query(v << 1, L, R);//在左半段
else
{
if (L > mid) ans=query(v << 1 | 1, L, R);//在右半段
else
{//橫跨左右兩段
ans=query(v << 1, l, mid)+ query(v << 1 | 1, mid + 1, R);
}
}
return ans;
}