线段树从入门到出门

news/2025/1/7 1:59:28/文章来源:https://www.cnblogs.com/biyimouse/p/18647453

线段树详介(带lazy)

线段树和树状数组不同,它维护的是一个个子序列。

img

如上图,对于一个区间 \([l, r]\),它的左儿子就是 \([l, mid]\),右儿子就是 \([mid + 1, r]\),其中 \(mid = \frac{l+r}{2}\)

我们可以给线段树上的每一个结点编号,假设父节点编号为 \(x\),左儿子编号就是 \(x\times2\),右儿子编号就是 \(x\times2+1\)

struct Stree {int l, r, v;
} tr[N * 4]; 

以上代码是最简单的线段树,其中 \(l, r\) 为区间的左右端点,\(v\) 为区间内所有数的最大值。

然后我们来说如何维护线段树。

首先,建树

我们可以参照上图,我们从根节点开始,每次分别递归左儿子和右儿子,直到叶节点。此时它的最大值就应该是它本身。然后我们回溯到父节点,由于我们已经对它的儿子都做过了,所以我们可以用儿子的值求出父亲,即父亲的最大值可以通过儿子的最大值推出来。

void pushup(int u) {tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}
void build(int u, int l, int r) {tr[u].l = l, tr[u].r = r;if (l == r) return;int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);pushup(u);
}build(1, 1, n);

以上代码中,\(build\) 就是建树,\(pushup\) 就是用儿子来更新父亲。

有的时候我们可能会这样写 \(build\)

void build(int u, int l, int r) {if (l == r) tr[u] = {l, r, w[r]};else {int mid = l + r >> 1;tr[u] = {l, r};build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);pushup(u);}
}

此时请注意 \(else\) 中也要为 \(tr[u]\) 所对应区间赋值。

\(build\)\(O(nlogn)\)

接着,我们考虑单点修改。

假设当前我们要修改下标为 \(x\) 的数,那么从根节点开始,对于一个父节点, \(x\) 一定在它的左儿子或是右儿子。我们只要从中选择一个即可。

修改完儿子后,我们同样要 \(pushup\)

void modify(int u, int x, int val) {if (tr[u].l == x && tr[u].r == x) {tr[u].v = val;} else {int mid = tr[u].l + tr[u].r >> 1;if (x <= mid) modify(u << 1, x, val);if (x > mid) modify(u << 1 | 1, x, val);pushup(u);}
} 

\(modify\) 显然是 \(O(logn)\) 的。

然后是区间查询。

区间查询就是对于一个区间 \([l, r]\) 询问它的最大值,总和,或是其它线段树维护的值,这里以最大值为例。

对于线段树上的一个结点 \(u\),我们可以分四种情况:

  1. \(u\) 存储的区间被 \([l, r]\) 包含,那么我们直接范围 \(u\) 即可。
  2. \(u\) 属于 \([l, mid]\),那么我们递归左儿子。
  3. \(u\) 属于 \([mid + 1, r]\),我们递归右儿子。
  4. \(u\) 横跨左右儿子,我们左右一起递归。

下面是一个简便写法。

int query(int u, int l, int r) {if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;int mid = tr[u].l + tr[u].r >> 1;int s = 0;if (l <= mid) s = query(u << 1, l, r);if (r > mid) s = max(s, query(u << 1 | 1, l, r));return s;
}

为什么要分四种情况讨论呢?因为有的题目需要我们维护一些横跨两个子区间的信息,所以需要在第四种情况时进行额外的操作。

由于答案一般能够分成 \(O(logn)\) 个区间,所以 \(query\) 的时间复杂度是 \(O(logn)\) 的。

然后是比较困难的区间修改。

我们首先可以想到对于区间的每个点进行单点修改,但这样过于慢。我们发现,如果对一段区间进行修改后,它的儿子的值也都会变化,所以在最坏情况下我们会遍历整棵树。

这里我们引入懒标记,即为了偷懒而生的标记。我们在维护线段树上结点的信息的同时,再维护一个 \(add\)(这里以区间增加为例),意思是我们需要给该结点的所有儿子都加上 \(add\)(儿子是一个序列,如果长为 \(k\),那就相当于序列中的每个数都加 \(add\),即总和加 \(k\times add\))。

我们在执行区间修改的时候,如果某个结点表示的区间被询问的区间包含,那么就修改它的懒标记,同时更改这个区间的和。

