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

一些奇怪的树状数组技巧

先来个几个比较常用的:

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

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

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

常见于点分治题。

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

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

想清空时直接 (Timegets 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 imes (x-i+1) = (x+1)sum_{i=1}^{x} d_i - sum_{i=1}^{x} i imes d_i ]

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

例题!

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

  • 区间加 (1)
  • 区间求 (sumlimits_{i=l}^{r} i imes A_i)

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

[egin{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 imes dfrac{(x-k+2)(x-k+1)}{2} \ & = dfrac{1}{2}sum_{k=1} dd_k imes (k^2 -(3+2x)k+x^2+3x+2) end{aligned} ]

自然,你又只需要三个树状数组,分别维护 (sum dd_k)(sum k imes dd_k)(sum k^2 imes 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 = sumlimits_{i=k-lowbit(k)+1}^{k} a_i)

维护区间最值时,一个节点 (k) 维护的就是 (b_k = maxlimits_{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;

随时更新……

原文地址:https://www.cnblogs.com/imakf/p/14071208.html