线段树的一些经典操作

线段树

​ (线段树就长这样)

一些性质

​ 1.左儿子是父节点编号的二倍,右儿子是父节点编号的二倍加一。

#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)

​ 2.线段树数组长度不能小于4N。

​ 3.对于一个区间[\(l\), \(r\)], 它的左儿子是[\(l\), \(mid\)], 右儿子是[\(mid\) + 1, \(r\)]。

#define mid ((l + r) >> 1)

​ 4.线段树去掉最后一层一定是完全二叉树,深度为\(logn\)

建树

void up(int o) {
    t[o].dat = t[ls(o)].dat + t[rs(o)].dat; //从下往上走的时候子节点更新父节点
}

void build(int o, int l ,int r) {
    if(l == r) {t[o].dat = read(); return ;} //递归到叶子节点
    build(ls(o), l, mid), build(rs(o), mid + 1, r); //递归左右子树
    up(o);
}

build(1, 1, n); //调用入口

单点修改

void change(int o, int l ,int r, int x, int k) { //将x位置的值改为k
	if(l == r) { t[o].dat = k; return ;} //找到x位置,修改
    if(x <= mid) change(ls(o), l, mid, x, k);
    if(x > mid) change(rs(o), mid + 1, r, x, k);
	up(o); //一定记得要更新
}

区间查询

int ask(int o, int l, int r, int x, int y) {
	if(x <= l && r <= y) return t[o].dat; //当覆盖了一个完整的区间,直接返回这个区间的值,这就是线段树为啥很快
    int res = 0; 
    if(x <= mid) res += ask(ls(o), l, mid, x, y); //一定注意是x <= mid, 不是l <= mid,我在这里死了好几次了
    if(y > mid) res += ask(rs(o), mid + 1, r, x, y);
    return res;
} 

区间修改

void modify(int o, int l, int r, int k) {
	tag[o] += k; t[o].dat += (r - l + 1) * k;
}

void down(int o, int l, int r) {
	if(tag[o] != 0) modify(ls(o), l, mid, tag[o]), modify(rs(o), mid + 1,r, tag[o]), tag[o] = 0;
}

void change(int o, int l, int r, int x, int y, int k) {
	if(x <= l && r <= y) { return modify(o, l, r, k); }
    down(o, l, r);
    if(x <= mid) change(ls(o), l, mid, x, y, k);
    if(y > mid) changr(rs(o), mid + 1, r, x, y, k);
    up(o); //记得更新
}

​ 区间修改的时候我们引入懒标记\(tag\)数组。

​ 我们修改区间[\(l\), \(r\)]时,以该节点为根的子树所有点都要修改,复杂度为\(O(n)\)

​ 类似区间查询,我们如果发现[\(l\), \(r\)]包含在修改区间中,我们直接给这个节点打上\(tag\)懒标记,然后返回,等到后续的指令里需要从该节点向下递归时,我们直接下传标记,使两个子节点具有标记,然后该节点标记清零

​ 使用懒标记后复杂度降为\(O(logn)\)

带修改最大子段和

P4513 小白逛公园

void up(int o) {
    t[o].dat = t[ls(o)].dat + t[rs(o)].dat;
    t[o].l = max(t[ls(o)].l, t[ls(o)].dat + t[rs(o)].l);
    t[o].r = max(t[rs(o)].r, t[rs(o)].dat + t[ls(o)].r);
    t[o].s = max(max(t[ls(o)].s, t[rs(o)].s), t[ls(o)].r + t[rs(o)].l);
}

​ 这道题求带修改最大子段和。

​ 我们考虑线段树多维护一点东西,分别是紧靠左端的最大子段和,紧靠右端的最大子段和,最大子段和。

​ 维护紧靠左端最大子段和:将 左子树紧靠左端的最大子段和左子树的所有点的和+右子树紧靠左端的最大子段和\(max\)。(维护紧靠右端最大子段和也同理)


​ 维护最大子段和:将 左子树最大子段和右子树最大子段和左子树紧靠右端子段和+右子树紧靠左端子段和\(max\)

区间染色

poj 2777

​ 题目大意:有一个长度为\(L\)的色板,可均匀地分为\(L\)个小格。

​ 有两种操作:\(C\) \(l\) \(r\) \(x\) 表示把区间[\(l\), \(r\)]染为颜色\(x\) (\(x\) <= 30);

\(P\) \(l\) \(r\) 表示询问区间[\(l\), \(r\)]有几种颜色。

