【笔记】 对不起,我不会树状数组

一些奇怪的树状数组技巧

先来个几个比较常用的:

\(O(n)\) 初始化树状数组

不要做 \(O(n)\) 次单点修改,类似拓扑排序从 \(1 \to n\) 一个一个向直接祖先上传答案就行了。

\(O(1)\) 清空树状数组

常见于点分治题。

做法是给每个树状数组的节点设置一个时间戳 \(t_i\),并记录当前时间戳 \(Time\)

  • 如果当前时间戳与节点时间戳相同,那么该节点的值是正确的;
  • 如果不同,这个位置上的值将被重置为 \(0\),并更新时间戳 \(t_i \gets Time\)

想清空时直接 \(Time\gets Time+1\) 就行了。

树状数组二分

例题!

支持单点修改,查询区间最后一个前缀和 \(\le k\) 的位置。

只需对 bit 代码稍作修改即可。


class BIT{
	int data[MX]
	void add(int pos ,int val){
		while(pos <= MX){
			data[pos] += val;
			pos += pos & -pos;
		}
	}
	int lower_bound(int s){
		int sum = 0 ,Ans = 0;
		for(int i = lg2[MX] ; ~i ; --i){
			Ans += 1 << i;
			if(Ans >= MX || sum + data[Ans] > s)	Ans -= 1 << i;
			else sum += data[Ans];
		}return Ans + 1;
	}
}C;

复杂度 \(O(\log n)\)

树状数组区间加区间求和

这是一个经典的问题,但新手(指我自己)往往无法想到如何解决。

树状数组维护区间加,单点求和是这个问题的弱化版,做法是维护原数组的差分数组 \(d\)

一次区间加只要修改差分数组的 \(2\) 个位置。

一次单点查询只要查询差分数组的前缀和即可。

那么区间和怎么求呢?显然我们可以做 \(O(n)\) 次单点求值。

\[S_x = \sum_{i=1}^{x} \sum_{j=1}^{i} d_j \]

这样就得到了数组的原前缀和,两个前缀和相减就是区间和。

不过这个式子显然可以化简为:

\[S_x = \sum_{i=1}^{x} d_i \times (x-i+1) = (x+1)\sum_{i=1}^{x} d_i - \sum_{i=1}^{x} i\times d_i \]

你非常惊讶地发现只要维护两个树状数组,一个存 \(\sum d_i\),另一个存 \(\sum i \times d_i\) 就可以了。

例题!

维护一个数组 \(A\),支持:

  • 区间加 \(1\)
  • 区间求 \(\sum\limits_{i=l}^{r} i \times A_i\)

这相当于是区间加等差数列,不妨考虑维护二阶差分序列 \(dd\)。再通过二阶前缀和的方法求出来原数组。

\[\begin{aligned} S_x & = \sum_{i=1}^{x} \sum_{j=1}^{i} d_j \\ & = \sum_{i=1}^{x} \sum_{j=1}^{i} \sum_{k=1}^{j} dd_k \\ & = \sum_{k=1}^{x} dd_k \sum_{j=k}^{x} \sum_{i=j}^{x} 1 \\ & = \sum_{k=1}^x dd_k \times \dfrac{(x-k+2)(x-k+1)}{2} \\ & = \dfrac{1}{2}\sum_{k=1} dd_k \times (k^2 -(3+2x)k+x^2+3x+2) \end{aligned} \]

自然,你又只需要三个树状数组,分别维护 \(\sum dd_k\)\(\sum k \times dd_k\)\(\sum k^2 \times dd_k\) 就行啦!

不过常数好像有点大呢(笑)

一份示例代码

struct BIT{
	LL data[MX];
	void add(int x ,LL v){
		while(x < MX){
			data[x] = (data[x] + v + MOD) % MOD;
			x += x & -x;
		}
	}
	LL sum(int x){
		LL s = 0;
		while(x > 0){
			s = (s + data[x]) % MOD;
			x -= x & -x;
		}
		return s;
	}
};

struct BIT3{
	BIT A ,B ,C;
	void add_d(int x ,LL v){
		A.add(x ,v);
		B.add(x ,x * v % MOD);
		C.add(x ,x * v % MOD * x % MOD);
	}
	void add(int l ,int r ,LL v){
		add_d(l ,l * v % MOD);
		add_d(l + 1 ,(1 - l + MOD) * v % MOD);
		add_d(r + 1 ,(MOD - r - 1) * v % MOD);
		add_d(r + 2 ,r * v % MOD);
	}
	LL sum(LL x){
		LL ans = (x * x + 3 * x + 2) % MOD * A.sum(x) % MOD;
		ans = (ans - (3 + 2 * x) % MOD * B.sum(x) % MOD + MOD) % MOD;
		ans = (ans + C.sum(x)) % MOD;
		ans = (ans * iv) % MOD;
                  // iv 指的是 2 的逆元
		return ans;
	}
	LL sum(int l ,int r){
		return (sum(r) - sum(l - 1) + MOD) % MOD;
	}
}C3;

树状数组单点修改区间最值

前排提醒:复杂度只能做到修改 \(O(\log ^2n)\),查询 \(O(\log ^2 n)\),比较鸡肋。

修改操作

类比维护区间和时候的操作,一个节点 \(k\) 维护的是 \(b_k = \sum\limits_{i=k-lowbit(k)+1}^{k} a_i\)

维护区间最值时,一个节点 \(k\) 维护的就是 \(b_k = \max\limits_{i=k-lowbit(k)+1}^{k}a_i\)

但,区间最值与区间和的区别就在于取最值没有逆运算,不可以通过差分的方式得到区间的答案。

但好在,我们依然可以让树状数组像线段树,通过子树的信息更新当前节点的信息。

树状数组上一个节点有最多 \(O(\log n)\) 个孩子(不知道为什么的 emmm,建议重学 BIT),单点修改最多会更新 \(O(\log n)\) 个节点。复杂度 \(O(\log^2 n)\)

查询操作

此时我们访问 \(b\) 数组的复杂度是 \(O(1)\) 的,所以不要有什么忌惮啦!

显然最值操作不能差分。

有一种想法是像线段树那样提取出树状数组上的一些节点,把它们的答案拼成整体答案。

我们直接实现这个做法就可以了,假设询问区间为 \([l,r]\),设查询函数为 int qmax(int l ,int r):

  • 如果 \(r-lowbit(r)<l-1\),返回 \(\max(a_r,\operatorname{qmax}(l,r-1))\)
  • 否则返回 \(\max(b_r , \operatorname{qmax}(l,r-lowbit(r)))\)

容易发现,我们会在 \(O(\log n)\) 次递归后将最高位降低至少一位,所以复杂度是 \(O(\log^2 n)\)

代码

struct BIT{
	int data[MX] ,a[MX];
	void set(int x ,int v){
		a[x] = v;
		while(x < MX){
			data[x] = v;
			for(int i = 1 ; i < (x & -x) ; i <<= 1){
				data[x] = std::max(data[x] ,data[x - i]);
			}
			x += x & -x;
		}
	}
	int qmax(int l ,int r){
		int ans = INT_MIN;
		while(l <= r){
			if(r - (r & -r) < l - 1){
				ans = std::max(ans ,a[r]);
				--r;
			}
			else{
				ans = std::max(ans ,data[r]);
				r -= r & -r;
			}
		}
		return ans;
	}
}C;

随时更新……

posted @ 2020-12-01 22:20  Imakf  阅读(202)  评论(2编辑  收藏  举报