线段树

\(\text{hdu-1166}\)

给定一个长度为 \(n\) 的序列 \(a_i\),有以下三种操作:

  1. Add x k 表示将 \(a_x \gets a_x + k\)
  2. Sub x k 表示将 \(a_x \gets a_x - k\)
  3. Query l r 表示询问 \(a_l + a_{l+1} + \dots + a_r\)

\(1 \le n \le 5 \times 10^4\),操作次数 \(\le 4 \times 10^4\)


最基本的线段树板子。甚至可以用树状数组写,但是这里记录线段树的写法。

首先建树,没什么好写的,就递归下去就好了:

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r;
	if(l == r) { t[p].res = a[l]; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	return;
}

然后查询操作:

long long query(long long p, long long l, long long r) {
	if(t[p].l >= l && t[p].r <= r) return t[p].res;
	long long mid = (t[p].l + t[p].r) >> 1, res = 0;
	if(l <= mid) res += query(p << 1, l, r);
	if(r > mid) res += query(p << 1 | 1, l, r);
	return res;
}

更新操作:

void update(long long p, long long x, long long k) {
	if(t[p].l == t[p].r) { t[p].res += k; return; }
	long long mid = (t[p].l + t[p].r) >> 1;
	if(x <= mid) update(p << 1, x, k);
	else update(p << 1 | 1, x, k);
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	return;
}

注意:查询操作里右子树的判断条件是 r > mid,显然不能取等。

\(\text{uestc-1918}\)

给定一个长度为 \(n\) 的序列 \(a_i\),初始值都为 \(0\),有 \(m\) 次操作:

  1. 0 x y 表示将 \(a_x \gets y\)
  2. 1 l r 表示询问 \(a_l,a_{l+1},\dots,a_r\) 去除一个最大值和一个最小值之后的和。

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


显然维护区间和、\(\max\)\(\min\) 即可。

\(\text{uestc-1933}\)

给定一个长度为 \(n\) 的序列 \(a_i\),初始值都为 \(0\),有 \(m\) 次操作:

  1. 0 l r w 表示将 \([l, r]\) 区间内的数 \(+w\)
  2. 1 l r 0 表示询问 \([l, r]\) 的区间和。

\(1 \le n,m \le 10^6\)\(1 \le l \le r \le n\)\(|w| \le 10^3\)


区间修改就需要懒标记了,对于每个节点维护一个 \(lz\) 即可,需要写 \(\text{pushdown}\) 函数下传:

void pushdown(long long p) {
	long long w = t[p].lz;
	t[p << 1].lz += w, t[p << 1 | 1].lz += w;
	t[p << 1].res += w * (t[p << 1].r - t[p << 1].l + 1); 
	t[p << 1 | 1].res += w * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1); 
	t[p].lz = 0;
	return;
}

修改操作也需要稍微变一下:

void update(long long p, long long l, long long r, long long w) {
	if(t[p].l >= l && t[p].r <= r) {
		t[p].res += w * (t[p].r - t[p].l + 1);
		t[p].lz += w;
		return;
	}
	long long mid = (t[p].l + t[p].r) >> 1;
	if(t[p].lz) pushdown(p);
	if(l <= mid) update(p << 1, l, r, w);
	if(r > mid) update(p << 1 | 1, l, r, w);
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	return;
}

注意:下传懒标记为累加操作 t[p << 1].lz += w,而非赋值。且记得调用 \(\text{pushdown}\)

\(\text{OpenJ_Bailian-3439}\)

跟上一题一摸一样,不写了。

\(\text{zzfls-P122}\)

给定一个长度为 \(n\) 的序列 \(a_i\),每个元素初始值为 \(1\)。同时有 \(n\) 个灯泡对应序列的每个位置,初始状态下都是亮着的,有以下三种操作:

  • 1 p 表示按下位置 \(p\) 对应的灯泡开关。
  • 2 l r x 表示将区间 \([l, r]\) 内所有亮着的灯泡对应位置上的值 \(\times x\)
  • 3 l r x 判断区间 \([l, r]\) 内的数是否都是 \(x\) 的倍数。若是,将区间内所有亮着的灯泡对应位置上的值 \(\div x\);否则什么都不做。

对于每个操作 \(3\) 回答是否成功进行了操作,YES/NO

\(1 \le n, m \le 10^5\)\(1 \le x \le 30\)\(1 \le p \le n\)\(1 \le l \le r \le n\)


这道题实际上就是线段树维护了区间 \(\min\),然后只有区间加减操作。很适合练手。

模拟赛 \(\text{B}\) 题,大概是个蓝。不过赛时没写正解,就写了个暴力。这里写一下正解的思路。

考虑没有 \(1\) 操作的部分分。注意到 \(x\) 很小,所以 \(a_i\) 的质因数种类不多,只有 \(10\) 种。

对于每个质因数开线段树,维护每个位置当前的质因数次数,区间 \(\min\),则等价于区间加、区间减。

如果加上 \(1\) 操作,我们线段树上分开维护每个区间内”亮着的灯“和”暗着的灯“对应位置的次数 \(\min\)。那么对于 \(1\) 操作等价于对一个叶子节点交换了这两个值,不难维护。