​ 这道题的不同点在于它新的颜色会覆盖掉原来的颜色,但是我们发现它的颜色个数很少,我们可以用二进制压缩来做,每一个线段树上存一个二进制数,第\(i-1\)位为1表示这个区间具有颜色\(i\)

​ 在询问颜色个数时,我们可以将每个区间的二进制数用 | 来合并。

#include <iostream>
#include <cstdio>
#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

inline int read() {
	int s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;
}

const int N = 1e6 + 5;
int n, T, m;
struct tree { int d, tag; } t[N << 2];

void up(int o) {
	t[o].d = t[ls(o)].d | t[rs(o)].d;
}

void modify(int o, int l, int r, int k) {
	t[o].tag = 1; t[o].d = k; //这里是将父节点的颜色都给了子节点,有人可能会想父节点的颜色个数不一定就等于子节点的颜色个数呀。这个函数实际上是在tag不为								 0的时候执行的,也就数说该父节点一定是一种颜色,所以他的子节点也是和父亲节点一样的颜色。
}

void down(int o, int l, int r) {
	if(t[o].tag != 0) modify(ls(o), l, mid, t[o].d), modify(rs(o), mid + 1, r, t[o].d), t[o].tag = 0;
}

void build(int o, int l, int r) {
	if(l == r) { t[o].tag = t[o].d = 1; return ;}
	build(ls(o), l, mid); build(rs(o), mid + 1, r);
	up(o);
}

void change(int o, int l, int r, int x, int y, int val) {
	if(x > r || y < l) return ;
	if(x <= l && y >= r) {
		t[o].tag = 1; t[o].d = (1 << (val - 1)); return ;  //记得减一
	}
	down(o, l, r);
	if(y <= mid) change(ls(o), l, mid, x, y, val);
	else if(x > mid) change(rs(o), mid + 1, r, x, y, val);
	else {
		change(ls(o), l, mid, x, y, val);
		change(rs(o), mid + 1, r, x, y, val);
	}
	up(o);
}

int query(int o, int l, int r, int x, int y) {
	if(x <= l && y >= r) { return t[o].d; } 
	down(o, l, r);
	if(y <= mid) return query(ls(o), l, mid, x, y);
	else if(x > mid) return query(rs(o), mid + 1, r, x, y);
	else {
		int a = query(ls(o), l, mid, x, y), b = query(rs(o), mid + 1, r, x, y);
		return a | b;
	}
}

int main() {
	
	n = read(); T = read(); m = read();
	build(1, 1, n);
	for(int i = 1;i <= m; i++) {
		char ch; cin >> ch;
		if(ch == 'C') {
			int x = read(), y = read(), val = read();
			if(x > y) swap(x, y); //这里一定要记得写
			change(1, 1, n, x, y, val);
		}
		else {
			int x = read(), y = read();
			if(x > y) swap(x, y); //记得写
			int ans = query(1, 1, n, x, y);
			int num = 0;
			while(ans) {
				if(ans & 1) num++;
				ans >>= 1; //和快速幂那个差不多,检查哪一位是一,然后记录答案
			}
			printf("%d\n", num);
		}
	}
	
	
	return 0;
}

区间第k​小值

poj 2761

​ 题目大意:给定一个长度为\(n\)的序列,有\(m\)条询问,询问区间[\(l\), \(r\)]的第\(k\)小值为多少,没有一个区间完全包含另一个区间,只可能会交叉。

​ 这道题的做法挺多的,有平衡树,主席树。线段树也可以做,相对简单一点。

​ ''没有一个区间完全包含另一个区间,只可能会交叉。''这句话的意思是一个区间的起点只对应一个终点, 或者说一个区间的终点只对应一个起点。

​ 首先离散化。然后我们考虑线段树多维护一点东西,\(t[o].small\)表示在当前有序线段树内小于\(mid\)的个数,也可以说是左子树的大小。

​ 对于每一个询问,我们可以把它们都离线下来,按左端点排序,使用两个指针\(x\), \(y\),不断移向新的区间,每加入一个数或丢出一个数就更改线段树来维护\(small\)。找当前询问第\(k\)小值时,递归线段树,如果\(k <= t[o].small\),说明这个第\(k\)小值在\(o\)的左子树内;如果\(k > t[o].small\),说明这个第\(k\)小值在\(o\)的右子树内,那就找右子树的第\(k - t[o].small\)小值。