void pushdown(int u) {STree &left = tr[u << 1], &right = tr[u << 1 | 1], &root = tr[u];if (root.add) {// 给区间内的每个数加上 add,那么区间的总和就要加上区间长度 * addleft.add += root.add, left.s += (LL)(left.r - left.l + 1) * root.add;right.add += root.add, right.s += (LL)(right.r - right.l + 1) * root.add;root.add = 0;}
}
void modify(int u, int l, int r, int d) {if (l <= tr[u].l && tr[u].r <= r) {tr[u].add += d;tr[u].s += (LL)(tr[u].r - tr[u].l + 1) * d;} else {pushdown(u);int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}
}
LL query(int u, int l, int r) {if (l <= tr[u].l && tr[u].r <= r) return tr[u].s;pushdown(u);int mid = tr[u].l + tr[u].r >> 1; LL res = 0;if (l <= mid) res += query(u << 1, l, r);if (r > mid) res += query(u << 1 | 1, l, r);return res;
}

我们发现在 \(modify\) 中多出了一个 \(pushdown\),它的含义就是将父亲的懒标记下传,因为此时我们要遍历子区间了。

同时查询操作也要加上 \(pushdown\) 操作。

245. 你能回答这些问题吗 - AcWing题库

我们考虑在线段树中维护 \(lmax\),意思是当前区间最大连续前缀和,同样维护 \(rmax\)

此时父节点的最大连续子段和就是子节点的最大连续子段和,以及左儿子的 \(rmax\) 加上右儿子的 \(lmax\) 的最大值。

#include <bits/stdc++.h>
using namespace std;
const int N = 500010;
int n, m;
int a[N];struct Stree {int l, r, s, smax, lmax, rmax;
} tr[N * 4];void pushup(Stree &u, Stree &a, Stree &b) {u.s = a.s + b.s;u.lmax = max(a.lmax, a.s + b.lmax);u.rmax = max(b.rmax, a.rmax + b.s);u.smax = max({a.smax, b.smax, a.rmax + b.lmax});
}void pushup(int u) {pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}void build(int u, int l, int r) {if (l == r) tr[u] = {l, r, a[r], a[r], a[r], a[r]};else {tr[u] = {l, r};int mid = l + r >> 1;build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);pushup(u);}
}void modify(int u, int x, int v) {if (tr[u].l == x && tr[u].r == x) tr[u].s = tr[u].smax = tr[u].lmax = tr[u].rmax = v;else {int mid = tr[u].l + tr[u].r >> 1;if (x <= mid) modify(u << 1, x, v);else modify(u << 1 | 1, x, v);pushup(u);}
}Stree query(int u, int l, int r) {if (tr[u].l >= l && tr[u].r <= r) return tr[u];else {int mid = tr[u].l + tr[u].r >> 1;if (r <= mid) return query(u << 1, l, r);else if (l > mid) return query(u << 1 | 1, l, r);else {auto a = query(u << 1, l, r), b = query(u << 1 | 1, l, r);Stree c;pushup(c, a, b);return c;}}
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);build(1, 1, n);while (m --) {int op, l, r;scanf("%d%d%d", &op, &l, &r);if (op == 1) {if (l > r) swap(l, r);printf("%d\n", query(1, l, r).smax);} else {modify(1, l, r);}}return 0;
}

246. 区间最大公约数 - AcWing题库

由更相减损术我们可以知道:

\[\gcd(w[i], w[i + 1], w[i + 2], ..., w[j]) = gcd(w[i], w[i + 1] - w[i], w[i + 2] - w[i - 1], ..., w[j] - w[j - 1]) \]

这其实是差分的形式,这启发我们用线段树维护差分。

具体我们可以在线段树中维护差分,具体地我们维护一个 \(s\) 表示 \([l, r]\) 的差分和,再维护一个 \(d\) 表示 \([l, r]\) 的最大公约数。

那么原来的式子就可以表示为 \(gcd(w[i], gcd(b[i + 1], ..., b[j])) = gcd(query(1, i).s, query(i + 1, j).d)\),其中 \(query\) 返回的是一整个线段树结点。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 500010;
int n, m;
LL w[N];struct Stree {int l, r; LL s, d;
} tr[N * 4];LL gcd(LL a, LL b) {return b ? gcd(b, a % b) : abs(a);
}void pushup(Stree &u, Stree &l, Stree &r) {u.s = l.s + r.s;u.d = gcd(l.d, r.d);
}void pushup(int u) {pushup(tr[u], tr[u << 1], tr[u << 1 | 1]);
}void build(int u, int l, int r) {if (l == r) tr[u] = {l, r, w[r] - w[l - 1], w[r] - w[l - 1]};else {tr[u] = {l, r}; int mid = l + r >> 1;build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);pushup(u);}
}void modify(int u, int x, LL v) {if (tr[u].l == x && tr[u].r == x) tr[u] = {x, x, tr[u].s + v, tr[u].s + v};else {int mid = tr[u].l + tr[u].r >> 1;if (x <= mid) modify(u << 1, x, v);else modify(u << 1 | 1, x, v);pushup(u);}
}Stree query(int u, int l, int r) {if (tr[u].l >= l && tr[u].r <= r) return tr[u];else {int mid = tr[u].l + tr[u].r >> 1;if (l > mid) return query(u << 1 | 1, l, r);else if (r <= mid) return query(u << 1, l, r);else {auto a = query(u << 1, l, r), b = query(u << 1 | 1, l, r);Stree c;pushup(c, a, b);return c;}}
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++) scanf("%lld", &w[i]);build(1, 1, n);while (m --) {char op[2]; int l, r; LL d;scanf("%s%d%d", op, &l, &r);if (*op == 'Q') {if (l == r) printf("%lld\n", query(1, 1, l).s);else {auto a = query(1, 1, l), b = query(1, l + 1, r);printf("%lld\n", gcd(a.s, b.d));}} else {scanf("%lld", &d);modify(1, l, d); if (r + 1 <= n) modify(1, r + 1, -d);}}return 0;
}