时间复杂度为 \(\Theta (\pi (x)n \log n)\)

补了一下题,居然一遍过样例,一遍 \(\text{AC}\)。贴一下代码吧:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 100005
#define INF 0x3f3f3f3f

long long read() {
	long long 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;
}

struct node { long long l, r, m0, m1, lz; } t[11][MAXN << 2];
long long n, m, op, l, r, x, p[11];

void pushup(long long x, long long p) {
	t[x][p].m1 = min(t[x][p << 1].m1, t[x][p << 1 | 1].m1); 
	t[x][p].m0 = min(t[x][p << 1].m0, t[x][p << 1 | 1].m0); 
	return;
}

void build(long long x, long long p, long long l, long long r) {
	t[x][p].l = l, t[x][p].r = r;
	if(l == r) { t[x][p].m0 = INF; return; }
	long long mid = (l + r) >> 1;
	build(x, p << 1, l, mid);
	build(x, p << 1 | 1, mid + 1, r);
	pushup(x, p); return;
}

void pushdown(long long x, long long p) {
	long long w = t[x][p].lz;
	t[x][p << 1].lz += w, t[x][p << 1 | 1].lz += w;
	t[x][p << 1].m1 += w, t[x][p << 1 | 1].m1 += w;
	t[x][p].lz = 0;
	return;
}

void upd(long long x, long long p, long long k) {
	if(t[x][p].l == t[x][p].r) { swap(t[x][p].m0, t[x][p].m1); return; }
	long long mid = (t[x][p].l + t[x][p].r) >> 1; pushdown(x, p);
	if(k <= mid) upd(x, p << 1, k);
	else upd(x, p << 1 | 1, k);
	pushup(x, p); return;
}

void update(long long x, long long p, long long l, long long r, long long k) {
	if(t[x][p].l >= l && t[x][p].r <= r) {
		t[x][p].m1 += k, t[x][p].lz += k;
		return;
	}
	long long mid = (t[x][p].l + t[x][p].r) >> 1; pushdown(x, p);
	if(l <= mid) update(x, p << 1, l, r, k);
	if(r > mid) update(x, p << 1 | 1, l, r, k);
	pushup(x, p); return;
}

long long query(long long x, long long p, long long l, long long r) {
	if(t[x][p].l >= l && t[x][p].r <= r) return min(t[x][p].m0, t[x][p].m1);
	long long mid = (t[x][p].l + t[x][p].r) >> 1, res = INF; pushdown(x, p);
	if(l <= mid) res = min(res, query(x, p << 1, l, r));
	if(r > mid) res = min(res, query(x, p << 1 | 1, l, r));
	return res;
}

int main() {
	freopen("tempIate.in", "r", stdin);
	freopen("tempIate.out", "w", stdout);
	
	n = read(), m = read();
	p[1] = 2, p[2] = 3, p[3] = 5, p[4] = 7, p[5] = 11;
	p[6] = 13, p[7] = 17, p[8] = 19, p[9] = 23, p[10] = 29;
	for(int i = 1; i <= 10; i ++) build(i, 1, 1, n);
	while(m --) {
		op = read();
		if(op == 1) {
			x = read();
			for(int i = 1; i <= 10; i ++) upd(i, 1, x);
		}
		else if(op == 2) {
			l = read(), r = read(), x = read();
			for(int i = 1; i <= 10; i ++) {
				long long cnt = 0;
				while(!(x % p[i])) x /= p[i], cnt ++;
				if(!cnt) continue;
				update(i, 1, l, r, cnt);
			}
		}
		else {
			l = read(), r = read(), x = read();
			bool fg = true; long long y = x;
			for(int i = 1; i <= 10; i ++) {
				long long cnt = 0;
				while(!(y % p[i])) y /= p[i], cnt ++;
				if(!cnt) continue;
				if(query(i, 1, l, r) < cnt) { fg = false; break; }
			}
			if(!fg) { cout << "NO\n"; continue; }
			cout << "YES\n";
			for(int i = 1; i <= 10; i ++) {
				long long cnt = 0;
				while(!(x % p[i])) x /= p[i], cnt ++;
				if(!cnt) continue;
				update(i, 1, l, r, -cnt);
			}
		}
	}
	return 0;
}

\(\text{loj-10129}\)

给定一个长度为 \(n\) 的序列 \(a_i\),初始值都为 \(0\),有 \(m\) 次操作:

  1. 1 l r c 表示将 \([l, r]\) 区间内的数 \(\times c\)
  2. 2 l r c 表示将 \([l, r]\) 区间内的数 \(+ c\)
  3. 3 l r 0 表示询问 \([l, r]\) 的区间和,答案对 \(P\) 取模。

\(1 \le n,q \le 10^5\)\(1 \le l \le r \le n\)\(0 \le c, a_i \le 10^9\)\(1 \le P \le 10^9 + 7\)


需要维护乘的懒标记和加的懒标记,因此就会出现运算优先级的问题。

于是遵循先乘再加的原则即可,写一个函数处理节点懒标记更新情况:

void upd(long long p, long long mul, long long add) {
	t[p].res = (t[p].res * mul + add * (t[p].r - t[p].l + 1)) % P;
	t[p].mul = t[p].mul * mul % P;
	t[p].add = (t[p].add * mul + add) % P;
	return;
}

注意加的懒标记也要乘上 \(mul\)。然后 \(\text{pushdown}\) 也需要改一下:

void pushdown(long long p) {
	upd(p << 1, t[p].mul, t[p].add);
	upd(p << 1 | 1, t[p].mul, t[p].add);
	t[p].add = 0, t[p].mul = 1;
	return;
}

剩下的几乎一样。

注意:\(\text{build}\) 时记得初始化乘的懒标记为 \(1\)。取模也要注意。

标记永久化

维护区间修改的方式有两种,一种是懒标记和标记下传,另一种叫做”标记永久化“。

标记永久化,就是不下传标记,在每次查询时把经过的标记累加起来,查询时加起来。

于是只有 \(\text{build}\) 的时候需要 \(\text{pushup}\)。任何时刻都不需要 \(\text{pushdown}\)

更新操作变成这样:

void update(long long p, long long l, long long r, long long k) {
	t[p].res += k * (min(t[p].r, r) - max(t[p].l, l) + 1);
    // 至于为什么是交集,因为我们没有 pushup,这个东西相当于 pushup。
	if(t[p].l >= l && t[p].r <= r) { t[p].lz += k; return; }
	long long mid = (t[p].l + t[p].r) >> 1;
	if(l <= mid) update(p << 1, l, r, k);
	if(r > mid) update(p << 1 | 1, l, r, k);
	return;
}

于是查询的时候就要多记录一个懒标记的值:

long long query(long long p, long long l, long long r, long long lz) {
	if(t[p].l >= l && t[p].r <= r) {
		long long res = lz * (t[p].r - t[p].l + 1);
        // 计算懒标记的贡献。
		return t[p].res + res;
	}
	lz += t[p].lz; // 累加懒标记。
    long long mid = (t[p].l + t[p].r) >> 1, res = 0;
	if(l <= mid) res += query(p << 1, l, r, lz);
	if(r > mid) res += query(p << 1 | 1, l, r, lz);
	return res;
}

标记永久化大概就是这样。

\(\text{hdu-1698}\)

给定一个长度为 \(n\) 的序列 \(a_i\),初始值为 \(1\)。有 \(q\) 次操作:

  • l r x\(a_l, a_{l+1}, \dots, a_r\) 都赋值为 \(x\)

\(q\) 次操作后序列的元素和。

\(1 \le T \le 10\)\(1 \le n \le 10^5\)\(1 \le l \le r \le n\)\(1 \le x \le 3\)


线段树维护即可,需要区间赋值、区间查询操作。

注意:多组数据,懒标记记得清空。

贴一下代码:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 100005

long long read() {
	long long 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;
}

struct node { long long l, r, res, lz; } t[MAXN << 2];
long long T, n, q, l, r, x, cnt;

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r, t[p].lz = 0;
	if(l == r) { t[p].res = 1; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	return;
}

void pushdown(long long p) {
	long long lz = t[p].lz;
	t[p << 1].lz = t[p << 1 | 1].lz = lz;
	t[p << 1].res = lz * (t[p << 1].r - t[p << 1].l + 1);
	t[p << 1 | 1].res = lz * (t[p << 1 | 1].r - t[p << 1 | 1].l + 1);
	t[p].lz = 0;
	return;
}

void update(long long p, long long l, long long r, long long x) {
	if(t[p].l >= l && t[p].r <= r) {
		t[p].res = x * (t[p].r - t[p].l + 1);
		t[p].lz = x; return;
	}
	if(t[p].lz) pushdown(p);
	long long mid = (t[p].l + t[p].r) >> 1;
	if(l <= mid) update(p << 1, l, r, x);
	if(r > mid) update(p << 1 | 1, l, r, x);
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	return;
}

int main() {
	T = read();
	while(T --) {
		n = read(), q = read();
		build(1, 1, n);
		while(q --) {
			l = read(), r = read(), x = read();
			update(1, l, r, x);
		}
		cout << "Case " << (++ cnt) << ": The total value of the hook is " << t[1].res << ".\n";
	}
	return 0;
}

\(\text{uva-12299}\)

给定一个长度为 \(n\) 的序列 \(a_i\),有 \(m\) 次以下操作:

  • 给定若干个下标 \(i_1,i_2,\dots,i_k\),让这些下标上的数向左循环移一位。
  • 询问 \([l, r]\) 区间上的最小值。

\(1 \le n,q,a_i \le 10^5\)\(1 \le k \le 30\)


由于 \(k\) 比较小,于是考虑将循环移位直接刻画为单点修改。维护区间 \(\min\) 即可。

注意:修改时应修改的是当前值,即 \(\text{query}(1,b_{i+1},b_{i+1})\),而非 \(a_{b_{i+1}}\)

贴一下代码:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 100005
#define INF 0x3f3f3f3f

long long read() {
	long long 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;
}

struct node { long long l, r, minn; } t[MAXN << 2];
long long n, q, a[MAXN], b[MAXN];
string s;

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r;
	if(l == r) { t[p].minn = a[l]; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	t[p].minn = min(t[p << 1].minn, t[p << 1 | 1].minn);
	return;
}

