線段樹·題解報告
參考資料
·課件
·Blog
選題目錄
· Poj3468 A Simple Problem with Integers(區間加減,區間求和)
· Poj2777 Count Color(區間修改,區間查詢,染色)
線段樹總結
I 普通版遞歸線段樹:
每層都是[a,b]的劃分. 記L=b-a, 則共log2L層
任兩個結點要麼是包含關係要麼沒有公共部分, 不可能部分重疊
給定一個葉子p, 從根到p路徑上所有結點(即p的所有直系祖先)代表的區間都包含點p, 且其他結點代表的區間都不包含點p
給定一個區間[l, r], 可以把它分解爲不超過2log2L條不相交線段的並
Lazy思想: 記錄有哪些指令, 而不真正執行它們. 等到需要計算的時候再說
根據題目要求確定維護信息和附加信息
具有解決問題的通用性
II zkw線段樹:
堆式存儲,好寫,效率較高
自底向上,非遞歸更新和查詢
Lazy標記
題解報告
題目1:Hdu1166 敵兵佈陣
題目鏈接
http://acm.hdu.edu.cn/showproblem.php?pid=1166
題目大意
有N個正整數,對其進行三種操作:
Add i, j 第i個數增加j
Sub i, j 第i個數減去j
Query i, j 查詢區間[i, j]中數的和
Input
第一行一個整數T,表示有T組數據。
每組數據第一行一個正整數N(N<=50000),接下來有N個整數。
接下來每行有一條命令,命令有4種形式:
Add i, j 第i個數增加j
Sub i, j 第i個數減去j
Query i, j 查詢區間[i, j]中數的和
End 表示結束
每組數據最多有40000條命令
Output
對第i組數據, 首先輸出”Case i:”和回車,
對於每個Query詢問,輸出一個整數並回車,表示詢問的區間中數的總和,這個數不超過1000000.
思路
線段樹的單點修改,區間求和,可以用zkw線段樹維護區間和,提高效率,並便於編寫代碼.
算法步驟
1. 建樹
存儲空間: T[4 * N], 初始化爲0
M: M = 1; while (M < n + 2) M <<= 1;
T[M+1]到T[M+n]: 存n個輸入的整數
T[M-1]到T[1]: T[i] = T[2*i] + T[2*i+1];
2. 更新x
更新最下層的數T[x+M], 並自底向上更新其父結點的值
T[i] = T[2*i] + T[2*i+1];
3. 查詢[l,r]
令閉區間[l,r]變成(l-1,r+1).
自底向上查詢, 從(l = l-1+M, r = r+1+m)開始查詢, 如果l
是樹中左結點(~l & 1)則加上其右端點值;
如果r是樹中右結點(r & 1)則加上其左端點值;
向上查詢: l >>= 1; r >>= 1;
當l和r是同層兄弟時結束(l ^ r ^ 1)
算法複雜度
建樹O(n), 查詢和修改O(logn)
總時間複雜度: O(Alogn) (A爲總操作數)
總空間複雜度: O(4 * n)
源程序
/*
* Author: HongSheng Zeng
* Email: [email protected]
* FileName: 1166.cpp
* Creation: 2014/08/31
*/
#include <iostream>
#include <cstdio>
#include <string>
#include <string.h>
using namespace std;
const int N = 50000 + 10;
int n, M, i;
int T[4 * N];
void BuildTree()
{
memset(T, 0, sizeof(T));
M = 1;
while (M < n + 2)
M <<= 1;
for (i = M + 1; i <= M + n; ++i)
scanf("%d", &T[i]);
for (i = M - 1; i; --i)
T[i] = T[2 * i] + T[2 * i + 1];
}
int Query(int l, int r)
{
l += M - 1; r += M + 1;
int ans = 0;
while (l ^ r ^ 1) {
if (~l & 1) ans += T[l + 1];
if (r & 1) ans += T[r - 1];
l >>= 1; r >>= 1;
}
return ans;
}
void update(int x, int k)
{
x += M;
T[x] += k;
while (x > 1) {
x >>= 1;
T[x] = T[2 * x] + T[2 * x + 1];
}
}
int main()
{
int t, l, r, x, value;
char ch[10];
scanf("%d", &t);
for (int i = 1; i <= t; ++i) {
scanf("%d", &n);
BuildTree();
printf("Case %d:\n", i);
while (scanf("%s", ch)) {
string s = string(ch);
if (s == "End")
break;
if (s == "Query") {
scanf("%d%d", &l, &r);
printf("%d\n", Query(l, r));
}
if (s == "Add") {
scanf("%d%d", &x, &value);
update(x, value);
}
if (s == "Sub") {
scanf("%d%d", &x, &value);
update(x, -value);
}
}
}
}
評測系統上運行結果
Accepted. 運行時間 312ms, 佔用內存1128KB.
題目2:Hdu1754 I Hate It
題目鏈接
http://acm.hdu.edu.cn/showproblem.php?pid=1754
題目大意
老師需要詢問從某某到某某當中,分數最高的是多少,也需要更新某位同學的成績.
Input
題目包含多組測試,處理到文件結束。
在每個測試的第一行,有兩個正整數 N 和 M ( 0<N<=200000,0<M<5000 ),分別代表學生的數目和操作的數目。
學生ID編號分別從1編到N。
第二行包含N個整數,代表這N個學生的初始成績,其中第i個數代表ID爲i的學生的成績。
接下來有M行。每一行有一個字符 C (只取'Q'或'U') ,和兩個正整數A,B。
當C爲'Q'的時候,表示這是一條詢問操作,它詢問ID從A到B(包括A,B)的學生當中,成績最高的是多少。
當C爲'U'的時候,表示這是一條更新操作,要求把ID爲A的學生的成績更改爲B。
Output
對於每一次詢問操作,在一行裏面輸出最高成績。
思路
線段樹的單點修改,區間求最值(RMQ),可以用zkw線段樹維護區間最值,提高效率,並便於編寫代碼.
算法步驟
1.建樹
存儲空間: T[4 * N], 初始化爲0
M: M = 1; while (M < n + 2) M <<= 1;
T[M+1]到T[M+n]: 存n個輸入的整數
T[M-1]到T[1]: T[i] = max(T[2*i], T[2*i+1]);
2.更新x
更新最下層的數T[x+M], 並自底向上更新其父結點的值
T[i] = max(T[2*i], T[2*i+1]);
3. 查詢[l,r]
令閉區間[l,r]變成(l-1,r+1).
自底向上查詢, 從(l = l-1+M, r = r+1+m)開始查詢.
Int ans = 0;
如果l是樹中左結點(~l & 1)且其右端點值大於ans,則ans等於其右端點值;
如果r是樹中右結點(r & 1)且其左端點值大於ans,則ans等於其左端點值;
向上查詢: l >>= 1; r >>= 1;
當l和r是同層兄弟時結束(l ^ r ^ 1)
算法複雜度
建樹O(n), 查詢和修改O(logn)
總時間複雜度: O(Alogn) (A爲總操作數)
總空間複雜度: O(4 * n)
源程序
/*
* Author: HongSheng Zeng
* Email: [email protected]
* FileName: 1754.cpp
* Creation: 2014/09/01
*/
#include <iostream>
#include <cstdio>
#include <string.h>
using namespace std;
const int N = 200000 + 10;
int n, m, M, l, r, x, k;
int T[4 * N];
void Build_Tree()
{
memset(T, 0, sizeof(T));
M = 1;
while (M < n + 2)
M <<= 1;
for (int i = M + 1; i <= M + n; ++i)
scanf("%d", &T[i]);
for (int i = M - 1; i; --i)
T[i] = max(T[i * 2], T[i * 2 + 1]);
}
void Update()
{
x += M;
T[x] = k;
while (x) {
x >>= 1;
T[x] = max(T[x * 2], T[x * 2 + 1]);
}
}
int Query()
{
l += M - 1; r += M + 1;
int ans = 0;
while (l ^ r ^ 1) {
if (~l & 1)
ans = max(ans, T[l + 1]);
if (r & 1)
ans = max(ans, T[r - 1]);
l >>= 1; r >>= 1;
}
return ans;
}
int main()
{
while (scanf("%d%d", &n, &m) != EOF) {
Build_Tree();
char ch;
while (m--) {
scanf(" %c", &ch);
if (ch == 'Q') {
scanf("%d%d", &l, &r);
printf("%d\n", Query());
}
else {
scanf("%d%d", &x, &k);
Update();
}
}
}
}
評測系統上運行結果
Accepted. 運行時間 953ms, 佔用內存3448KB.
題目3:Hdu3308 敵兵佈陣
題目鏈接
http://acm.hdu.edu.cn/showproblem.php?pid=3308
題目大意
給出一個長度爲N(N <= 100000)的數列,然後是兩種操作:
U A B: 將第A個數替換爲B (下標從零開始)
Q A B: 輸出區間[A, B]的最長連續遞增子序列
詢問的次數m <= 100000。
Input
第一行一個整數T,表示有T組數據。
每組數據第一行有一個整數n和m(0<n,m<=105),接下來有n個整數(0<=val<=105);
接下來m行,每行有一條命令,命令有2種形式:
U A B (用B替換第A個數, 下標從0開始計數)
Q A B (查詢區間[A, B]中最長連續子序列的長度)
Output
對於每個Q操作,輸出結果.
思路
線段樹的單點修改,求區間並(查詢區間的最長連續子序列)。
Case1: 父節點的左兒子右端點值 >= 父節點的右兒子左端點值
父節點維護區間中的最長連續子序列=max(左兒子最長連續子序列,右兒子最長連續子序列)
Case2: 父節點的左兒子右端點值 < 父節點的右兒子左端點值
父節點維護區間中的最長連續子序列=max(左兒子最長連續子序列,右兒子最長連續子序列,左兒子維護區間中以其右端點結束的最長連續子序列+右兒子維護區間中以其左端點開始的最長連續子序列)
故線段樹中需要維護區間的最長連續子序列(smax),區間以左端點開始的最長連續子序列(lmax),和區間以右端點結束的最長連續子序列(rmax).維護信息較複雜且查詢時自頂向下比較方便,故用普通版的遞歸線段樹.
算法步驟
0. 信息向上更新
通過Up操作(詳見源代碼)將父節點的左右兒子的smax,lmax,rmax信息更新到父節點。
1.建樹
struct node
{
int l, r, lmax, rmax, smax;
} tree[3 * N];
從上往下建樹,然後從下往上更新維護信息.
2.更新x
從上往下,確定x,並更新其值,然後通過Up操作往上更新維護信息。
3. 查詢[l,r]
從上往下遞歸查詢區間,跨區間時需合併結果。
算法複雜度
建樹O(n), 查詢和修改O(logn)
總時間複雜度: O(Alogn) (A爲總操作數)
總空間複雜度: O(3 * n)
源程序
/*
* Author: HongSheng Zeng
* Email: [email protected]
* FileName: 3308.cpp
* Creation: 2014/09/10
*/
#include <iostream>
#include <cstdio>
#include <string.h>
using namespace std;
const int N = 100000 + 10;
int num[N];
struct node
{
int l, r, lmax, rmax, smax;
} tree[3 * N];
void Up(int x)
{
int l = tree[x].l, r = tree[x].r;
int mid = (l + r) / 2;
tree[x].lmax = tree[x * 2].lmax;
tree[x].rmax = tree[x * 2 + 1].rmax;
tree[x].smax = max(tree[x * 2].smax, tree[x * 2 + 1].smax);
if (num[mid] < num[mid + 1]) {
if (tree[x].lmax == (mid - l + 1))
tree[x].lmax += tree[x * 2 + 1].lmax;
if (tree[x].rmax == (r - mid))
tree[x].rmax += tree[x * 2].rmax;
tree[x].smax = max(tree[x].smax, tree[x * 2].rmax + tree[x * 2 + 1].lmax);
}
}
void BuildTree(int x, int l, int r)
{
tree[x].l = l;
tree[x].r = r;
if (l == r) {
tree[x].lmax = tree[x].rmax = tree[x].smax = 1;
return;
}
int mid = (l + r) / 2;
BuildTree(x * 2, l, mid);
BuildTree(x * 2 + 1, mid + 1, r);
Up(x);
}
void Update(int x, int k)
{
if (tree[x].l == tree[x].r)
return;
int l = tree[x].l , r = tree[x].r;
int mid = (l + r) / 2;
if (k <= mid)
Update(x * 2, k);
else
Update(x * 2 + 1, k);
Up(x);
}
int Query(int x, int l, int r)
{
if (tree[x].l == l && tree[x].r == r)
return tree[x].smax;
int mid = (tree[x].l + tree[x].r) / 2;
if (r <= mid)
return Query(x * 2, l, r);
else if (l > mid)
return Query(x * 2 + 1, l, r);
else {
int ans = max(Query(x * 2, l, mid), Query(x * 2 + 1, mid + 1, r));
if (num[mid] < num[mid + 1]) {
ans = max(ans, min(tree[x * 2].rmax, mid - l +1) +
min(tree[x * 2 + 1].lmax, r - mid));
}
return ans;
}
}
int main()
{
int t, l, r, index, k, n, m;
char ch;
scanf("%d", &t);
while (t--) {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%d", &num[i]);
BuildTree(1, 1, n);
while (m--) {
scanf(" %c", &ch);
if (ch == 'Q') {
scanf("%d%d", &l, &r);
++l; ++r;
printf("%d\n", Query(1, l, r));
}
else {
scanf("%d%d", &index, &k);
++index;
num[index] = k;
Update(1, index);
}
}
}
}
評測系統上運行結果
Accepted. 運行時間 484ms, 佔用內存5832KB.
題目4:Poj3468 A Simple Problem with Integers
題目鏈接
http://poj.org/problem?id=3468
題目大意
有N個正整數,對其進行兩種操作:
‘Q a b ’ 詢問a~b這段數的和
‘C a b c’ 把a~b這段數都加上c
Input
第一行兩個整數N,Q, 1 ≤ N,Q ≤ 100000.
接下來有N個整數:A1, A2, ... , AN, -1000000000 ≤ Ai ≤ 1000000000.
接下來有Q行,每行有一條命令,命令有2種形式:
"C a b c" Aa, Aa+1, ... , Ab 的值都加上c, -10000 ≤ c ≤ 10000.
"Q a b" 查詢區間Aa, Aa+1, ... , Ab的值的總和.
Output
對於每個Query詢問,輸出一個數並回車,表示詢問的區間中數的總和.
思路
線段樹的區間加,區間求和,使用線段樹+lazy標記,因爲只需要維護區間和,從下往上也可以便利地查詢,所以用zkw線段樹。
算法步驟
1. 建樹
存儲空間: T[4 * N], 初始化爲0
標記空間: mark[4 * N], 初始化爲0, mark[i]表示i結點以下所有結點更新值之和,當mark[i]不爲0 時說明結點i的更新信息還沒更新到其子結點.
M: M = 1; while (M < n + 2) M <<= 1;
h: 樹的高度
T[M+1]到T[M+n]: 存n個輸入的整數
T[M-1]到T[1]: T[i] = T[2*i] + T[2*i+1];
2. down(x)操作
標記下傳, 沿着根結點到x的線路,父結點的標記信息更新到其子結點,並清空父結點的標記 信息.
3. up(x)操作
沿着x到根結點的路線,用子結點的維護信息更新父結點的維護信息.
4.更新[l, r], +k
l += M - 1; r += M + 1;
標記下傳down(l), down(r);
lazy更新結點:
如果l是樹中左結點(~l & 1)則更新其右端點值+k,並更新其標記值+k;
如果r是樹中右結點(r & 1)則更新其左端點值+k,並更新其標記值+k;
向上更新: l >>= 1; r >>= 1;
當l和r是同層兄弟時結束(l ^ r ^ 1)
對l父結點和r父結點進行up操作
5. 查詢[l,r]
l += M - 1; r += M + 1;
標記下傳down(l), down(r)
查詢:
如果l是樹中左結點(~l & 1)則加上其右端點值;
如果r是樹中右結點(r & 1)則加上其左端點值;
向上查詢: l >>= 1; r >>= 1;
當l和r是同層兄弟時結束(l ^ r ^ 1)
算法複雜度
建樹O(n), 查詢和修改O(logn)
總時間複雜度: O(Alogn) (A爲總操作數)
總空間複雜度: O(4 * n)
源程序
/*
* Author: HongSheng Zeng
* Email: [email protected]
* FileName: 3468.cpp
* Creation: 2014/09/03
*/
#include <iostream>
#include <cstdio>
#include <string.h>
using namespace std;
const int N = 100000 + 10;
int n, q, M, h, l , r, ll, rr, t;
long long k;
long long T[4 * N];
long long mark[4 * N];
void Build_Tree()
{
memset(T, 0, sizeof(T));
memset(mark, 0, sizeof(mark));
int i;
M = 1; h = 0;
while (M < n + 2) {
M <<= 1;
++h;
}
for (i = M + 1; i <= M + n; ++i)
scanf("%lld", &T[i]);
for (i = M - 1; i; --i)
T[i] = T[i * 2] + T[2 * i + 1];
}
void down(int x)
{
for (int i = h; i; --i)
if (mark[t = x >> i]) {
mark[t] >>= 1;
mark[t << 1] += mark[t]; mark[t * 2 + 1] += mark[t];
T[t << 1] += mark[t]; T[t * 2 + 1] += mark[t];
mark[t] = 0;
}
}
void up(int x)
{
while (x) {
T[x] = T[x << 1] + T[x * 2 + 1];
x >>= 1;
}
}
void Update()
{
l += M -1; r += M + 1;
ll = l >> 1; rr = r >> 1;
down(l); down(r);
while (l ^ r ^ 1) {
if (~l & 1) {
T[l + 1] += k;
mark[l + 1] += k;
}
if (r & 1) {
T[r - 1] += k;
mark[r - 1] += k;
}
k <<= 1;
l >>= 1; r >>= 1;
}
up(ll); up(rr);
}
long long Query()
{
long long ans = 0;
l += M - 1; r += M + 1;
down(l); down(r);
while (l ^ r ^ 1) {
if (~l & 1) ans += T[l + 1];
if (r & 1) ans += T[r - 1];
l >>= 1; r >>= 1;
}
return ans;
}
int main()
{
scanf("%d%d", &n, &q);
Build_Tree();
char ch;
for (int i = 0; i < q; ++i) {
scanf(" %c%d%d", &ch, &l, &r);
if (ch == 'Q') {
printf("%lld\n", Query());
} else {
scanf("%lld", &k);
Update();
}
}
}
評測系統上運行結果
Accepted. 運行時間 2219ms, 佔用內存6952KB.
題目5:Poj2777 Count Color
題目鏈接
http://poj.org/problem?id=2777
題目大意
有一個區間[1,L],被分成L段,標記爲1,2...L;最多有T種顏色。有兩種操作,一種是對某一個區間段染上某一種顏色,一種是詢問該區間有多少種不同的顏色。整個區間剛開始爲1.
Input
第一行L (1 <= L <= 100000), T (1 <= T <= 30) 和 O (1 <= O <= 100000).
接下來O行,每行有一條命令,命令有2種形式:
C A B C (給A到B區間染上C色)
Q A B (查詢區間[A, B]有多少種不同的顏色)
Output
對於每個Q操作,輸出結果.
思路
線段樹的染色問題,區間修改,區間查詢.
結點維護信息: 0-非純色; 非0-純顏色. 當結點爲非純色時,才需要訪問其子結點查詢.
自上向下查詢比較方便,用普通版遞歸線段樹.
算法步驟
1.建樹
struct node
{
int l, r, color;
} tree[3 * N];
從上往下建樹,顏色初始化爲1.
2.更新[l, r]
區間顏色標記下放,從上往下lazy更新.
3. 查詢[l,r]
從上往下遞歸查詢,當結點爲非純色時,才向下查詢其子結點。
算法複雜度
建樹O(n), 查詢和修改O(logn)
總時間複雜度: O(Alogn) (A爲總操作數)
總空間複雜度: O(3 * n)
源程序
/*
* Author: HongSheng Zeng
* Email: [email protected]
* FileName: 2777.cpp
* Creation: 2014/09/07
*/
#include <iostream>
#include <cstdio>
#include <string.h>
using namespace std;
const int N = 100000 + 10;
struct node
{
int l, r, color;
} tree[3 * N];
bool mark[35];
void Build_Tree(int x, int l, int r)
{
tree[x].l = l;
tree[x].r = r;
tree[x].color = 1;
if (l == r)
return;
int mid = (l + r) / 2;
Build_Tree(x * 2, l, mid);
Build_Tree(x * 2 + 1, mid + 1, r);
}
void Update(int x, int l, int r, int color)
{
if (tree[x].l == l && tree[x].r == r) {
tree[x].color = color;
return;
}
// Down
if (tree[x].color != 0 && tree[x].color != color) {
tree[x * 2].color = tree[x].color;
tree[x * 2 + 1].color = tree[x].color;
tree[x].color = 0;
}
int mid = (tree[x].l + tree[x].r) / 2;
if (r <= mid)
Update(x * 2, l, r, color);
else if (l > mid)
Update(x * 2 + 1, l, r, color);
else {
Update(x * 2, l, mid, color);
Update(x * 2 + 1, mid + 1, r, color);
}
}
void Query(int x, int l, int r)
{
if (tree[x].color > 0) {
mark[tree[x].color] = true;
return;
}
int mid = (tree[x].l + tree[x].r) / 2;
if (r <= mid)
Query(x * 2, l, r);
else if (l > mid)
Query(x * 2 + 1, l, r);
else {
Query(x * 2, l, mid);
Query(x * 2 + 1, mid + 1, r);
}
}
int main()
{
int L, T, O, l, r, k;
char ch;
scanf("%d%d%d", &L, &T, &O);
memset(mark, false, sizeof(mark));
Build_Tree(1, 1, L);
while (O--) {
scanf(" %c%d%d", &ch, &l, &r);
if (l > r) {
l = l ^ r;
r = l ^ r;
l = l ^ r;
}
if (ch == 'C') {
scanf("%d", &k);
Update(1, l, r, k);
}
else {
memset(mark, false, sizeof(mark));
Query(1, l, r);
int sum = 0;
for (int i = 1; i <= T; ++i)
if (mark[i])
++sum;
printf("%d\n", sum);
}
}
}
評測系統上運行結果
Accepted. 運行時間 422ms, 佔用內存3764KB.