243. 一个简单的整数问题2 - AcWing题库

懒标记模板。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 100010;
int n, m, w[N];struct STree {int l, r;LL s, add;
} tr[N * 4];void pushup(int u) {tr[u].s = tr[u << 1].s + tr[u << 1 | 1].s;
}void pushdown(int u) {STree &left = tr[u << 1], &right = tr[u << 1 | 1], &root = tr[u];if (root.add) {left.add += root.add, left.s += (LL)(left.r - left.l + 1) * root.add;right.add += root.add, right.s += (LL)(right.r - right.l + 1) * root.add;root.add = 0;}
}void build(int u, int l, int r) {if (l == r) tr[u] = {l, r, w[r], 0};else {int mid = l + r >> 1;tr[u] = {l, r};build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);pushup(u);}
}void modify(int u, int l, int r, int d) {if (l <= tr[u].l && tr[u].r <= r) {tr[u].add += d;tr[u].s += (LL)(tr[u].r - tr[u].l + 1) * d;} else {pushdown(u);int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}
}LL query(int u, int l, int r) {if (l <= tr[u].l && tr[u].r <= r) return tr[u].s;pushdown(u);int mid = tr[u].l + tr[u].r >> 1; LL res = 0;if (l <= mid) res += query(u << 1, l, r);if (r > mid) res += query(u << 1 | 1, l, r);return res;
}int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++) scanf("%d", &w[i]);build(1, 1, n);while (m --) {char op[2]; int l, r;scanf("%s%d%d", op, &l, &r);if (*op == 'C') {int d; scanf("%d", &d);modify(1, l, r, d);} else {printf("%lld\n", query(1, l, r));}}return 0;
}

扫描线

假设当前平面内有一堆的矩形,我们要统计它们的面积(不能重复),那么我们就可以像下图一样,用一条平行 \(y\) 轴的扫描线从左往右,扫。根据下图,总面积就应该是 \(\sum^{2n - 1}_{i = 1}h_i \times (x_{i + 1} - x_i)\),其中 \(n\) 为矩形的数量,\(h_i\) 为扫描线上覆盖有矩形的长度。

具体地,我们把每扫到一个矩形的左宽,就把左宽覆盖的区间 \(+1\),扫到右宽就 \(-1\),这样就可以算出 \(h_i\)

我们发现该操作设计区间修改和区间查询,所以我们考虑用线段树加速。

线段树中存储 \(cnt\) 表示直接完全覆盖此区间的矩形数量,\(len\) 表示此区间被矩形覆盖的长度。

并且注意,线段树上的每个叶节点存的都是区间,所以需要注意坐标。

根据以上的存储方式,可能会出现子节点的 \(cnt\) 大于父节点的 \(cnt\) 的情况。

我们再考虑 \(pushup\) 操作如何写。分三种情况:

  1. \(cnt \ne 0\),说明该区间被完全覆盖,\(len\) 就为该区间的长度。
  2. \(cnt = 0\),且 \(l \ne r\),可以用左儿子和右儿子来更新自己。
  3. \(cnt = 0\),且 \(l = r\),不合法,无用线段,\(len = 0\)

我们可以通过一些手段证明 \(pushdown\) 某些时候是不用写的。(另外一些时候比如第二道例题要写,下文会提到)。

引用的证明:

此时 \(cnt\) 表达的含义:当前区间被覆盖的次数,跟其它节点无关。

可以发现,因为对于修改区间 \([l, r]\) 操作,是一对一对的。

所以,一个节点代表的区间被覆盖的次数不需要继承其父亲信息的情况。

因此需要去掉 \(pushDown\)

img

我们 \(Atlantis\) 一题为例。

247. 亚特兰蒂斯 - AcWing题库

在上文的思路上,由于本题坐标可能有小数,所以需要进行离散化。

#include <bits/stdc++.h>
using namespace std;
const int N = 10010;
int n;
vector<double> tmp;struct Segment {double x, y1, y2;int k;bool operator < (const Segment &t) const{return x < t.x;}
} segs[N * 2];struct Stree {int l, r, cnt;double len;
} tr[N * 8];// tmp 存的是小数,它在 tmp 中的位置就是离散化的值
// find 查找的是某个小数被映射到了什么位置
int find(double v) {return lower_bound(tmp.begin(), tmp.end(), v) - tmp.begin();
}void pushup(int u) {if (tr[u].cnt) {tr[u].len = tmp[tr[u].r + 1] - tmp[tr[u].l];} else if (tr[u].l != tr[u].r) {tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len;} else {tr[u].len = 0;}
}void build(int u, int l, int r) {if (l == r) tr[u] = {l, r, 0, 0};else {tr[u] = {l, r};int mid = l + r >> 1;build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r);}
}void modify(int u, int l, int r, int d) {if (l <= tr[u].l && tr[u].r <= r) {tr[u].cnt += d; pushup(u);} else {int mid = tr[u].l + tr[u].r >> 1;if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}
}int main() {int T = 0;while (scanf("%d", &n), n) {tmp.clear();for (int i = 1, j = 0; i <= n; i ++) {double x_1, y_1, x_2, y_2;scanf("%lf%lf%lf%lf", &x_1, &y_1, &x_2, &y_2);segs[j ++] = {x_1, y_1, y_2, 1};segs[j ++] = {x_2, y_1, y_2, -1};tmp.push_back(y_1); tmp.push_back(y_2);}sort(segs, segs + 2 * n);sort(tmp.begin(), tmp.end());tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end());build(1, 0, tmp.size() - 2);    double res = 0;for (int i = 0; i < n * 2; i ++) {if (i > 0) res += tr[1].len * (segs[i].x - segs[i - 1].x);modify(1, find(segs[i].y1), find(segs[i].y2) - 1, segs[i].k);}printf("Test case #%d\n", ++ T);printf("Total explored area: %.2lf\n\n", res);}return 0;
}