void update(long long p, long long x, long long k) {
	if(t[p].l == t[p].r) { t[p].minn = k; return; }
	long long mid = (t[p].l + t[p].r) >> 1;
	if(x <= mid) update(p << 1, x, k);
	else update(p << 1 | 1, x, k);
	t[p].minn = min(t[p << 1].minn, t[p << 1 | 1].minn);
	return;
}

long long query(long long p, long long l, long long r) {
	if(t[p].l >= l && t[p].r <= r) return t[p].minn;
	long long mid = (t[p].l + t[p].r) >> 1, res = INF;
	if(l <= mid) res = min(res, query(p << 1, l, r));
	if(r > mid) res = min(res, query(p << 1 | 1, l, r));
	return res; 
}

int main() {
	n = read(), q = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	build(1, 1, n);
	while(q --) {
        cin >> s; long long cnt = 1;
        for(int i = 1; i <= 30; i ++) b[i] = 0;
        for(int i = 6; i < s.size(); i ++) {
            if(s[i] == ')') continue;
            if(s[i] == ',') cnt ++;
            else b[cnt] = b[cnt] * 10 + (s[i] - '0');
        }
        if(s[0] == 'q') cout << query(1, b[1], b[2]) << "\n";
        else {
        	long long res = query(1, b[1], b[1]);
        	for(int i = 1; i < cnt; i ++) 
				update(1, b[i], query(1, b[i + 1], b[i + 1]));
        	update(1, b[cnt], res);
        }
	}
	return 0;
}

\(\text{uva-11992}\)

一个矩阵最多包含 \(10^6\) 个元素,分为 \(r\) 行和 \(c\) 列。其中的每个元素都有一个位置 \((x, y)\),其中 \(1 \le x \le r,1\le y\le c\)。最初,所有元素都为零。您需要处理\(3\)种操作:

  • \(1~x_1~y_1~x_2~y_2~v\):将子矩阵 \((x_1, y_1, x_2, y_2)\) 中的每个元素 \((x, y)\) 增加 \(v(v >0)\)

  • \(2~x_1~y_1~x_2~y_2~v\):将子矩阵 \((x_1, y_1, x_2, y_2)\) 中的每个元素 \((x, y)\) 设置为 \(v\)

  • \(3~x_1~y_1~x_2~y_2\):输出子矩阵 \((x_1,y_1,x_2,y_2)\) 中所有元素的总和、最小值和最大值。

在上面的描述中,子矩阵 \((x_1,y_1,x_2,y_2)\) 是指所有满足 \(x_1\le x\le x_2\)\(y_1\le y\le y_2\) 的元素 \((x,y)\)。保证 \(1≤x_1≤x_2≤r,1≤y_1≤y_2≤c\)。任意操作后,矩阵中所有元素之和不超过\(10^9\)

\(1 \le m \le 2 \times 10^4\)\(1 \le r \le 20\)


注意到行数很小,所以很容易想到把二维转化成一维,那么修改子矩阵就可以暴力修改区间。

于是问题就变成区间加、区间赋值、区间查询最小值、最大值、总和。

需要注意 \(\text{pushdown}\) 的优先级,贴一下代码:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 1000005
#define INF 0x3f3f3f3f3f3f3f3f

long long read() {
	long long 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;
}

struct node { long long l, r, res, minn, maxn, lz1, lz2; } t[MAXN << 2];
long long n, m, q, op, xl, xr, yl, yr, w;

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r;
	t[p].lz1 = 0, t[p].lz2 = INF;
	t[p].res = t[p].minn = t[p].maxn = 0;
	if(l == r) return;
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	return;
}

void pushup(long long p) {
	t[p].res = t[p << 1].res + t[p << 1 | 1].res;
	t[p].minn = min(t[p << 1].minn, t[p << 1 | 1].minn);
	t[p].maxn = max(t[p << 1].maxn, t[p << 1 | 1].maxn);
	return;
}

void upd(long long p, long long add, long long mk) {
	if(mk == INF) {
		t[p].res += add * (t[p].r - t[p].l + 1);
		t[p].minn += add, t[p].maxn += add;
		t[p].lz1 += add;
	}
	else {
		t[p].res = (mk + add) * (t[p].r - t[p].l + 1);
		t[p].minn = t[p].maxn = mk + add;
		t[p].lz1 = add, t[p].lz2 = mk;	
	}
	return;
}

void pushdown(long long p) {
	upd(p << 1, t[p].lz1, t[p].lz2);
	upd(p << 1 | 1, t[p].lz1, t[p].lz2);
	t[p].lz1 = 0, t[p].lz2 = INF;
	return;
}

void update(long long p, long long l, long long r, long long add, long long mk) {
	if(t[p].l >= l && t[p].r <= r) { upd(p, add, mk); return; }
	long long mid = (t[p].l + t[p].r) >> 1; pushdown(p);
	if(l <= mid) update(p << 1, l, r, add, mk);
	if(r > mid) update(p << 1 | 1, l, r, add, mk);
	pushup(p);
	return;
}

long long query1(long long p, long long l, long long r) {
	if(t[p].l >= l && t[p].r <= r) return t[p].res;
	long long mid = (t[p].l + t[p].r) >> 1, res = 0;
	pushdown(p);
	if(l <= mid) res += query1(p << 1, l, r);
	if(r > mid) res += query1(p << 1 | 1, l, r);
	pushup(p); return res;
}