#include <iostream>
#include <cstdio>
#include <algorithm>
#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

inline int read() {
	int s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;
}

const int N = 1e5 + 10;
int n, m, cnt;
int a[N], b[N], ans[N * 5];
struct tree { int small; } t[N << 2];
struct ask { int l, r, k, id; } q[N * 5];

int cmp(ask a, ask b) { return a.l < b.l; }

void change(int o, int l, int r, int x, int flag) {
	if(l == r) return ;
	if(x <= mid) t[o].small += flag, change(ls(o), l, mid, x, flag);
	else change(rs(o), mid + 1, r, x, flag);
}

int query(int o, int l, int r, int k) {
	if(l == r) return l;
	if(k <= t[o].small) return query(ls(o), l, mid, k);
	else return query(rs(o), mid + 1, r, k - t[o].small);
}

int main() {
	
	n = read(); m = read(); 
	for(int i = 1;i <= n; i++) b[i] = a[i] = read();
	sort(b + 1, b + n + 1);
	int cnt = unique(b + 1, b + n + 1) - b - 1;
	for(int i = 1;i <= n; i++) {
		a[i] = lower_bound(b + 1, b + cnt + 1, a[i]) - b;
	} //离散化
	for(int i = 1;i <= m; i++) {
		q[i].l = read(); q[i].r = read(); q[i].k = read(); q[i].id = i;
	}
	sort(q + 1, q + m + 1, cmp);
	int x = q[1].l, y = x;
	for(int i = 1;i <= m; i++) {
		while(x < y && x < q[i].l) {
			change(1, 1, n, a[x], -1); x++; //丢出一个数
		}
		if(y < q[i].l) y = q[i].l;
		while(y <= q[i].r) {
			change(1, 1, n, a[y], 1); y++; //加入一个数
		}
		ans[q[i].id] = b[query(1, 1, n, q[i].k)];
	}
	for(int i = 1;i <= m; i++) printf("%d\n", ans[i]); 
	
	return 0;
}

求LIS

​ 怎么用线段树求带修改最长上升子序列。

P4198 楼房重建

​ 题目大意:有\(n\)(位置从1到\(n\))个楼房,开始时高度都为零。有\(m\)个操作,每次输入\(x\), \(y\),表示把位置为\(x\)的楼房的高度改为\(y\)。每次操作后都要输出一个数,表示当前修改完后从0位置能看见的楼房个数。

​ 很显然,这道题让你求的是最长上升子序列。但不是找楼房高度的最长上升子序列,因为在后面的楼房虽然可能会比前面的高,但不一定会看见,比如:

(看不见吧)

​ 我们发现,只要把高度转化为斜率就好了,是0位置连楼房顶端的直线的斜率,问题变成了求斜率的最长上升子序列。

​ 现在考虑线段树维护什么东西。维护一个区间内最长上升子序列,维护一个斜率最大值。斜率最大值比较好转移,直接对左右子树的斜率最大值取\(max\)就好了。那怎么转移区间内最长上升子序列呢?

​ 首先,一个区间[\(l\), \(r\)]内的最长上升子序列一定包括左端点\(l\),对于叶子节点它的区间内最长上升子序列是1。

​ 设一个区间的最长上升子序列为\(len\),这个区间的\(len\)一定大于等于左儿子的\(len\),因为它一定包含左儿子的左端点,也就是它的左端点。对于它的右儿子,如果它右儿子的左儿子的斜率最大值比它的左儿子的斜率最大值小(或者等于)(有点绕,好好想想),那么就没有它的右儿子的左儿子的事了,直接找右儿子的右儿子,方法和这个相同;如果它右儿子的左儿子的斜率最大值比它的左儿子的斜率最大值大,说明这个区间内对答案有贡献,用同样的方法找右儿子的左儿子,然后加上\(len\)(它的右儿子) - \(len\)(它的右儿子的左儿子)。(这里看不懂的可以画个图理解一下或结合代码理解一下)

#include <iostream>
#include <cstdio>
#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

const int N = 1e5 + 10;
int n, m;
double a[N];
struct tree { double mk; int len; } t[N << 2];

inline int read() {
	int s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;
}

void up1(int o) { t[o].mk = max(t[ls(o)].mk, t[rs(o)].mk); }

int up2(int o, int l, int r, double lm) { //传进来的lm一定要开double,要不你都不知道你怎么死的
	if(t[o].mk <= lm) { return 0; } //如果这个区间的斜率最大值小于等于lm,说明这个区间都不可能对答案有贡献,直接返回0
	if(a[l] > lm) { return t[o].len; } //如果这个区间内的左端点的斜率比lm大,说明这个区间的len可以直接加到答案里面
	if(l == r) { return a[l] > lm; } //找到了叶子节点,如果这个点的斜率大于lm,说明这个点对答案有1的贡献
	if(t[ls(o)].mk <= lm) return up2(rs(o), mid + 1, r, lm); //这里就是上面那一段很绕的话
	else return up2(ls(o), l, mid, lm) + t[o].len - t[ls(o)].len;
}

void change(int o, int l, int r, int x, int h) {
	if(l == r) { t[o].mk = (double)h / x; t[o].len = 1; return ; }
	if(x <= mid) change(ls(o), l, mid, x, h);
	if(x > mid) change(rs(o), mid + 1, r, x, h);																	
	up1(o);
	t[o].len = t[ls(o)].len + up2(rs(o), mid + 1, r, t[ls(o)].mk);
}

int main() {
	
	n = read(); m = read();
	for(int i = 1;i <= m; i++) {
		int x = read(), y = read();
		a[x] = (double)y / x;
		change(1, 1, n, x, y);
		printf("%d\n", t[1].len); 	
	}
	
	return 0;
}

区间面积并

P5490 【模板】扫描线

​ 题目大意:给你\(n\)个矩形,让你求这\(n\)个矩形覆盖的面积是多少,重叠部分只算一次。

​ 我们从下往上扫描一遍,只要碰到矩形的上底或下底就记录一下,将扫描到的线记为一个四元组\((x1, x2, h, flag)\),分别表示这一条线的左端点,右端点,纵坐标,是下底还是上底(下底为1,上底为-1)。(个数是\(2n\)个)

​ 首先肯定要离散化,我们设\(X[x]\)表示\(x\)被离散化后的值。

​ 然后我们考虑线段树怎么用,让线段树维护一个长度\((len)\)和权值\((sum)\),权值表示该节点自身被覆盖的次数,长度表示该节点被矩形覆盖的长度。当从下往上扫,扫到第一条线的时候,1, 2节点会被更新;扫到第二条线的时候,1, 2, 3, 5, 6节点就会被更新。只要这个节点的权值大于0,那么它的就可以被算到面积里。

\(S = \displaystyle \sum_{i = 1}^{n - 1} t[1].len * (h[i + 1] - h[i])\)

​ 那么怎么更新\(len\)呢?如果\(t[o].sum != 0\),说明这个节点被矩形覆盖(完全覆盖)了,\(t[o].len = x_r - x_l\)就可以了;如果这个节点没有被矩形完全覆盖,直接把它的左右儿子的\(len\)加起来就好了。

​ 得注意的是线段树上节点2管到的区间是[1, 2],节点3管到的区间是[3, 3]。为啥没有4,因为4个点只有三段。

#include <iostream>
#include <cstdio>
#include <algorithm>
#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

inline long long read() {
	long long s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;
}

const int N = 1e6 + 5; //题目数据给的1e5,但是开1e6才能过去,要不就二十分,并且是WA不是RE,就很离谱
int n, maxn, X[N << 1], raw[N << 1];
long long ans;
struct Line {
	int l, r, h, flag;
	friend bool operator < (Line a, Line b) { return a.h < b.h; } 
} line[N << 1];
struct tree { int len, sum; } t[N << 3];

void up(int o, int l, int r) {
	if(t[o].sum != 0) t[o].len = raw[r + 1] - raw[l]; //这里注意加一
	else t[o].len = t[ls(o)].len + t[rs(o)].len;
}

void change(int o, int l, int r, int x, int y, int k) {
	if(x <= l && y >= r) { t[o].sum += k; up(o, l, r); return ;}
	if(x <= mid) change(ls(o), l, mid, x, y, k);
	if(y > mid) change(rs(o), mid + 1, r, x, y, k);
	up(o, l, r);
}

