樹狀數組的妙用


作為一個常數非常小且非常好寫的數據結構,樹狀數組(Binary Index Tree, BIT)自然受到了很多選手的青睞。除了眾所周知的區間加區間求和,樹狀數組還能代替常數巨大的線段樹做不少事情,如維護高維差分或在 BIT 上二分,是卡常的不二選擇。

本篇文章主要介紹最近碰到的樹狀數組維護高維差分和 BIT 二分。以后碰到更多用法的時候會更新。

1. BIT 二分(倍增)

CSPS 2021 前學習一下,感覺會用到

樹狀數組的樹形結構決定了它可以倍增的性質。實際上,BIT 就是省略了右兒子的線段樹,因此線段樹的功能完全包含 BIT,但為此付出的代價是 \(2\sim 10\) 倍的常數。將 BIT 的樹狀結構牢記於心,可以更好理解 BIT 倍增:

下標為 \(p\) 的位置存儲着 \([l,p]\) 的信息和,其中 \(l=p-2^{\mathrm{lowbit}(p)}+1\),其長度為 \(p-l+1=2^{\mathrm{lowbit}(p)}\),這給予我們倍增的條件。具體流程如下:

維護一個信息前綴和 \(cur\) 與當前位置 \(p\),從大到小枚舉 \(2^k\leq n\),若 \(p+2^k\leq n\)\(cur+\mathrm{Information}(p+2^k)\) 符合條件,則令 \(p\gets p+2^k\),否則不變。最終得到的 \(p\) 一定是最大且符合條件的位置。

為什么這樣可以呢,注意到如果我們給 \(p\) 加上 \(2^k\) 滿足 \(k<\mathrm{lowbit}(p)\),則 \(\mathrm{Information}(p+2^k)\) 實際上維護了 \([p+1,p+2^k]\) 的信息和(顯然 \(\mathrm{lowbit}(p+2^k)=k\)),故算法正確。

當然,這樣的倍增所適用的信息仍然局限於 BIT 可以維護的信息范圍內,不過一般來說 \(\mathrm{sum}\) 已經足以應付大多數題目了。來看幾道例題。

I. P6619 [省選聯考 2020 A/B 卷] 冰火戰士

太經典了。

首先將溫度離散化,那么我們就是要找冰系戰士能力關於溫度的前綴和與火系戰士能力的后綴和在某個溫度處較小值的最大值。由於能力都是正整數因此前綴和單調遞增,后綴和單調遞減,考慮用樹狀數組維護冰火戰士能力前綴和(后綴和等於總和減去前綴和)然后二分找到最大的 \(p\) 使得 \(\mathrm{Icesum}_p\leq\mathrm{Firetotal}-\mathrm{Firesum}_{p-1}\),以及最大的 \(p\)(這個溫度要求最大就挺麻煩)使得 \(\mathrm{Icesum}_p\geq \mathrm{Firetotal}-\mathrm{Firesum}_{p-1}\)\(\mathrm{Icesum}_p\) 最小,兩者對比取更優解即可。

時間復雜度 \(\mathcal{O}(n\log n)\)。通過卡常拿到了最優解(10.23)。

const int N = 2e6 + 5;

int n, lg, cnt, d[N], op[N], t[N], x[N], y[N];
int c1[N], c2[N], Fire;
void add(int x, int v, int *c) {while(x <= cnt) c[x] += v, x += x & -x;}
void query() {
	int p = 0, v1 = 0, v2 = 0;
	for(int i = lg; ~i; i--) {
		int np = p + (1 << i);
		if(np <= cnt && v1 + c1[np] <= Fire - c2[np] - v2)
			p = np, v1 += c1[p], v2 += c2[p];
	}
	if(p < cnt) {
		int x = p + 1, w1 = 0, w2 = 0;
		while(x) w1 += c1[x], w2 += c2[x], x -= x & -x;
		if(v1 <= min(w1, Fire - w2)) {
			v1 = w1, v2 = w2, p = 0;
			for(int i = lg, w2 = 0; ~i; i--) {
				int np = p + (1 << i);
				if(np <= cnt && w2 + c2[np] <= v2) w2 += c2[p = np];
			}
		}
	}
	int ans = min(v1, Fire - v2);
	if(ans) print(d[p]), pc(' '), print(ans << 1), pc('\n');
	else pc('P'), pc('e'), pc('a'), pc('c'), pc('e'), pc('\n');
}