long long query2(long long p, long long l, long long r) {
	if(t[p].l >= l && t[p].r <= r) return t[p].minn;
	long long mid = (t[p].l + t[p].r) >> 1, res = INF;
	pushdown(p);
	if(l <= mid) res = min(res, query2(p << 1, l, r));
	if(r > mid) res = min(res, query2(p << 1 | 1, l, r));
	pushup(p); return res;
}

long long query3(long long p, long long l, long long r) {
	if(t[p].l >= l && t[p].r <= r) return t[p].maxn;
	long long mid = (t[p].l + t[p].r) >> 1, res = -INF;
	pushdown(p);
	if(l <= mid) res = max(res, query3(p << 1, l, r));
	if(r > mid) res = max(res, query3(p << 1 | 1, l, r));
	pushup(p); return res;
}

int main() {
	while(cin >> n >> m >> q) {
		build(1, 1, n * m);
		while(q --) {
			op = read(), xl = read(), yl = read();
			xr = read(), yr = read();
			if(op == 1) {
				w = read();
				for(int i = xl - 1; i < xr; i ++)
					update(1, i * m + yl, i * m + yr, w, INF);
			}
			else if(op == 2) {
				w = read();
				for(int i = xl - 1; i < xr; i ++)
					update(1, i * m + yl, i * m + yr, 0, w);
			}
			else {
				long long res = 0, minn = INF, maxn = -INF;
				for(int i = xl - 1; i < xr; i ++)
					res += query1(1, i * m + yl, i * m + yr), 
					minn = min(minn, query2(1, i * m + yl, i * m + yr)),
					maxn = max(maxn, query3(1, i * m + yl, i * m + yr));
				cout << res << " " << minn << " " << maxn << "\n";
			}
		}
	}
	return 0;
}

\(\text{loj-10116}\)

给定 \(n,k\),表示火车共有 \(n\) 节车厢以及 \(k\) 个事件。

接下来给出 \(k\) 个事件,每行开头都有一个字母 ABC

  • 如果字母为 A,接下来是一个数 \(m\),表示年级主任现在在第 \(m\) 节车厢;
  • 如果字母为 B,接下来是两个数 \(m,p\),表示在第 \(m\) 节车厢有 \(p\) 名学生上车;
  • 如果字母为 C,接下来是两个数 \(m,p\),表示在第 \(m\) 节车厢有 \(p\) 名学生下车。

学生总人数不会超过 \(10^5\)

\(1 \le n \le 5 \times 10^5\)\(1 \le k \le 10^5\),至少有 \(3 \times 10^4\)A


可以用权值线段树去做,不需要 \(\text{build}\)。贴一下代码:

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 500005

long long read() {
	long long 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;
}

long long n, k, t[MAXN << 2], x, y;
char op;

void update(long long p, long long l, long long r, long long x, long long y) {
	if(l == r) { t[p] += y; return; }
	long long mid = (l + r) >> 1;
	if(x <= mid) update(p << 1, l, mid, x, y);
	else update(p << 1 | 1, mid + 1, r, x, y);
	t[p] = t[p << 1] + t[p << 1 | 1];
	return;
}

long long query(long long p, long long l, long long r, long long pl, long long pr) {
	if(l >= pl && r <= pr) return t[p];
	long long mid = (l + r) >> 1, res = 0;
	if(pl <= mid) res += query(p << 1, l, mid, pl, pr);
	if(pr > mid) res += query(p << 1 | 1, mid + 1, r, pl, pr);
	return res; 
}

int main() {
	n = read(), k = read();
	while(k --) {
		cin >> op;
		if(op == 'A') x = read(), cout << query(1, 1, n, 1, x) << "\n";
		else if(op == 'B') x = read(), y = read(), update(1, 1, n, x, y);
		else x = read(), y = read(), update(1, 1, n, x, -y);
	}
	return 0;
}

\(\text{zkw }\)线段树

\(\text{zkw}\) 线段树是一种用循环实现的线段树,比正常的递归式线段树快很多,而且好写。

线段树的叶子节点的下标是连续的,因此叶子节点的编号与原数组的编号有一种对应关系。

简单观察一下就会发现他们的编号差是个定值,即除去叶子节点之外的节点数量。

为了方便,我们约定,无论树有没有那么大,我们都把差值看作 \(n\),无数据的叶节点空置即可。

这样就可以用循环建树:

void build() {
	n = read(), m = read();
	for(int i = n + 1; i <= (n << 1); i ++) t[i] = read();
	for(int i = n - 1; i >= 1; i --) t[i] = t[i << 1] + t[i << 1 | 1];
	return;
}

对于单点修改,我们直接自底向上更新即可:

void update1(long long x, long long k) {
	for(int i = x + n; i >= 1; i /= 2) t[i] += k;
	return;
}

单点查询就更简单了:

long long query1(long long x) { return t[x + n]; }

那么怎么区间查询呢?我们搞两个指针 \(pl = l - 1, pr = r + 1\)

\(pl, pr\) 一直往上跳,直到他们的父亲节点相同,此时这两个指针路径围着的部分就是查询区间。