248. 窗内的星星 - AcWing题库

题解 P1502 【窗口的星星】 - 洛谷专栏 (luogu.com.cn)

思路这篇题解说的很明白,我们来谈谈这个问题为什么需要懒标记。

最重要的一点是,我们线段树中存的数据不再是 \(cnt\)\(len\),而是 \(sm\) 表示的该子区间对应的亮度总和。亮度的增加不同于 \(cnt\) 是要下传的。并且线段树中叶子存的区间不再是 \([l, l+1]\) 而是 \([l, l]\),可以理解为一个点。所以我们需要 \(lasytag\) 来帮助操作。

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 10010;
int n, w, h;
vector<LL> tmp;struct Segment {LL x, l, r, c;bool operator < (const Segment &t) const{return x < t.x || x == t.x && c > t.c;}
} segs[N * 2];struct Stree {int l, r; LL sm, add; 
} tr[N * 8];LL find(LL x) {return lower_bound(tmp.begin(), tmp.end(), x) - tmp.begin();
}void pushup(int u) {tr[u].sm = max(tr[u << 1].sm, tr[u << 1 | 1].sm);
}void pushdown(int u) {if (tr[u].add) {tr[u << 1].sm += tr[u].add;tr[u << 1].add += tr[u].add;tr[u << 1 | 1].sm += tr[u].add;tr[u << 1 | 1].add += tr[u].add;tr[u].add = 0; }
}void build(int u, int l, int r) {tr[u] = {l, r, 0, 0};if (l != r) {int mid = l + r >> 1;build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);}
}void modify(int u, int l, int r, int d) {if (l <= tr[u].l && tr[u].r <= r) {tr[u].sm += d, tr[u].add += d;} else {int mid = tr[u].l + tr[u].r >> 1;pushdown(u);if (l <= mid) modify(u << 1, l, r, d);if (r > mid) modify(u << 1 | 1, l, r, d);pushup(u);}
}int main() {ios::sync_with_stdio(false);cin.tie(0); cout.tie(0);while (cin >> n >> w >> h) {tmp.clear();for (int i = 1, j = 0; i <= n; i ++) {LL x, y, c;cin >> x >> y >> c;segs[j ++] = {x, y, y + h - 1, c};segs[j ++] = {x + w - 1, y, y + h - 1, -c};tmp.push_back(y); tmp.push_back(y + h - 1);}sort(tmp.begin(), tmp.end());tmp.erase(unique(tmp.begin(), tmp.end()), tmp.end());sort(segs, segs + n * 2);  build(1, 0, tmp.size() - 1);LL res = 0;for (int i = 0; i < n * 2; i ++) {modify(1, find(segs[i].l), find(segs[i].r), segs[i].c);res = max(res, tr[1].sm);}cout << res << endl;}return 0;
}

动态开点

根据上文所说,一棵普通线段树要用到 \(4 n\) 的空间,我们为了节省空间,可以采用动态开点的方式。具体地,线段树上的每个结点的 \(l\)\(r\) 不再表示它的左、右儿子,而是直接表示它的左、右子结点的坐标。所以在动态开点线段树中我们不能再用 \(p << 1\)\(p << 1 | 1\) 来访问左右儿子,而是直接使用 \(tr[u].l\)\(tr[u].r\)。为了简化代码,我们可以在程序开头宏定义 #define lc(u) tr[(u)].l\(rc(u)\) 同理)。

总而言之,动态开点线段树的精髓就是只开要用到的点。动态开点线段树的空间复杂度应该是 \(mlogn\) 级别的,且一定小于 \(2n\)

这里以 1275. 最大数 - AcWing题库 为例(即线段树维护区间最大值),代码如下。

