貓樹學習筆記


本文參考自算法發明者 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;

}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM