常用算法题锦——线段树
线段树
题目:
单值修改,区间查询,维护信息:最大值和最大值的个数
题目描述 牛牛有n个宝石,第i个宝石的价值是w[i]. 有m个操作,操作分为两种类型 − Change x y 把第x个宝石的价值改成 y − Ask l r 询问区间[l,r]内宝石的最大价值,和最大价值的宝石有多少个。 输入描述: 第一行两个整数 n , m (1 ≤ n,m ≤ 2e5) 第二行有n个整数 w[i] (0 ≤ w[i] ≤ 1e9) 接下来m行,每行代表一个操作。具体见题目描述。 输出描述: 每次询问输出一行,每行两个整数 val cnt,val代表所有宝石中的最大价值,cnt代表价值最大的宝石有多少个。 示例1 输入 复制 5 3 2 4 3 6 8 Ask 1 5 Change 2 10 Ask 1 3 输出 复制 8 1 10 1
代码:
#include <bits/stdc++.h> using namespace std; const int N = 200005; int n, m; int w[N]; struct Node{ int l, r; int mx, cnt; }tr[N * 4]; struct Seg_Tree{ void pushup(Node &c, Node &a, Node &b) { if (a.mx == b.mx) c.cnt = a.cnt + b.cnt; else if (a.mx > b.mx) c.cnt = a.cnt; else c.cnt = b.cnt; c.mx = max(a.mx, b.mx); } 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[l], 1}; return; } tr[u] = {l, r}; int mid = l + r >> 1; build(u << 1, l, mid); build(u << 1 | 1, mid + 1, r); pushup(u); } void change(int u, int x, int c) { if (tr[u].l == x && tr[u].r == x) { tr[u] = {x, x, c, 1}; return; } int mid = tr[u].l + tr[u].r >> 1; if (x <= mid) change(u << 1, x, c); if (x > mid) change(u << 1 | 1, x, c); pushup(u); } Node query(int u, int l, int r) { if (tr[u].l >= l && tr[u].r <= r) return tr[u]; 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 { Node left = query(u << 1, l, r); Node right = query(u << 1 | 1, l, r); Node ans; pushup(ans, left, right); return ans; } } }t; int main(void) { cin >> n >> m; for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]); t.build(1, 1, n); char op[10]; int l, r; while (m -- ) { scanf("%s", op); scanf("%d%d", &l, &r); if (*op == 'C') { t.change(1, l, r); } else { Node x = t.query(1, l, r); printf("%d %d\n", x.mx, x.cnt); } } return 0; }
扫描线:
题目:
题目描述
求 n 个矩形的面积并。
输入格式
第一行一个正整数 n。
接下来 n 行每行四个非负整数x1,y1,x2,y2,表示一个矩形的左下角坐标为 (x1,y1),右上角坐标为 (x2,y2)。
输出格式
一行一个正整数,表示 n 个矩形的并集覆盖的总面积。
输入输出样例
输入 #12 100 100 200 200 150 150 250 255输出 #118000说明/提示
对于 20% 的数据,1≤n≤1000。
对于 100% 的数据,11≤n≤105,1<x2≤109,0≤y1<y2≤109。
代码:
#include <bits/stdc++.h> using namespace std; const int N = 100005; int n; struct Segment{ int x, y1, y2, mark; bool operator< (const Segment &W) const{ return x < W.x; } }seg[N << 1]; struct Node{ int l, r; int tag, len; }tr[N << 3]; vector<int> ys; void build(int u, int l, int r) { tr[u] = {l, r, 0, 0}; if (l == r) return; int mid = l + r >> 1; build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); } void pushup(int u) { if (tr[u].tag) { tr[u].len = ys[tr[u].r + 1] - ys[tr[u].l]; } else { if (tr[u].l == tr[u].r) tr[u].len = 0; else tr[u].len = tr[u << 1].len + tr[u << 1 | 1].len; } } void update(int u, int l, int r, int c) { if (tr[u].l >= l && tr[u].r <= r) { tr[u].tag += c; pushup(u); return; } int mid = tr[u].l + tr[u].r >> 1; if (l <= mid) update(u << 1, l, r, c); if (r > mid) update(u << 1 | 1, l, r, c); pushup(u); } int find(int y) { return lower_bound(ys.begin(), ys.end(), y) - ys.begin(); } int main() { cin >> n; for (int i = 0, j = 0; i < n; i ++ ) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); seg[j ++ ] = {x1, y1, y2, 1}, seg[j ++ ] = {x2, y1, y2, -1}; ys.push_back(y1), ys.push_back(y2); } sort(ys.begin(), ys.end()); ys.erase(unique(ys.begin(), ys.end()), ys.end()); build(1, 0, ys.size() - 2); sort(seg, seg + 2 * n); long long ans = 0; for (int i = 0; i < 2 * n; i ++ ) { if (i) ans += 1ll * tr[1].len * (seg[i].x - seg[i - 1].x); update(1, find(seg[i].y1), find(seg[i].y2) - 1, seg[i].mark); } printf("%lld\n", ans); return 0; }
题目:
平面上有n个矩形,给定d,需要找到一个位置(x,y)使得所有的(x + kd, y + kd)均不落在矩形中
分析:
扫描线
我们在使用扫描线的时候,以线段树维护y轴为标准,x是使用点坐标来计算宽的,而y是使用线段也就是区间来计算的。
所以,我们这里对x2-1,y2-1,但是在建立segment的时候,x2还要再加回来,但是,y2 不用加回来,y2-1正好就是线段的长度
而我们的整个正方形区域,按坐标来看,是从[0, 0]到[d - 1,d - 1]的,但是,我们的横坐标,是0~d - 1,但是纵坐标是用线段树维护的,长度是0到d-2
对于x2也要和y2一样都要先-1的另一个原因是,将x2从1~n映射到0~n-1,可以在取模的时候更精准。比如,x1 = 1, x2 = d, 在这种情况下,取模,就得到了x1 = 1, x2 = 0,很显然是不符合我们需要的,这种情况,我们要保证x1小于x2 的,所以,我们将值往前移动一格就完美的解决了这个问题了。而这个技巧也是很常见的。
比如将1~n的数字,映射到二维表格中,我们将原本的1~n先映射为0~n-1,再对每个数取模和下取整的时候,就是很符合规范的映射了,这里就不在赘述了。
代码:
#include <bits/stdc++.h> using namespace std; const int N = 100005; int n, d, ql, qr, mark; struct Node{ int tag, len; }tr[N << 2]; struct Segment{ int y1, y2, mark; }; vector<Segment> vt[N]; void pushup(int l, int r, int rt) { if (tr[rt].tag) tr[rt].len = r - l + 1; else if (l == r) tr[rt].len = 0; else tr[rt].len = tr[rt << 1].len + tr[rt << 1 | 1].len; } void add(int x1, int x2, int y1, int y2) { vt[x1].push_back((Segment){y1, y2, 1}); vt[x2 + 1].push_back((Segment){y1, y2, -1}); } void update(int l, int r, int rt) { if (ql <= l && r <= qr) { tr[rt].tag += mark; pushup(l, r, rt); return; } int mid = l + r >> 1; if (ql <= mid) update(l, mid, 2 * rt); if (qr > mid) update(mid + 1, r, 2 * rt + 1); pushup(l, r, rt); } void query(int l, int r, int rt) { if(tr[rt].len == 0) { printf("%d\n", l); return; } int mid = l + r >> 1; if (tr[2 * rt].len < mid - l + 1) query(l, mid, 2 * rt); else query(mid + 1, r, 2 * rt + 1); } int md(int x) { return (x % d + d) % d; } int main() { cin >> n >> d; for (int i = 1; i <= n; i ++ ) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); x2 --, y2 --; if (x2 - x1 + 1 >= d) x1 = 0, x2 = d - 1; if (y2 - y1 + 1 >= d) y1 = 0, y2 = d - 1; x1 = md(x1); y1 = md(y1); x2 = md(x2); y2 = md(y2); if (x1 <= x2) { if (y1 <= y2) add(x1, x2, y1, y2); else add(x1, x2, 0, y2), add(x1, x2, y1, d - 1); } else { if (y1 <= y2) add(0, x2, y1, y2), add(x1, d - 1, y1, y2); else add(0, x2, 0, y2), add(0, x2, y1, d - 1), add(x1, d - 1, 0, y2), add(x1, d - 1, y1, d- 1); } } for (int i = 0; i < d; i ++ ) { for (int j = 0; j < vt[i].size(); j ++ ) { ql = vt[i][j].y1; qr = vt[i][j].y2; mark = vt[i][j].mark; update(0, d - 1, 1); } if (tr[1].len < d) { puts("YES"); printf("%d ", i); query(0, d - 1, 1); return 0; } } puts("NO"); return 0; }
下面是逆十字的做法,我在懒标记那里还是有些不懂为什么要这么做的
#include <bits/stdc++.h> using namespace std; typedef pair<int, int> PII; const int N = 100005; int n, d; vector<PII> op0[N], op1[N]; int md(int x) { return (x % d + d) % d; } //tg为懒标记, int t[N * 4], tg[N * 4];//t表示当前区间是否有空白,由子节点更新而来,tg表示以u为根节点所再区间是不是全1 //u根节点,l和r表示节点u的左右端点,x和y表示修改区间,c表示求改为c void update(int u, int l, int r, int x, int y, int c) { //当前以u为跟节点的整个区间,都是有c的 if (l >= x && r <= y) { t[u] += c, tg[u] += c; return; } int mid = l + r >> 1; if (x <= mid) update(u << 1, l, mid, x, y, c); if (y > mid) update(u << 1 | 1, mid + 1, r, x, y, c); t[u] = min(t[u << 1], t[u << 1 | 1]) + tg[u]; //t[u] == 0表示左子树或者有子树有空白部分,而且u所在区间没有被标记 } //找到空白部分 int query(int u, int l, int r) { if (l == r) return l; int mid = l + r >> 1; if (tg[u] + t[u << 1] == t[u]) return query(u << 1, l, mid); return query(u << 1 | 1, mid + 1, r); } int main() { scanf("%d%d", &n, &d); for (int i = 1; i <= n; i ++ ) { int x1, y1, x2, y2; scanf("%d%d%d%d", &x1, &y1, &x2, &y2); vector<PII> range1, range2; x2 --, y2 --; if (x2 - x1 + 1 >= d) x1 = 0, x2 = d - 1; if (y2 - y1 + 1 >= d) y1 = 0, y2 = d - 1; x1 = md(x1), y1 = md(y1), x2 = md(x2), y2 = md(y2); if (x2 - x1 + 1 >= d) range1.push_back({0, d - 1}); else if (x2 >= x1) range1.push_back({x1, x2}); else { range1.push_back({x1, d - 1}); range1.push_back({0, x2}); } if (y2 - y1 + 1 >= d) range2.push_back({0, d - 1}); else if (y2 >= y1) range2.push_back({y1, y2}); else { range2.push_back({y1, d - 1}); range2.push_back({0, y2}); } for (auto j : range1) for (auto k : range2) { op0[j.first].push_back(k); op1[j.second + 1].push_back(k); } } for (int i = 0; i < d; i ++ ) { //将x=i这一列的所有信息都更新,然后判断这一行是否有空白 for (auto j : op0[i]) update(1, 0, d - 1, j.first, j.second, 1); for (auto j : op1[i]) update(1, 0, d - 1, j.first, j.second, -1); if (t[1] == 0) { puts("YES"); printf("%d %d\n", i, query(1, 0, d - 1)); return 0; } } puts("NO"); return 0; }