发现一个规律:

  • \(pl\) 当前是父亲节点的左儿子,那么右儿子一定在答案中。
  • \(pr\) 当前是父亲节点的右儿子,那么左儿子一定在答案中。

这样就可以用循环区间查询了:

long long query(long long l, long long r) {
	long long res = 0, pl = l - 1, pr = r + 1;
	while(pl / 2 != pr / 2) {
		if(!(pl & 1)) res += t[pl + 1];
		if(pr & 1) res += t[pr - 1];
		pl /= 2, pr /= 2;
	}
	return res;
}

但是 \(\text{zkw}\) 线段树要求 \(n\)\(2\) 的次幂,虽然不是也可以调整吧,但感觉没什么太大用。

区间修改和单点查询就不写了,感觉还不如常规线段树。

\(\text{uva-10587}\)

有一个长度为 \(10^7\) 的序列 \(a_i\),和 \(n\) 个区间 \([l_i, r_i]\),表示依次将 \([l_i, r_i]\) 上的数设为 \(i\)

求操作完后序列中有多少不同的数。

\(1 \le n \le 10^4\)\(1 \le l_i \le r_i \le 10^7\)


这是一道权值线段树的题,但维护值域为 \([1, 10^7]\) 的线段树显然不现实。

考虑离散化,但这个不能直接离散化,否则会出现一些边界问题,反例就不举了。

\(l_i,r_i,r_i+1\) 都加进离散化数组里即可。

然后对于这道题的查询是这样的:

void query(long long p) {
	if(t[p].res != -1) {
		if(!mp[t[p].res]) mp[t[p].res] = 1, ans ++;
		return;
	}
	long long mid = (t[p].l + t[p].r) >> 1;
	query(p << 1), query(p << 1 | 1);
	return;
}

这样就统计了多少个不同的数。

\(\text{loj-10128}\)

给定长度为 \(n\) 的序列 \(a_i\),有 \(q\) 次以下两种操作:

  • 1 l r 区间查询操作,输出 \(\sum\limits_{i=l}^r a_i\) 的值。
  • 2 l r 区间开根操作,对于 \(i \in [l,r],a_i \gets \sqrt a_i\),下取整。

\(1 \le n \le 10^5\)\(1 \le q \le 2 \times 10^5\)\(1 \le l \le r \le n\)\(0 \le a_i \le 10^9\)


开根操作比较特殊,对于 \(10^9\) 范围内的数,最多开 \(8\) 次根就会变成 \(0/1\)

因此若一个区间内最大值为 \(0/1\),则这个区间可以跳过不操作,因为操作无变化。

我们也不需要考虑懒标记,开根操作可以直接更新到叶子节点,因为每个数最多操作 \(8\) 次。

时间复杂度为单次操作 \(O(\log n)\),需要维护区间最大值、区间和。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 100005

long long read() {
	long long 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;
}

struct node { long long l, r, w, mx; } t[MAXN << 2];
long long n, m, a[MAXN];

void pushup(long long p) {
	t[p].w = t[p << 1].w + t[p << 1 | 1].w;
	t[p].mx = max(t[p << 1].mx, t[p << 1 | 1].mx);
	return;
}

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r;
	if(l == r) { t[p].w = t[p].mx = a[l]; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	pushup(p); return;
}

void upd(long long p, long long l, long long r) {
	if(t[p].mx <= 1) return;
	if(t[p].l == t[p].r) { t[p].w = t[p].mx = sqrt(t[p].w); return; }
	long long mid = (t[p].l + t[p].r) >> 1;
	if(l <= mid) upd(p << 1, l, r);
	if(r > mid) upd(p << 1 | 1, l, r);
	pushup(p); return;
}

long long qry(long long p, long long l, long long r) {
	if(l <= t[p].l && t[p].r <= r) return t[p].w;
	long long mid = (t[p].l + t[p].r) >> 1, res = 0;
	if(l <= mid) res += qry(p << 1, l, r);
	if(r > mid) res += qry(p << 1 | 1, l, r);
	return res;
}

int main() {
	n = read();
	for(int i = 1; i <= n; i ++) a[i] = read();
	build(1, 1, n), m = read();
	while(m --) {
		long long x = read(), l = read(), r = read();
		if(x == 2) upd(1, l, r);
		else cout << qry(1, l, r) << "\n";
	}
	return 0;
}

\(\text{hdu-3308}\)

给定长度为 \(n\) 的序列 \(a_i\),有 \(q\) 次以下两种操作:

  • U x k 替换操作,\(a_x \gets k\)
  • Q l r 查询操作,查询 \([l,r]\) 区间内最长连续严格递增子序列的长度。

注意,本题输入下标从 \(0\) 开始。

\(1 \le n,q \le 10^5\)\(0 \le a_i \le 10^5\)


区间合并经典题。只需要考虑如何向上 pushup。

其实也很好处理,记录当前节点分别从左端/右端开始的最长子序列长度,和总长度即可。

pushup 时注意处理区间合并的关系。

#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
#define MAXN 100005

long long read() {
	long long 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;
}

struct node { long long l, r, ls, rs, mx; } t[MAXN << 2];
long long T, n, q, a[MAXN];

