本文參考自算法發明者 immortalCO(貓錕) 的博客 一種高效處理無修改區間或樹上詢問的數據結構(附代碼) 。
感謝 貓錕 提供了對於一類題比較通用的解決辦法,以及思路啟發。
問題描述
給出一個某種元素的序列 \(a_1,a_2,\dots ,a_n\),要求進行 \(m\) 次詢問,每一次是詢問一段區間 \([l,r]\) 的某種支持結合律和快速合並的信息,要求在線。
這類問題比較通用,比如在 DP 的優化中就常常見到。
算法實現
算法介紹
對於常規問題,比如區間最值,區間最大子段和。我們常常能用線段樹等數據結構達到,構造 \(O(n)\) ,詢問 \(O(\log n)\) 的時間復雜度。
對於這些做法,只有一點不好,詢問復雜度 不夠優秀,且對於一些特定問題,線段樹的 push_up
合並也不好寫。
但對於區間最值這類的問題, 我們可以類似 \(RMQ\) 那樣,在一般的問題上,以預處理的時間和空間,換取快速的詢問。
我們首先考慮詢問一個區間 \([q_l, q_r]\) 。如果 \(q_l = q_r\) ,就可以直接得到答案。否則會不斷在線段樹上定位,而且會在幾個區間的中點 \(mid\) 處被分開。
我習慣於線段樹每個區間維護的一個閉區間 \([l, r]\) ,其中中點 \(\displaystyle mid = \lfloor \frac{l + r}{2} \rfloor\) 。
考慮第一次被分開的位置,假設為 \(p\) 。那么原來的區間 \([q_l, q_r]\) 就被分為 \([q_l, mid]\) ,與 \([mid + 1, q_r]\) 。
我們考慮對於每一個 \(mid\) 預處理他向前的后綴 \([i, mid]\) 的答案(其中 \(l \le i \le mid\)) ,以及他向后的前綴 \([mid + 1, j]\) (其中 \(mid + 1 \le j \le r\))。
如果我們知道了 \(p\) 點所在的位置,我們可以直接利用之前預處理的 \([q_l, mid]\) 以及 \([mid + 1, q_r]\) 的答案直接合並即可。
不難發現預處理的復雜度是 \(O(n \log n)\) 的(對於每一層每個數剛好被考慮一次 \(O(n) \times O(\log n) = O(n \log n)\))
然后怎么快速知道這個位置呢?
不難發現這個 \(p\) 的位置,就是線段樹上對應 \([l, l]\) 和 \([r, r]\) 節點的 \(lca\) (最近公共祖先)所處的位置,我們可以考慮用 \(st\) 表預處理,然后可以直接查詢 \(p\) 的位置,但這樣顯然太麻煩了。
如果整棵線段樹滿足堆式存儲(也就是對於點 \(i\) ,它的兩個兒子分別為 \(2 i\) 和 \(2i + 1\) ),就有一個很好的性質。
對於任意兩個同深度的點,他們的 \(lca\) 是他們二進制下 \(lcp\) (最長公共前綴)。
這個是十分顯然的,因為對於兩個深度相同的點,他們第一次分開的位置,必然導致當前最后一位的二進制不同,而前面都是相同的。
我們把整棵樹建成一個滿二叉樹 \([1, 2^k]\),那么對於任意一個區間 \([i, i]\) 都是滿足他們的深度是最深且在同一層的。
注意對於不同深度的點不一定滿足這個情況!!這就是為什么我們為什么要建滿的原因。
然后對於兩個數 \(x, y\) 的二進制下的 \(lcp\) 為 x >> Log2[x ^ y]
。(這個很顯然,丟掉第一個不相同的位后面的所有位就行了)
這樣我們就可以實現詢問 \(O(1)\) 啦。
我們稱這個數據結構為 貓樹 。
算法本質
看了一下 UOJ 評論區。。。
其實就是將分治進行離線,我們用一個東西來存儲這個分治結構,以及前面按位置分治的答案。
所以這個算法最重要的還是,尋找特定問題的分治方案。
例題講解
區間最大子段和
題意
給你一個序列 \(a_i\) ,有 \(m\) 次詢問,每次詢問一個區間 \([l, r]\) ,表示詢問這段區間的最大子段和。
題解
如果沒有區間,那么這個是個經典的分治問題。
最大子段和,要么完全在左區間,要么完全在右區間,要么跨越中點。
所以我們只需要預處理 \([i, mid]\) 的最大前綴和與 \([mid + 1, j]\) 的最大前綴和,這個一邊遍歷一邊取 \(\max\) 。
以及這兩個區間的最大子段和。至於那個最大子段和,可以利用前綴和相減,保存一個前綴和最小值就行了。
這個算法比標准線段樹上合並信息,要好寫並且更快。
代碼
對於第一道題,還是建議看看代碼怎么寫的。。(瓶頸在輸入輸出上也是沒誰啦)
#include <bits/stdc++.h>
#define For(i, l, r) for(register int i = (l), i##end = (int)(r); i <= i##end; ++i)
#define Fordown(i, r, l) for(register int i = (r), i##end = (int)(l); i >= i##end; --i)
#define Set(a, v) memset(a, v, sizeof(a))
#define Cpy(a, b) memcpy(a, b, sizeof(a))
#define debug(x) cout << #x << ": " << x << endl
#define DEBUG(...) fprintf(stderr, __VA_ARGS__)
using namespace std;
inline bool chkmin(int &a, int b) {return b < a ? a = b, 1 : 0;}
inline bool chkmax(int &a, int b) {return b > a ? a = b, 1 : 0;}
inline int read() {
int x = 0, fh = 1; char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') fh = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * fh;
}
inline void Out(int x) {
static char sta[18], top, flag = false;
if (!x) { puts("0"); return ; }
sta[top = 1] = '\n';
if (x < 0) flag = true, x = -x;
for (; x; x /= 10) sta[++ top] = (x % 10) + 48;
if (flag) putchar ('-'), flag = false;
while (top) putchar (sta[top --]);
}
void File() {
#ifdef zjp_shadow
freopen ("1043.in", "r", stdin);
freopen ("1043.out", "w", stdout);
#endif
}
const int N = 50100, Maxn = N * 4, MaxLog = 20, inf = 0x7f7f7f7f;
int pos[Maxn], val[Maxn], Log2[Maxn], maxlen;
namespace CatTree {
inline int Max(int a, int b) { return a > b ? a : b; }
int Pre[MaxLog][Maxn], Sum[MaxLog][Maxn];
void Build(int o, int l, int r, int dep) {
if (l == r) { pos[l] = o; return ; }
int mid = (l + r) >> 1, sum, minv;
Sum[dep][mid] = Pre[dep][mid] = sum = minv = val[mid]; chkmin(minv, 0);
Fordown(i, mid - 1, l) {
Pre[dep][i] = Max(Pre[dep][i + 1], sum += val[i]);
Sum[dep][i] = Max(Sum[dep][i + 1], sum - minv);
chkmin(minv, sum);
}
Sum[dep][mid + 1] = Pre[dep][mid + 1] = sum = minv = val[mid + 1]; chkmin(minv, 0);
For (i, mid + 2, r) {
Pre[dep][i] = Max(Pre[dep][i - 1], sum += val[i]);
Sum[dep][i] = Max(Sum[dep][i - 1], sum - minv);
chkmin(minv, sum);
}
Build(o << 1, l, mid, dep + 1);
Build(o << 1 | 1, mid + 1, r, dep + 1);
}
inline int Query(int l, int r) {
if (l == r) return val[l];
register int dep = Log2[pos[l]] - Log2[pos[l] ^ pos[r]];
return Max(Max(Sum[dep][l], Sum[dep][r]), Pre[dep][l] + Pre[dep][r]);
}
}
int n;
int main() {
File();
n = read();
For (i, 1, n) val[i] = read();
for (maxlen = 1; maxlen < n; maxlen <<= 1);
CatTree :: Build(1, 1, maxlen, 1);
For (i, 2, maxlen << 1) Log2[i] = Log2[i >> 1] + 1;
for (register int m = read(), l, r; m; -- m) {
l = read(), r = read();
Out(CatTree :: Query(l, r));
}
#ifdef zjp_shadow
cerr << (double) clock() / CLOCKS_PER_SEC << endl;
#endif
return 0;
}