int main(){
	n = read(), lg = log2(n);
	for(int i = 1; i <= n; i++) {
		op[i] = read(), t[i] = read();
		if(op[i] == 1) d[i] = x[i] = read(), y[i] = read();
	} sort(d + 1, d + n + 1), cnt = unique(d + 1, d + n + 1) - d - 1;
	for(int i = 1; i <= n; i++) {
		if(op[i] == 1) {
			x[i] = lower_bound(d + 1, d + cnt + 1, x[i]) - d;
			if(t[i] == 1) Fire += y[i], add(x[i] + 1, y[i], c2);
			else add(x[i], y[i], c1);
		} else {
			int p = t[i];
			if(t[p] == 1) Fire -= y[p], add(x[p] + 1, -y[p], c2);
			else add(x[p], -y[p], c1);
		} query();
	}
    return flush(), 0;
}

II. 2021NOIP 聯考 石室中學 T3 集合

題意簡述:維護一個數堆 \(a_i\),支持插入一個數或詢問 \(c\),求出最多進行多少次選出 \(c\) 個數並減去 \(1\)

\(1\leq a_i\leq 10^9\)\(1\leq n\leq 10^6\)

一個比較顯然的想法是每次選出最大的 \(c\) 個數減去 \(1\)(賽時只想到了這一點)。

有這樣一個結論:找出最大的數 \(v\) 以及剩下數的和 \(s\),若 \(v>\dfrac s {c-1}\),則 \(v\) 每次必然被選,令 \(c\gets c-1\) 並丟掉 \(v\)。否則 \(v\leq \dfrac{s}{c-1}\),每次操作后仍然滿足這個關系,因此答案就是 \(\dfrac{s+v}{c}\)

如果將所有數從大到小排序,我們就是要找最后一個位置 \(i\) 使得 \(a_i> \dfrac{\mathrm{suffixsum}_{i+1}}{c-i}\),稍做變形得到 \(c> \dfrac{\mathrm{suffixsum}_{i+1}}{a_i}+i\)。注意到不等號右邊的柿子兩部分在任何時候都是隨着 \(i\) 增加而(非)嚴格遞增的,因此若 \(i\) 滿足要求,則任意 \(j<i\) 都滿足要求,滿足可二分性。同時增加一個數相當於后綴加,可以 BIT 維護,故使用 BIT 倍增。

具體地,我們倍增找到最靠右的位置 \(p\) 使得 \(c>\dfrac{\mathrm{suffixsum}_{p+1}}{a_p}+p\),那么 \(\dfrac{\mathrm{totalsum} - \mathrm{prefixsum}_p}{c-p}\) 就是答案。時間復雜度 \(\mathcal{O}(n\log n)\)

const int N = 1e6 + 5;
int n, u, q, lg, rk[N], qu[N];
ll cnt[N], sum[N];
pii a[N];

int main() {
	cin >> n, lg = log2(n);
	for(int i = 1; i <= n; i++) {
		int op = read();
		if(op == 1) a[++u] = {read(), i};
		else qu[++q] = read();
	}
	sort(a + 1, a + u + 1, [&](pii x, pii y) {return x.fi > y.fi;});
	for(int i = 1; i <= u; i++) rk[a[i].se] = i;
	for(ll i = 1, p = 0, q = 0, tot = 0; i <= n; i++) {
		if(rk[i]) {
			int x = rk[i], v = a[x].fi; q++, tot += v;
			while(x <= u) cnt[x]++, sum[x] += v, x += x & -x;
		} else {
			ll c = qu[++p], x = 0;
			ll csum = 0, ccnt = 0;
			for(int i = lg; ~i; i--) {
				ll nx = x + (1 << i);
				if(nx > n) continue;
				ll v = a[nx].fi;
				ll nsum = csum + sum[nx];
				ll ncnt = ccnt + cnt[nx];
				if(ncnt >= c) continue;
				if(c * v > tot - nsum + ncnt * v)
					x = nx, csum = nsum, ccnt = ncnt;
			}
			print((tot - csum) / (c - ccnt)), pc('\n'); 
		}
	}
	return flush(), 0;
}


免責聲明!

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



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