int main() {
	
	n = read();
	for(int i = 1;i <= n; i++) {
		int x, y, z, u;
		x = read(); y = read(); z = read(); u = read();
		line[i * 2 - 1] = (Line) {x, z, y, 1};
		line[i * 2] = (Line) {x, z, u, -1};													
		X[i * 2 - 1] = x; X[i * 2] = z;
	}
	n <<= 1;
	sort(X + 1, X + n + 1);
	int cnt = unique(X + 1, X + n + 1) - X - 1;
	for(int i = 1;i <= n; i++) {
		int pos1 = lower_bound(X + 1, X + cnt + 1, line[i].r) - X;
		int pos2 = lower_bound(X + 1, X + cnt + 1, line[i].l) - X;
		raw[pos1] = line[i].r; raw[pos2] = line[i].l; //原坐标
		line[i].r = pos1, line[i].l = pos2; //离散化后,是上面的X数组
	}
	sort(line + 1, line + n + 1);
	for(int i = 1;i < n; i++) { //最后一条线肯定不用算
		change(1, 1, n, line[i].l, line[i].r - 1, line[i].flag); //这里为啥要减一呢?因为线段树的两个节点不可能重合,比如[1, 5], [6, 9],而																	  不能写成[1, 5][5, 9],但图中是重合的,所以要减一。
		ans += t[1].len * 1ll * (line[i + 1].h - line[i].h);
	}
	printf("%lld", ans);
	
	return 0;
}

区间排序

P2824 [HEOI2016/TJOI2016]排序

​ 题目大意:给你一个1到\(n\)的排列,有\(m\)次操作。“\(0 \ l \ r\)”表示将区间[\(l\), \(r\)]的数升序排列;“\(1 \ l \ r\)”表示将区间[\(l\), \(r\)]的数降序排列。最后询问位置\(q\)上的数是多少。

​ 我们可以二分位置\(q\)上的数,设为\(mid\)。将序列里小于\(mid\)的数看成0,将大于等于\(mid\)的数看成1。对于每次排序,我们直接排序0和1就好。说是排序,其实就是把1放前面(后面),把0放后面(前面)。比如现在是升序排列区间[\(l\), \(r\)],我们只要知道了此区间1的个数,那么就可以将[\(l\), \(r - cnt + 1\) ]全部附盖为0,将[\(r - cnt\)\(r\)]全部覆盖为1,于是问题就转化成了区间修改,区间查询,单点查询。

​ 所以线段树维护一个区间1的个数就好啦。

​ 还有一个注意的地方,将区间全部覆盖为0和全部覆盖为1的\(tag\)值不同,注意区分就好了。

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#define ls(o) (o << 1)
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

inline int read() {
	int s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;
}

const int N = 1e5 + 5;
int n, m, p, ans;
int a[N], b[N];
struct tree { 
	int tag, one; 
	tree():one(0) {}
} t[N << 2];
struct que { int opt, x, y; } q[N];

void up(int o) {
	t[o].one = t[ls(o)].one + t[rs(o)].one;
}

void modify(int o, int l, int r, int k) {
	t[o].tag = k; 
	if(k == 1) t[o].one = r - l + 1;  else t[o].one = 0;
}

void down(int o, int l, int r) {
	if(t[o].tag == 0) return ;
	if(t[o].tag == 1) { modify(ls(o), l, mid, 1), modify(rs(o), mid + 1, r, 1); }
	if(t[o].tag == -1) { modify(ls(o), l, mid, -1), modify(rs(o), mid + 1, r, -1); }
	t[o].tag = 0;
}

void build(int o, int l, int r, int x) {
	if(l == r) { t[o].one = a[l] >= x; t[o].tag = 0; return ; }
	build(ls(o), l, mid, x); build(rs(o), mid + 1, r, x);
	up(o); t[o].tag = 0;
}

void change1(int o, int l, int r, int x, int y, int val) {
	if(x <= l && y >= r) { t[o].one = val * (r - l + 1); t[o].tag = val ? 1 : -1; return ; }
	if(x > r || y < l) return ;
	down(o, l, r);
	if(x <= mid) change1(ls(o), l, mid, x, y, val);
	if(y > mid) change1(rs(o), mid + 1, r, x, y, val);
	up(o);
}

int query1(int o, int l, int r, int x, int y) {
	if(x <= l && y >= r) { return t[o].one; }
	if(x > r || y < l) return 0; 
	down(o, l, r);
	int res = 0;
	if(x <= mid) res += query1(ls(o), l, mid, x, y);
	if(y > mid) res += query1(rs(o), mid + 1, r, x, y);
	return res;
}

int query2(int o, int l, int r, int p) {
	if(l == r && r == p) {return  t[o].one; }
	down(o, l, r);
	if(p <= mid) return query2(ls(o), l, mid, p);
	if(p > mid) return query2(rs(o), mid + 1, r, p);
}