#include <bits/stdc++.h>
using namespace std;
#define lc(x) tr[(x)].lc
#define rc(x) tr[(x)].rc
#define v(x) tr[(x)].v
const int N = 200010;int m, p, idx;
struct node {int lc, rc, v;
} tr[N << 1];void pushup(int u) {v(u) = max(v(lc(u)), v(rc(u)));
}void update(int &u, int l, int r, int x, int val) {if (!u) u = ++ idx;if (l == r) { v(u) = val; return; }int mid = l + r >> 1;if (x <= mid) update(lc(u), l, mid, x, val);else update(rc(u), mid + 1, r, x, val);pushup(u);
}int query(int u, int cl, int cr, int l, int r) {if (!u) return 0;if (l <= cl && cr <= r) return v(u);int mid = cl + cr >> 1;if (l > mid) return query(rc(u), mid + 1, cr, l, r);else if (r <= mid) return query(lc(u), cl, mid, l, r);else return max(query(lc(u), cl, mid, l, r), query(rc(u), mid + 1, cr, l, r));
}int main() {scanf("%d%d", &m, &p);int last = 0, n = 0, rt = 0;while (m --) {char s[2]; int x;scanf("%s%d", s, &x);if (*s == 'A') {x = ((long long)x + last) % p;update(rt, 1, 200000, ++ n, x);} else {last = query(rt, 1, 200000, n - x + 1, n);printf("%d\n", last);}}return 0;
}

这里的代码作为示例就不把线段树装进结构体里了,一般我的写法是:

struct segment_tree_node {int l, r;int ...
} tr[范围];
struct segment_tree {... // 需要使用的函数
} stree;stree.函数名;

动态开点其实有点类似于 Trie,在后面的部分会经常用到。

可持久化线段树(主席树)

可持久化线段树(下文均称为主席树)指的是将线段树每一时刻的线段树都保存下来,进行查询的数据结构。可以与可持久化 Trie 进行类比。同样地,我们不能将所有版本都存下来。但是我们发现一次操作,比如单点修改,只会经过一条链上的点,所以我们可以从上一个版本开始遍历,将经过的所有点拷贝一份出来,进行修改。

如下图(来自 oiwiki),黑色的 \(1,2,4,8\) 属于上一个版本,我们以 \(2\) 举例。首先我们将 \(2\) 这个点拷贝一份给最新版本,然后考虑接下来要递归左子树还是右子树。对于假如我们递归左子树就更改最新版本新拷贝出来点的左儿子即可。

在刚刚的过程中我们其实可以发现所做的过程是不好用堆式存储的,所以我们选择动态开点。

我们直接来看一道经典例题。

P3834 【模板】可持久化线段树 2

本题需要求的是 \([l, r]\) 区间内的第 \(k\) 小数。我们可以使用权值主席树,根据上文的描述,我们可以很轻松地求出 \([1, r]\) 区间内的信息和 \([1, l - 1]\) 的信息,因为可以直接调用 \(r\)\(l - 1\) 号版本。所以我们可以运用前缀和的思想,同时遍历两个版本,然后错位相减即可求出答案。

具体过程中,如果左子树的结点个数 \(cnt\) 大于等于当前要求的 \(k\),我们就递归左子树,否则我们递归右子树并让 \(k - cnt\)

由于值域很大,我们需要离散化。接着再分析一下空间问题,由于我们是动态开点,且维护的是值域线段树,所以总的空间应该是 \(O(MlogV + 2N - 1)\),这里 \(N,M\) 同阶,且离散化后 \(V\) 也和 \(N\) 同阶,所以可以看做 \(O(NlogN + 2N)\)。由于 \(N \leq 200000\),所以总共就 \(20N\) 左右,我们直接开到 \(N<<5\) 即可。

#include <bits/stdc++.h>
using namespace std;
#define lc(x) tr[(x)].l
#define rc(x) tr[(x)].r
#define v(x) tr[(x)].v
const int N = 200010;
int n, m, a[N], tmp[N];int cnt;
struct stree_node {int l, r, v;
} tr[N << 5];
struct Stree {void pushup(int u) {v(u) = v(lc(u)) + v(rc(u));        }// 这个 build 事实上毫无用处,因为并没有对值进行任何修改int build(int l, int r) {int p = ++ cnt;if (l == r) return p;int mid = l + r >> 1;lc(p) = build(l, mid); rc(p) = build(mid + 1, r);return p;}int modify(int q, int l, int r, int x, int val) {int p = ++ cnt;tr[p] = tr[q];if (l == r) { v(p) += val; return p; }int mid = l + r >> 1;if (x <= mid) lc(p) = modify(lc(q), l, mid, x, val);else rc(p) = modify(rc(q), mid + 1, r, x, val);pushup(p);return p;}int query(int q, int p, int l, int r, int k) {// 找到了if (l == r) return r;int cnt = v(lc(p)) - v(lc(q));int mid = l + r >> 1;if (k <= cnt) return query(lc(q), lc(p), l, mid, k);else return query(rc(q), rc(p), mid + 1, r, k - cnt);}
} stree;
int rt[N];int main() {scanf("%d%d", &n, &m);for (int i = 1; i <= n; i ++) { scanf("%d", &a[i]); tmp[i] = a[i]; }sort(tmp + 1, tmp + 1 + n);int k = unique(tmp + 1, tmp + 1 + n) - tmp - 1;for (int i = 1; i <= n; i ++) a[i] = lower_bound(tmp + 1, tmp + 1 + k, a[i]) - tmp;rt[0] = stree.build(1, k); // 同理这一句也没用for (int i = 1; i <= n; i ++) rt[i] = stree.modify(rt[i - 1], 1, k, a[i], 1); while (m --) {int l, r, x; scanf("%d%d%d", &l, &r, &x);printf("%d\n", tmp[stree.query(rt[l - 1], rt[r], 1, k, x)]);}return 0;
}

先写这些,其它的题有空再写。

线段树合并

线段树合并会用到动态开点,所以上文特地提了几句。

顾名思义,线段树合并就是把两棵线段树合并在一起。我们同样地动态开点,假设要将 \(B\) 合并到 \(A\),那么就从 \(A\)\(B\) 两棵线段树的根出发,往儿子递归。如果在某一时刻 \(A\)\(B\) 对应的结点是空的,就直接返回另一棵树上的对应节点。如果当前遍历到叶子结点,就将 \(B\) 对应的值加到 \(A\) 上。然后递归地合并左子树和右子树,做完了后再 \(pushup\) 一遍。

我们还是以一道题为例。

P4556 Vani有约会

题意概括一下就是有两种操作,一种是给 \(x\)\(y\) 的路径上的每个点发一包 \(z\) 类型的储备粮,一种是查询一个点上储存最多的是哪种类型的粮。

我们会发现在树上区间修改的话是非常不好做的,所以我们考虑将操作一转换为差分。就应该是在 \(y\) 到根节点的路径上发一包 \(z\)\(x\) 到根节点的路径上发一包 \(z\),在 \(lca(x, y)\) 到根节点的路径上处没收一包 \(z\),因为 \(lca\) 是被重复计算了,最后再在 \(fa_{lca(x, y)}\) 到根节点的路径上没收一包 \(z\)。我们发现这其实可以直接单点修改,最后再从下往上做一次前缀和即可。

于是我们可以想到,对于每一个结点动态开一棵权值线段树,每次进行四次单点修改。最后再从下往上进行线段树合并即可。

我们分析一下空间,由于每个修改会转换为四次操作,所以总共的空间应该是 \(O(4MlogV + 2N)\),转化为 \(4NlogN + 2N\),其中 \(logN\) 大概是 \(17\),我们直接开 \(80N\) 即可。

再看时间,我们分析线段树合并的过程,显然每次执行 \(merge\) 递归时,一定会将一个点合并到另一个点上,相当于每个点只递归一次(注意是相当),这样总共的复杂度就应该和线段树的总点数同阶。大概是 \(mlogn\) 级别的。当然这个复杂度并不是非常科学,而且常数比较大。还是更建议使用可并堆,可并堆后面会另开分享。

代码:

#include <bits/stdc++.h>
using namespace std;
#define lc(x) tr[(x)].l
#define rc(x) tr[(x)].r
const int N = 100010;int h[N], e[N << 1], ne[N << 1], idx;
void add(int a, int b) {e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}int n, m, cnt; struct Stree_node {int l, r, s, p;
} tr[N * 20 * 4];
struct Stree {void pushup(int u) {int l = lc(u), r = rc(u);if (!l || tr[l].s < tr[r].s) tr[u].s = tr[r].s, tr[u].p = tr[r].p;else tr[u].s = tr[l].s, tr[u].p = tr[l].p;}void modify(int &u, int l, int r, int x, int val) {if (!u) u = ++ cnt;if (l == r) { tr[u].s += val, tr[u].p = x; return; }int mid = l + r >> 1;if (x <= mid) modify(lc(u), l, mid, x, val);else modify(rc(u), mid + 1, r, x, val);pushup(u);}int merge(int a, int b, int l, int r) {if (!a || !b) return a + b;if (l == r) { tr[a].s += tr[b].s; return a; }int mid = l + r >> 1;lc(a) = merge(lc(a), lc(b), l, mid);rc(a) = merge(rc(a), rc(b), mid + 1, r);pushup(a);return a;}
} stree; int fa[N][20], depth[N];
void init() {queue<int> q; q.push(1);memset(depth, 0x3f, sizeof depth);depth[0] = 0, depth[1] = 1;while (q.size()) {int ver = q.front(); q.pop();for (int i = h[ver]; ~i; i = ne[i]) {int j = e[i];if (depth[j] > depth[ver] + 1) {depth[j] = depth[ver] + 1;q.push(j);fa[j][0] = ver;for (int k = 1; k < 20; k ++)fa[j][k] = fa[fa[j][k - 1]][k - 1]; }}}
}
int lca(int a, int b) {if (depth[a] < depth[b]) swap(a, b);for (int i = 19; i >= 0; i --)if (depth[fa[a][i]] >= depth[b])a = fa[a][i];if (a == b) return a;for (int i = 19; i >= 0; i --)if (fa[a][i] != fa[b][i])a = fa[a][i], b = fa[b][i];return fa[a][0];
}// rt 指的是每棵线段树的根节点位置
int rt[N], ans[N];
void calc(int u, int father) {for (int i = h[u]; ~i; i = ne[i]) {int j = e[i];if (j == father) continue;calc(j, u);rt[u] = stree.merge(rt[u], rt[j], 1, 100000);} ans[u] = tr[rt[u]].p;// 这里要根据题意特判if (tr[rt[u]].s == 0) ans[u] = 0;
} int main() {scanf("%d%d", &n, &m);memset(h, -1, sizeof h);for (int i = 1; i < n; i ++) {int a, b; scanf("%d%d", &a, &b);add(a, b); add(b, a);}init();while (m --) {int x, y, z; scanf("%d%d%d", &x, &y, &z);stree.modify(rt[x], 1, 100000, z, 1);stree.modify(rt[y], 1, 100000, z, 1);int LCA = lca(x, y);stree.modify(rt[LCA], 1, 100000, z, -1);stree.modify(rt[fa[LCA][0]], 1, 100000, z, -1);}calc(1, -1);for (int i = 1; i <= n; i ++) printf("%d\n", ans[i]);return 0;
}