void pushup(long long p) {
	t[p].ls = t[p << 1].ls, t[p].rs = t[p << 1 | 1].rs;
	t[p].mx = max(t[p << 1].mx, t[p << 1 | 1].mx);
	long long mid = (t[p].l + t[p].r) >> 1;
	if(a[mid] < a[mid + 1]) {
		if(t[p << 1].ls == t[p << 1].r - t[p << 1].l + 1)
			t[p].ls += t[p << 1 | 1].ls;
		if(t[p << 1 | 1].rs == t[p << 1 | 1].r - t[p << 1 | 1].l + 1)
			t[p].rs += t[p << 1].rs;
		t[p].mx = max(t[p].mx, t[p << 1].rs + t[p << 1 | 1].ls);
	}
	return;
}

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r, t[p].ls = t[p].rs = t[p].mx = 0;
	if(l == r) { t[p].ls = t[p].rs = t[p].mx = 1; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	pushup(p); return;
}

void upd(long long p, long long x) {
	if(t[p].l == t[p].r) return;
	long long mid = (t[p].l + t[p].r) >> 1;
	if(x <= mid) upd(p << 1, x);
	else upd(p << 1 | 1, x);
	pushup(p); return;
}

long long qry(long long p, long long l, long long r) {
	if(l <= t[p].l && t[p].r <= r) return t[p].mx;
	long long mid = (t[p].l + t[p].r) >> 1, res = 0;
	if(r <= mid) res = max(res, qry(p << 1, l, r));
	else if(l > mid) res = max(res, qry(p << 1 | 1, l, r));
	else {
		res = max(res, qry(p << 1, l, r));
		res = max(res, qry(p << 1 | 1, l, r));
		if(a[mid] < a[mid + 1]) res = max(res, min(mid - l + 1, 
			t[p << 1].rs) + min(r - mid, t[p << 1 | 1].ls));
	}
	return res;
}

int main() {
	T = read();
	while(T --) {
		n = read(), q = read();
		for(int i = 1; i <= n; i ++) a[i] = read();
		build(1, 1, n);
		while(q --) {
			char c; cin >> c;
			long long x = read() + 1, k = read();
			if(c == 'U') a[x] = k, upd(1, x);
			else cout << qry(1, x, k + 1) << "\n";
		}
	}
	return 0;
}

\(\text{uva-1400}\)

给定长度为 \(n\) 的序列 \(a_i\),有 \(q\) 次询问,每次询问给定 \(l,r\),求 \([l,r]\) 中哪一段连续区间和最大。

多个答案时输出起点最小的那个,若仍有多个答案,输出终点最小的那个。

\(1 \le n,q \le 5 \times 10^5\)


跟上一题的区间合并操作处理方式一致,只不过需要记录答案而已。

\(\text{hdu-1540}\)

给定长度为 \(n\)\(01\) 序列 \(a_i\),初始时全为 \(1\)。有 \(q\) 次以下三种操作:

  • D x\(a_x\) 设为 \(0\)
  • Q x 查询与 \(a_x\) 相连的 \(1\) 的个数,即包含 \(x\) 的全 \(1\) 区间的长度。
  • R 撤销最近的 D 操作。

注意,本题包含多组测试样例。

\(1 \le n,q \le 5 \times 10^4\)


本质上还是和区间合并问题差不多。

记录每个区间分别从左端/右端开始的最长的全 \(1\) 区间长度。

只需要稍微改一下查询方式即可。

至于撤销操作,可以用栈记录下每次 D 操作。

注意:本题多测,题面没说,太坑人了 \(\text{hdu}\)

#include<iostream>
#include<cstdio>
using namespace std;
#define MAXN 50005

long long read() {
	long long 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;
}

struct node { long long l, r, ls, rs; } t[MAXN << 2];
long long n, q, s[MAXN], top;

void pushup(long long p) {
	t[p].ls = t[p << 1].ls, t[p].rs = t[p << 1 | 1].rs;
	if(t[p << 1].ls == t[p << 1].r - t[p << 1].l + 1)
		t[p].ls += t[p << 1 | 1].ls;
	if(t[p << 1 | 1].rs == t[p << 1 | 1].r - t[p << 1 | 1].l + 1)
		t[p].rs += t[p << 1].rs;
	return;
}

void build(long long p, long long l, long long r) {
	t[p].l = l, t[p].r = r;
	if(l == r) { t[p].ls = t[p].rs = 1; return; }
	long long mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	pushup(p); return;
}

void upd(long long p, long long x, long long k) {
	if(t[p].l == t[p].r) { t[p].ls = t[p].rs = k; return; }
	long long mid = (t[p].l + t[p].r) >> 1;
	if(x <= mid) upd(p << 1, x, k);
	else upd(p << 1 | 1, x, k);
	pushup(p); return;
}

long long qry(long long p, long long x) {
	if(t[p].l == t[p].r) return t[p].ls;
	long long mid = (t[p].l + t[p].r) >> 1;
	if(x <= mid) {
		if(t[p << 1].r - x < t[p << 1].rs) 
			return t[p << 1].rs + t[p << 1 | 1].ls;
		return qry(p << 1, x);
	}
	else if(x - t[p << 1].r <= t[p << 1 | 1].ls)
		return t[p << 1].rs + t[p << 1 | 1].ls;
	return qry(p << 1 | 1, x);
}

