本篇將介紹些 CP 值較高的資料結構
其中 C 代表的是 coding 複雜度,
或把 CP 譯作競程
RMQ 全名 Range Maximum/Minimum Query
給定長度
樸素的枚舉區間會有
的查詢複雜度
不失一般性,下面只以找最大值為例
定義狀態
例如
則 ,位置從 開始
最大值可從更小的長度
則狀態轉移為
而邊界為長度
for(int i = 0; i < n; i++) ST[i][0] = a[i];
for(int k = 1; k <= log2(n); k++)
for(int i = 0; i+(1<<k) <= n; i++)
ST[i][k] = max(ST[i][k-1], ST[i+(1<<k-1)][k-1]);
建構 Sparse Table 的時間複雜度為
如此能利用
注意這裡區間表示方式是左閉右開符號
ZEROJUDGE d539 區間 MAX
TIOJ 1603 胖胖殺蚯事件
線段樹是各種區間問題的絕招,相較其他延伸應用 本節只是冰山一角
建議先熟悉第三週的分治法以及第五週的合併排序法再閱讀本節
先考慮一個非常眼熟(?)的問題
給定數列
針對更新哪些值,以及查詢哪些解,精確的分類可分為:
而所謂要求的解(答案)可能有:
線段樹,顧名思義就是以保存很多區間解的一顆樹,例如數列長度
(注意這裡區間表示方式是左閉右開符號)
先明確的以簡單的問題設計簡單的線段樹
給定長度
例如
將
將
詢問區間
線段樹的節點保存該區間的解,以及左右子節點的位置
struct node {
int sum;
node *lc, *rc; // left child, right child
node() { sum = 0; lc = rc = NULL; }
};
根據給定數列把初始線段樹造出來
node* build(int L, int R) {
node* u = new node();
if(R-L == 1) { // 區間中只剩一個數,也就是葉節點
u->sum = a[L];
} else {
int M = (L+R) / 2;
u->lc = build(L, M); // 左子樹
u->rc = build(M, R); // 右子樹
u->sum = u->lc->sum + u->rc->sum;
}
return u;
}
以
區間旁的數代表總和,例如區間
將數列中某位置
void update(node* u, int L, int R, int i, int d) {
if(i < L || R <= i) return; // 未包含欲更新位置
u->sum += d;
if(R-L == 1) return; // 葉節點
int M = (L+R) / 2;
update(u->lc, L, M, i, d);
update(u->rc, M, R, i, d);
}
以前個樹為例,將
每次只往包含
查詢區間
int query(node* u, int L, int R, int qL, int qR) {
if(R <= qL || qR <= L) return 0; // 不在欲查詢區間內
if(qL <= L && R <= qR) return u->sum;
int M = (L+R) / 2;
return query(u->lc, L, M, qL, qR) + query(u->rc, M, R, qL, qR);
}
以前個樹為例,詢問區間
這樣的遞迴查詢,每一層最多只會往下遞迴兩個節點,所以複雜度為
利用反證法能證明當某層往下遞迴超過兩個節點會出現矛盾
HDU OJ 1166 敌兵布阵
CODEFORCES 339D Xenia and Bit Operations
*CODEFORCES 1253E Antenna Coverage
變得棘手一些的問題
給定長度
例如
將
詢問
可採用前面提的"單點更新/區間查詢"線段樹,
欲更新區間內所有數,對每數都單點更新,但總複雜度為
同理,這樣寫也可以做到區間更新:
void update(node* u, int L, int R, int qL, int qR, int d) {
if(R <= qL || qR <= L) return;
if(R-L == 1) {
u->sum += d; // 更新該單點
return;
}
int M = (L+R) / 2;
update(u->lc, L, M, qL, qR, d);
update(u->rc, M, R, qL, qR, d);
u->sum = u->lc->sum + u->rc->sum;
}
以
可是複雜度仍然為
顧名思義,就是懶
懶是工程師的美德
由於更新區間的總和同時也將其子節點一併更新,這麼勤快肯定會花不少時間
如果當前區間的數全都得更新,可先偷懶做個 tag 值表示下方的子孫節點還未更新上 tag,
等到真正需要查(遞迴)到其子孫節點,在將 tag 往下推給子節點,以完成更新
節點保存該區間的解及 tag,以及左右子節點的位置
struct node {
int sum, tag;
node *lc, *rc;
node() { sum = tag = 0; lc = rc = NULL; }
void pull() { sum = lc->sum + rc->sum; }
void push(int L, int R) {
lc->tag += tag;
rc->tag += tag;
int M = (L+R) / 2;
lc->sum += tag * (M-L);
rc->sum += tag * (R-M);
tag = 0;
}
};
pull()
將左右子區間的總和加起來push()
將該區間的 tag 給左右子區間,並完成子節點的更新void update(node* u, int L, int R, int qL, int qR, int d) {
if(R <= qL || qR <= L) return;
if(qL <= L && R <= qR) {
u->sum += d * (R-L);
u->tag += d;
return;
}
u->push(L, R);
int M = (L+R) / 2;
update(u->lc, L, M, qL, qR, d);
update(u->rc, M, R, qL, qR, d);
u->pull();
}
以
其中左上角的值代表 tag 值。
與沒有 lazy tag 的區間更新相比,走訪的區間少了許多
int query(node* u, int L, int R, int qL, int qR) {
if(R <= qL || qR <= L) return 0;
if(qL <= L && R <= qR) return u->sum;
u->push(L, R);
int M = (L+R) / 2;
return query(u->lc, L, M, qL, qR) + query(u->rc, M, R, qL, qR)
}
以前個樹為例,詢問區間
觀察到,在查詢過程中會將 tag 往下 push 給子區間
POJ 3468 A Simple Problem with Integers
CODEFORCES 52C Circular RMQ
又叫做 Fenwick Tree
翻譯為二元索引樹,可看作是線段樹的特化版本
整數可由一些
例如
每個數字可由這種方式唯一的表示。換句話說,兩個不同數字就得用不同方式表現
也就是說用
那麼用
證明二補數系統下運算 x&-x
是
設
有了以上的基礎,可以回到正題了
給定長度
若只有查詢操作,透過前綴和
但還得考慮更新操作,所以普通的前綴和是不行的
每次單點更新就得重新計算前綴和,複雜度會過高
根據前面基礎,用 for(; x >= 1; x-=lowbit(x)) sum += BIT[x]
求得
例如 sum
會得到 BIT[7] + BIT[6] + BIT[4]
其實就是把
兩者結合就是:BIT[
] + BIT[
] + BIT[
]
而其中 BIT[x]
就能構造為
例如
BIT
這種奇特區間定義能表示成一棵樹,例如
(觀察可知,這棵樹是把線段樹的右子樹通通移除的結果)
而若要更新數列的數,也要把包含此數的區間一併更新
例如更新位於位置
單點更新
void update(int p, int d)
{ for(; p <= N; p+=p&-p) BIT[p] += d; }
複雜度為
求前綴和
int pre(int p) {
int sum = 0;
for(; p >= 1; p-=p&-p) sum += BIT[p];
return sum;
}
複雜度為
練習:
CODEFORCES 356A Knight Tournament
CODEFORCES 459D Pashmak and Parmida's problem
CODEFORCES 1042D Petya and Array
TIOJ 1175 Longest Increasing Subsequence
TIOJ 1080 A.逆序數對
給定長度
應用差分(Difference)技巧令
且透過
則若是將所有數
那麼
也就是說
而其餘的
則利用兩次單點更新
void update(int p, int d)
{ for(; p <= N; p+=p&-p) BIT[p] += d; }
做 update(L, d)
及 update(R+1, -d)
就能完成區間更新了
根據差分定義推得
int pre(int p) {
int sum = 0;
for(; p >= 1; p-=p&-p) sum += BIT[p];
return sum;
}
做 pre(i)
能單點查詢
給定長度
根據差分定義有
能推得
能推得
依此類推得
於是,再多維護一個以數列