这种数据结构题其实并不是很难写,因为基本上全都是模板,具体操作的时候添加一下就行了。

最后一个事实是,带 \(lazy\) 的线段树合并很困难,听说可以用标记永久化,但感觉比较黑科技。

P8123 BalticOI 2021 Day1 Inside information(主席树合并)

这道题需要一些小技巧。

首先我们发现 \(S\)\(Q\) 操作就是裸的线段树合并,和上题的思路类似,每个结点开一棵权值线段树即可,查询就正常线段树查询。

那么 \(C\) 操作怎么做呢?我们显然不能把所有的树全部都跑一遍。但我们可以将每个合并操作视为连边,以合并的时间为边权画出图。

如上图。我们首先要搞清楚如果 \(A\) 先和 \(B\) 合并,再和 \(C\) 合并,此时的 \(A\)\(B\) 是不一样的,但是 \(B\) 一定被 \(A\) 包含。我们发现一个点 \(u\) 能包含 \(v\) 的条件是 \(u\)\(v\) 的边权单调递减,原因就是刚才提到的。现在我们要求的是有多少个点含有一个数据块 \(a\),就相当于求符合 \(a\)\(b\) 的路径上边权单调递增的点的数量加一(因为还有自己)。注意如果一个路径上只有一条边那它也是答案。

单调递增不是很好用线段树合并时维护(即 $$),于是我们可以想到倒序建图,即从最后一个合并操作开始,第一个合并操作结束。还是样例,我们可以建出这么一个图:

我们会发现此时的答案就时所有单调递减能到的边了,这是很好用线段树合并维护的。而且由于我们是先把所有的操作都进行完,再进行查询操作,相当于离线,所以需要主席树。

主席树合并和普通树合并很像,直接在下文代码中体现了。