int main() {
	ios::sync_with_stdio(0);
	cin.tie(0), cout.tie(0);
	while(cin >> n >> q) {
		build(1, 1, n);
		while(q --) {
			char c; cin >> c; long long x;
			if(c == 'D') cin >> x, upd(1, x, 0), s[++ top] = x;
			else if(c == 'Q') cin >> x, cout << qry(1, x) << "\n";
			else upd(1, s[top --], 1);
		}
	}
	return 0;
}

\(\text{hdu-4614}\)

爱丽丝人气爆棚,每天都能收到一大堆鲜花。她有 \(n\) 个花瓶,编号 \(0 \sim n-1\)

每当收到花时,她会尝试把花放进花瓶里,一朵花一个花瓶。

她会选一个花瓶 \(x\),先试着往花瓶 \(x\) 里放花。如果花瓶里没花,就放进去;有花的话就跳过。然后依次尝试花瓶 \(x+1 \sim n-1\),直到花放完或者试到最后一个花瓶。剩下没放进去的花就只能扔掉了。

当然,有时候她也会清理花瓶。因为花瓶太多,她会选一段编号 \([l,r]\) 的花瓶清理,清理时这些花瓶里的花都会被丢弃。

\(2 \le n,m \le 5 \times 10^4\)


用线段树维护一段区间内的空瓶子个数,懒标记区间内是否所有的瓶子都为空。

操作 \(2\) 很简单,就是一个简单的区间查询,首先查询出 \([l,r]\) 区间内的空瓶子个数,然后用总的区间长度减去空瓶子个数就是丢弃的花的个数,然后我们再把 \([l,r]\) 区间的非空瓶子都变成空的,一个简单的区间修改。

对于操作 \(1\) 而言,我们首先查询出区间 \([x,n]\) 区间的空瓶子个数,如果为 \(0\) 那么直接输出;否则我们需要二分去求出 \([x,n]\) 区间内的第一个空瓶子位置和最后一个空瓶子位置。然后我们就可以在 \([x,n]\) 区间去二分枚举空瓶子数等于 \(1\) 的位置,以及空瓶子数为 $\min \((花朵的数量、\)[x,n]$ 区间内的空瓶子个数)的位置,再把这两个位置之间的空瓶都处理成非空的。

\(\text{spoj-GSS1}\)

给定长度为 \(n\) 的序列 \(a\),有 \(q\) 次查询,每次查询给定 \([l,r]\),你需要给出下式的值:

\[\max_{l \le x \le y \le r} a_x + a_{x + 1} + \dots + a_y \]

\(1 \le n \le 5 \times 10^4\)\(|a_i| \le 15007\)


虽然是静态无修的,但也考虑用线段树维护。

其实比较板子的,线段树维护 \(lmx,rmx,mx,res\),分别表示区间左端开始的最大区间和、右端开始的最大区间和、整个区间的最大区间和以及区间和。

上传时大概就是下式:

\[\left\{ \begin{align} p.res &= l.res + r.res \\ p.lmx &= \max(l.lmx, l.res + r.lmx) \\ p.rmx &= \max(r.rmx, r.res + l.rmx) \\ p.mx &= \max(l.mx, r.mx, l.rmx + r.lmx) \end{align} \right. \]

其他就没什么注意的点了。

#include<iostream>
#include<cstdio>
using namespace std;
#define ll long long
#define MAXN 1000005

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

struct node { ll l, r, lmx, rmx, mx, res; } t[MAXN << 2];
ll n, m, a[MAXN];

void calc(node &p, node &l, node &r) {
    p.res = l.res + r.res;
    p.lmx = max(l.lmx, l.res + r.lmx);
    p.rmx = max(r.rmx, r.res + l.rmx);
    p.mx = max(l.rmx + r.lmx, max(l.mx, r.mx));
    return;
}

void build(ll p, ll l, ll r) {
    t[p].l = l, t[p].r = r;
    if(l == r) {
        t[p].lmx = t[p].rmx = t[p].mx = t[p].res = a[l];
        return;
    }
    ll mid = (l + r) >> 1;
    build(p << 1, l, mid), build(p << 1, mid + 1, r);
    calc(t[p], t[p << 1], t[p << 1 | 1]);
    return;
}

node qry(ll p, ll l, ll r) {
    if(l <= t[p].l && t[p].r <= r) return t[p];
    ll mid = (t[p].l + t[p].r) >> 1;
    if(l > mid) return qry(p << 1 | 1, l, r);
    if(r <= mid) return qry(p << 1, l, r);
    node x = qry(p << 1, l, r), y = qry(p << 1 | 1, l, r), res;
    calc(res, x, y); return res;
}

int main() {
    n = read();
    for(int i = 1; i <= n; i ++) a[i] = read();
    m = read(), build(1, 1, n);
    while(m --) {
        ll x = read(), y = read();
        cout << qry(1, x, y).mx << "\n";
    }
    return 0;
}
posted @ 2026-02-25 21:12  So_noSlack  阅读(14)  评论(0)    收藏  举报