树状数组

\(\text{loj-130}\)

给定数列 \(a_1, a_2, \dots, a_n\),你需要依次进行 \(q\) 个操作,操作有两类:

  • 1 i x:给定 \(i,x\),将 \(a_i\) 加上 \(x\)
  • 2 l r:给定 \(l,r\),求 \(\sum_{i=l}^r a_i\) 的值(换言之,求 \(a_l+a_{l+1}+\dots+a_r\) 的值)。

\(1 \le n,q \le 10^6\)\(|a_i|,|x| \le 10^6\)\(1 \le l \le r \le n\)


树状数组板子。

树状数组是这样的,可以完成单点修改、区间查询或者区间修改、单点查询,不能混用。

需要一个计算 \(\text{lowbit}\) 的函数:

long long lowbit(long long x) { return x & (-x); }

接着是单点修改:

void add(long long x, long long k) {
	while(x < MAXN) t[x] += k, x += lowbit(x);
	return;
}

区间查询(实际上是前缀和查询,但可以 \(\text{query}(r) - \text{query}(l - 1)\) 做到区间查询):

long long query(long long x) {
	long long res = 0;
	while(x) res += t[x], x -= lowbit(x);
	return res;
}

树状数组的优势就是简单易懂、码量少,但是缺点也很明显,功能太少了。

\(\text{loj-10114}\)

给定 \(n\) 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目。

点按 \(y\) 坐标增序给出,\(y\) 坐标相同的按 \(x\) 坐标增序给出。

统计每个等级有多少个点。

\(1 \le n \le 1.5 \times 10^4\)\(0 \le x,y \le 3.2 \times 10^4\)


对于 \(n\) 个点遍历,维护当前在 \(x\) 轴上 \([0, p]\) 区间的点数即可。

用树状数组维护,单点修改、区间查询。

注意:树状数组维护的左端点只能从 \(1\) 开始,所以需要将每个坐标 \(+1\) 处理。

\(\text{loj-10115}\)

校门外有很多树,学校决定在某个时刻在某一段种上一种树,保证任一时刻不会出现两段相同种类的树,现有两种操作:

  • 1 l r 表示在 \([l,r]\) 种上一种树,每次操作种的树的种类都不同;
  • 2 l r 表示询问 \([l,r]\) 区间内树的种类数。

注意:每个位置都可以重复种树。

\(1 \le n,m \le 5 \times 10^4\)\(1 \le l,r \le n\)


显然是树状数组的题,刚开始想的直接维护区间修改、单点查询,但发现好像没这么简单。

例如 \([1,3]\) 种上树,询问 \([2,5]\) 时,\([1,5]\) 共有一种树,\([1,1]\) 也有一种树,相减显然不是答案。

那么怎么处理呢?让我想想。

考虑把左右端点分开维护,开两个树状数组,分别维护 \([1,x]\) 左端点数目 \(t_{x,0}\) 和右端点数目 \(t_{x,1}\)

比如 \([2,6]\)\([5,10]\) 种上树,询问 \([8,12]\) 时,\([1,12]\) 左端点数目有 \(2\) 个,而 \([1,7]\) 右端点数目有 \(1\) 个,说明有一种树完全在给定区间的左边,减去即可。

因此答案为 \(t_{r,0} - t_{l-1,1}\),这样就处理完了。

\(\text{loj-10117}\)

有一个长度为 \(n\)\(01\) 数组 \(a_i\),初始值均为 \(0\)。给定 \(m\) 条指令:

  • 1 l r 表示将 \([l,r]\) 区间内的数反转。
  • 2 x 表示询问 \(a_x\) 的值。

\(1 \le n \le 10^5\)\(1 \le m \le 5 \times 10^5\)\(1 \le l \le r \le n\)


简单的树状数组维护区间修改、单点查询,\(a_i\) 被反转奇数次为 \(1\),否则为 \(0\)

\(\text{loj-133}\)

给出一个 \(n\times m\) 的零矩阵 \(A\),你需要完成如下操作:

  • 1 x y k:表示元素 \(a_{x,y}\) 自增 \(k\)
  • 2 a b c d:表示询问左上角为 \((a,b)\),右下角为 \((c,d)\) 的子矩阵内所有数的和。

\(1 \le n,m \le 2^{12}\)\(1 \le x,a,c \le n\)\(1 \le y,b,d \le m\)\(|k| \le 10^5\)

保证操作次数不超过 \(3 \times 10^5\),且询问的子矩阵存在。


二维树状数组板子。

其实跟一维的差不太多,直接贴代码了:

long long lowbit(long long x) { return x & (-x); }
// (x,y) ~ (n,m) +1
void add(long long x, long long y, long long k) {
	while(x <= n) {
		long long py = y;
		while(py <= m) t[x][py] += k, py += lowbit(py);
		x += lowbit(x);
	}
	return;
}
// 求 (1,1) ~ (x,y) 的和
long long query(long long x, long long y) {
	long long res = 0;
	while(x) {
		long long py = y;
		while(py) res += t[x][py], py -= lowbit(py);
		x -= lowbit(x);
	}
	return res;
}

\(\text{luogu-1972}\)

给定一个长度为 \(n\) 的序列 \(a\),有 \(m\) 次询问,每次询问给定 \([l,r]\),回答区间内不同数的个数。

\(1 \le n, m, a_i \le 10^6\)\(1 \le l \le r \le n\)


把询问离线下来,挂到右端点上。值域树状数组维护 \(x\) 最后一次出现的下标。

那么扫到 \(r_i\) 时,询问 \([l_i, r_i]\) 的答案就是 \(\text{qry}(r) - \text{qry}(l-r)\)

然后就做完了。

#include<iostream>
#include<cstdio>
#include<vector>
using namespace std;
#define ll long long
#define MAXN 1000005
#define pii pair<ll, ll>
#define fi first
#define se second 

ll read() {
	ll x = 0, f = 1;
	char c = getchar();
	while(c > 57 || c < 48) { if(c == 45) f = -1; c = getchar(); }
	while(c >= 48 && c <= 57) { x = (x << 1) + (x << 3) + (c - 48); c = getchar(); }
	return x * f;
}

ll n, m, a[MAXN], lst[MAXN], ans[MAXN], t[MAXN];
vector<pii > v[MAXN];

ll lowbit(ll x) { return x & (-x); }
void add(ll x, ll k) { while(x < MAXN) t[x] += k, x += lowbit(x); return; }
ll qry(ll x) { ll res = 0; while(x) res += t[x], x -= lowbit(x); return res; } 

int main() {
	n = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	m = read();
	for(int i = 1; i <= m; i ++) {
		ll l = read() - 1, r = read();
		v[r].push_back({l, i});
	}
	for(int i = 1; i <= n; i ++) {
		if(!lst[a[i]]) add(i, 1);
		else add(lst[a[i]], -1), add(i, 1);
		lst[a[i]] = i;
		for(auto it : v[i]) ans[it.se] = qry(i) - qry(it.fi);
	}
	for(int i = 1; i <= m; i ++) cout << ans[i] << "\n";
	return 0;
}
posted @ 2025-11-16 21:00  So_noSlack  阅读(5)  评论(0)    收藏  举报