#include <bits/stdc++.h>
using namespace std;
const int N = 240010;
int n, k;struct stree_node {int l, r; int v;
} tr[N << 5];
int cnt;
struct Stree {void pushup(int u) {tr[u].v = tr[tr[u].l].v + tr[tr[u].r].v;}int modify(int q, int l, int r, int x, int val) {int p = ++ cnt;tr[p] = tr[q]; // 警钟长鸣,这是一颗可持久化线段树,这一行一定不能漏if (l == r) { tr[p].v += val; return p; }int mid = l + r >> 1;if (x <= mid) tr[p].l = modify(tr[q].l, l, mid, x, val);else tr[p].r = modify(tr[q].r, mid + 1, r, x, val);pushup(p);return p;}int merge(int a, int b, int l, int r) {if (!a || !b) return a + b;int u = ++ cnt;if (l == r) { tr[u].v = tr[a].v + tr[b].v; return u; }int mid = l + r >> 1;tr[u].l = merge(tr[a].l, tr[b].l, l, mid);tr[u].r = merge(tr[a].r, tr[b].r, mid + 1, r);pushup(u);return u;}int query1(int u, int l, int r, int x) {if (l == r) return tr[u].v;int mid = l + r >> 1;if (x <= mid) return query1(tr[u].l, l, mid, x);else return query1(tr[u].r, mid + 1, r, x);}int query2(int u, int l, int r, int x) {if (!u || l > x || r < 1) return 0;if (r <= x) return tr[u].v;int mid = l + r >> 1;return query2(tr[u].r, mid + 1, r, x) + query2(tr[u].l, l, mid, x);  }
} stree;struct node {int a, d, op;
} q[N];
int ans[N], rt[N];int main() {scanf("%d%d", &n, &k);for (int i = 1; i <= n; i ++) rt[i] = stree.modify(0, 1, n, i, 1);for (int i = 1; i < n + k; i ++) {char op[2]; scanf("%s", op);if (*op == 'S') {int a, b; scanf("%d%d", &a, &b); q[i] = {a, b, 0};rt[a] = rt[b] = stree.merge(rt[a], rt[b], 1, n);} else if (*op == 'Q') {int a, d; scanf("%d%d", &a, &d);q[i] = {a, d, 1};ans[i] = stree.query1(rt[a], 1, n, d);} else {int x; scanf("%d", &x);q[i] = {x, 0, 2};}}memset(tr, 0, sizeof tr); cnt = 0;for (int i = 1; i <= n; i ++) rt[i] = ++ cnt;// 维护时刻 t 有多少加入for (int i = n + k - 1; i; i --)if (!q[i].op) {int a = q[i].a, b = q[i].d;rt[a] = stree.modify(rt[a], 1, n + k - 1, i, 1);rt[a] = stree.merge(rt[a], rt[b], 1, n + k - 1); rt[b] = rt[a];}for (int i = 1; i < n + k; i ++)if (q[i].op == 1) puts(ans[i] ? "yes" : "no");else if (q[i].op == 2) printf("%d\n", stree.query2(rt[q[i].a], 1, n + k - 1, i) + 1);return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/862881.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

(回顾)2023年度项目管理软件全方位对比:14款最佳工具推荐

在当今竞争激烈的商业环境中,高效的项目管理是企业成功的关键因素之一。无论是初创公司还是大型企业,选择一款合适的项目管理软件能够极大地提升团队协作效率、优化资源分配,并确保项目按时交付。2023 年,市场上涌现出众多优秀的项目管理工具,每一款都有其独特的优势和适用…

2025/1/2 【双指针法】LeetCode27.移除元素 【√】 ❗未完结❗

27. 移除元素 - 力扣(LeetCode) 代码随想录 数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。 My answer:快慢指针法class Solution:def removeElement(self, nums: List[int], val: int) -> int:n = len(nums)j = 0for i in range(n):if nums…

2025/1/2 【双指针法】LeetCode27.移除元素 【√】

27. 移除元素 - 力扣(LeetCode) 代码随想录 数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。 My answer:快慢指针法class Solution:def removeElement(self, nums: List[int], val: int) -> int:n = len(nums)j = 0for i in range(n):if nums…

招银网络一面,有点难度!

这是一位武汉理工大学同学的招银网络一面面经,同样附带超详细的参考答案。大家可以用来查漏补缺,针对性地补短板。 招银网络一面还是比较简单的,基本都是一些比较重要且高频的常规八股,项目问的不多。到了二面的时候, 会开始主要考察你的项目。1、自我介绍 自我介绍一般是…

【日记】2025 年的第一天!!!!(1688 字)

正文嗯…… 经过实践,以后还是把涩涩放到晚上好了,下午涩涩完就只想睡觉了。一天的元旦假期,下午就睡过去了哈哈哈哈哈哈哈哈哈。今天也没干什么,上午加了班,吃午饭,下午咳咳,睡了一觉,吃晚饭,然后看了两节内科学的视频。我不确定该不该勾掉总论部分。唐子益的内科学暂…

BOE(京东方)“向新2025”年终媒体智享会落地深圳 “屏”实力赋能产业创新发展

12月27日,BOE(京东方)“向新 2025”年终媒体智享会的收官之站在创新之都深圳圆满举行,为这场为期两周、横跨三地的年度科技盛会画上了完美句号。活动期间,全面回顾了 BOE(京东方)2024年在多个关键领域取得的卓越成绩,深入剖析其在六大维度构建的“向新”发展格局,精彩…

linux mint安装Idea

一、前言 这一节我们介绍在Linux下如何安装与破解Intellij idea2017。现在有很多公司开发环境都是Linux,所以掌握在Linux环境下使用Idea办公也是咱们必须得掌握的技能。 记住我们下载的是Ultimate(旗舰版),不是Community(社区版)。Ultimate(旗舰版)是收费的版本,本文主要是针…

linux mint下安装截图工具

在linux下尝试了多款截图工具,综合下来,觉得shutter是最好用的,推荐大家使用。 Shutter 是一个强大的截图工具,强大的功能集成到一个直观的简洁界面,应用程序自带多个区域截图(如全屏,选择界面,活动窗口,网站等),方便应用功能,以及有众多插件和上传到Web服务的功能…

linux mint安装eclipse

安装eclipse之前需要先安装配置jdk,参考上面, 一、Eclipse的下载与安装 1、首先,在Eclipse的官网中下载最新版的Luna SR2http://www.eclipse.org/downloads/packages/release/Luna/SR2(点击打开链接)直接下载最新版的Eclipse for JavaEE Linux 32bit/64bit,Eclipse各个版…

Vue 不同版本的vue

1、vue.js与vue.runtime.xxx.js的区别 a、vue.js是完整版的Vue,包含:核心功能和模板解析器。 b、vue.runtime.xx.js是运行版的vue。只包含核心功能,没有模板解析器 2、因为rvue.runtime.xxx.js没有模板解析器,所以不能使用template配置项,需要使用render函数接收到的creat…

视频分析框架VideoPipe完整介绍

(2024年4月编写) github地址 https://github.com/sherlockchou86/video_pipe_c 作者微信 zhzhi78(备注 videopipe),拉群交流(1000人群),及时获取代码更新。 网站介绍 http://www.videopipe.cool/ 配套教程 http://www.videopipe.cool/index.php/2024/09/11/videopipetu…

linux mint安装Scala

Scala由java编写,需要前期安装jdk 面向函数式编程 1、下载 Scala 二进制包2.11.8 http://www.scala-lang.org/downloads 解压到/usr/local/SoftWare/scala-2.11.8 2、配置环境变量 sudo gedit /etc/profile 3、使设置生效 source /etc/profileIdea配置scala file ==setting== …