int judge(int Mid) {
	build(1, 1, n, Mid);
	for(int i = 1;i <= m; i++) {
		int cnt = query1(1, 1, n, q[i].x, q[i].y);
		if(q[i].opt == 1) {
			change1(1, 1, n, q[i].x, q[i].x + cnt - 1, 1);
			change1(1, 1, n, q[i].x + cnt, q[i].y, 0);
		}
		else {
			change1(1, 1, n, q[i].y - cnt + 1, q[i].y, 1);
			change1(1, 1, n, q[i].x, q[i].y - cnt, 0);
		}
	}
	return query2(1, 1, n, p);
}

int main() {
	
	freopen("seq.in","r",stdin);
	freopen("seq.out","w",stdout);
	
	n = read(); m = read();
	for(int i = 1;i <= n; i++) a[i] = read();
	for(int i = 1;i <= m; i++) { q[i].opt = read(); q[i].x = read(); q[i].y = read(); }
	p = read();
	int ll = 1, rr = n;
	while(ll <= rr) {
		int midd = (ll + rr) >> 1;
		if(judge(midd)) ans = midd, ll = midd + 1;
		else rr = midd - 1;
	}
	printf("%d", ans);
	
	fclose(stdin); fclose(stdout);
	return 0;	
}

区间开方

P4145 上帝造题的七分钟2 / 花神游历各国

​ 题目大意:\(n\)个数,\(m\)个操作:“0,l,r”表示将区间[\(l\), \(r\)]的每个数开方,“1,l,r”表示询问区间[\(l\),\(r\)]的数的总和。

​ 对于一个\(10^{12}\)大小的数,开方6次就可以变为1(自己可以用计算器试一试)。我们用线段树写的话,维护一个区间和,再维护一个开方次数就好了。对于一个开方过6次的区间,我们直接返回区间和就好了;如果开方次数小于等于6次,那就递归到叶子节点,暴力开方就好。

​ 我们发现,如果一个区间的和小于等于区间长度,那么这个区间一定被开方了6次以上,那么我们就可以少维护一个开方次数,直接维护一个区间和就行了,递归的时候我们如果发现\(t[o].sum <= r - l + 1\),直接返回,用不着接着向下递归了(具体看代码吧)。

#include <iostream>
#include <cstdio>
#include <cmath>
#define ls(o) (o << 1) 
#define rs(o) (o << 1 | 1)
#define mid ((l + r) >> 1)

using namespace std;

inline long long read() {
	long long s = 0, f = 1; char ch = getchar();
	while(ch < '0' || ch > '9') { if(ch == '-') f = -1; ch = getchar(); }
	while(ch >= '0' && ch <= '9') { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); }
	return s * f;	
}

const int N = 5e5 + 5;
long long n, m;
struct tree { long long sum; } t[N << 2];

void up(int o) {
	t[o].sum = t[ls(o)].sum + t[rs(o)].sum;
}

void build(int o, int l, int r) {
	if(l == r) { t[o].sum = read(); return ;}
	build(ls(o), l, mid); build(rs(o), mid + 1, r);
	up(o);
}

void change(int o, int l, int r, int x, int y) {
	if(l == r) { t[o].sum = sqrt(t[o].sum); return ; }
	if(t[o].sum <= r - l + 1) { return ; }
	if(x <= mid) change(ls(o), l, mid, x, y);
	if(y > mid) change(rs(o), mid + 1, r, x, y);
	up(o);
}
 
long long query(int o, int l, int r, int x, int y) {
	if(x <= l && y >= r) { return t[o].sum; }
	long long res = 0;
	if(x <= mid) res += query(ls(o), l, mid, x, y);
	if(y > mid) res += query(rs(o), mid + 1, r, x, y);
	return res;
}
 
int main() {
	
	n = read(); 
	build(1, 1, n);
	m = read();
	for(int i = 1, opt, x, y;i <= m; i++) {
		opt = read(); x = read(); y = read();
		if(x > y) swap(x, y); //一定记得判断一下
		if(opt == 0) {
			change(1, 1, n, x, y);
 		}
		else {
			printf("%lld\n", query(1, 1, n, x, y));
		}
	}
	
	return 0;
}

摇花手飞走(233 )

posted @ 2020-07-21 06:56  C锥  阅读(253)  评论(5编辑  收藏  举报