[题解]LuoGu4092:[HEOI2016/TJOI2016]树

本文探讨了树剖和线段树在处理特定类型问题上的应用,通过实例讲解了如何利用这两种数据结构来求解关于树的最值查询问题。文章详细介绍了树剖和线段树的构建过程,以及如何通过它们高效地更新和查询树上点的属性。

原题传送门
又见树剖裸题!
想想树剖的性质,询问一个点,往上跳,跳的一段区间一定是连续的,只要在这段区间中找出最大值就是最近的点
所以只要开一个维护最大值的线段树就行啦
另外需要注意根节点初始有标记

Code:

#include <bits/stdc++.h>
#define maxn 100010
#define ls rt << 1
#define rs rt << 1 | 1
using namespace std;
struct Edge{
	int to, next;
}edge[maxn << 1];
struct Seg{
	int l, r, sum;
}seg[maxn << 2];
int head[maxn], num, d[maxn], fa[maxn], size[maxn], son[maxn], id[maxn], cnt, top[maxn], rk[maxn], n, m;

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

void add_edge(int x, int y){ edge[++num].to = y; edge[num].next = head[x]; head[x] = num; }

void dfs(int u){
	size[u] = 1, son[u] = -1;
	for (int i = head[u]; i; i = edge[i].next){
		int v = edge[i].to;
		if (v != fa[u]){
			fa[v] = u;
			dfs(v);
			size[u] += size[v];
			if (son[u] == -1 || son[u] != -1 && size[son[u]] < size[v]) son[u] = v;
		}
	}
}

void dfs(int u, int x){
	id[u] = ++cnt, top[u] = x, rk[cnt] = u;
	if (son[u] == -1) return;
	dfs(son[u], x);
	for (int i = head[u]; i; i = edge[i].next){
		int v = edge[i].to;
		if (v != fa[u] && v != son[u]) dfs(v, v);
	}
}

void pushup(int rt){ seg[rt].sum = max(seg[ls].sum, seg[rs].sum); }

void build(int rt, int l, int r){
	seg[rt].l = l, seg[rt].r = r;
	if (l == r){
		if (l == 1) seg[rt].sum = 1;
		return;
	}
	int mid = (l + r) >> 1;
	build(ls, l, mid); build(rs, mid + 1, r);
	pushup(rt);
}

void update(int rt, int x){
	if (seg[rt].l == seg[rt].r){
		seg[rt].sum = x; return;
	}
	if (seg[ls].r >= x) update(ls, x); else update(rs, x);
	pushup(rt);
}

int query(int rt, int l, int r){
	if (seg[rt].l > r || seg[rt].r < l) return 0;
	if (seg[rt].l >= l && seg[rt].r <= r) return seg[rt].sum;
	return max(query(ls, l, r), query(rs, l, r));
}

int main(){
	n = read(), m = read();
	for (int i = 1; i < n; ++i){
		int x = read(), y = read();
		add_edge(x, y); add_edge(y, x);
	}
	dfs(1); dfs(1, 1); build(1, 1, n);
	while (m--){
		char c = getchar(); for (; c != 'Q' && c != 'C'; c = getchar()); int x = read();
		if (c == 'C') update(1, id[x]); else{
			int ans = 0;
			while (top[x] != 1){
				int s = query(1, id[top[x]], id[x]);
				if (s) ans = s;
				if (ans) break;
				x = fa[top[x]];
			}
			if (!ans) ans = query(1, id[top[x]], id[x]);
			printf("%d\n", rk[ans]);
		}
	}
	return 0;
}

当然这道题也可以用暴力线段树解决
Code:

#include <bits/stdc++.h>
#define maxn 100010
#define ls rt << 1
#define rs rt << 1 | 1
using namespace std;
struct Edge{
	int to, next;
}edge[maxn << 1];
struct Seg{
	int l, r, d, node;
}seg[maxn << 2];
int num, head[maxn], d[maxn], size[maxn], id[maxn], Index, n, m;

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

void addedge(int x, int y){ edge[++num] = (Edge){y, head[x]}, head[x] = num; }

void dfs(int u, int pre){
	d[u] = d[pre] + 1, size[u] = 1, id[u] = ++Index;
	for (int i = head[u]; i; i = edge[i].next){
		int v = edge[i].to;
		if (v != pre) dfs(v, u), size[u] += size[v];
	}
}

void pushdown(int rt){
	if (seg[ls].d < seg[rt].d) seg[ls].d = seg[rt].d, seg[ls].node = seg[rt].node;
	if (seg[rs].d < seg[rt].d) seg[rs].d = seg[rt].d, seg[rs].node = seg[rt].node;
}

void build(int rt, int l, int r){
	seg[rt].l = l, seg[rt].r = r;
	seg[rt].d = 1, seg[rt].node = 1;
	if (l == r) return;
	int mid = (l + r) >> 1;
	build(ls, l, mid), build(rs, mid + 1, r);
}

void update(int rt, int l, int r, int x){
	if (seg[rt].l > r || seg[rt].r < l) return;
	if (seg[rt].l >= l && seg[rt].r <= r){
		if (d[x] > seg[rt].d) seg[rt].d = d[x], seg[rt].node = x;
		return;
	}
	pushdown(rt);
	update(ls, l, r, x), update(rs, l, r, x);
}

int query(int rt, int pos){
	if (seg[rt].l == seg[rt].r) return seg[rt].node;
	pushdown(rt);
	if (pos <= seg[ls].r) return query(ls, pos); else return query(rs, pos);
}

int main(){
	n = read(), m = read();
	for (int i = 1; i < n; ++i){
		int x = read(), y = read();
		addedge(x, y), addedge(y, x);
	}
	dfs(1, 0);
	build(1, 1, n);
	while (m--){
		char opt = getchar();
		for (; opt != 'C' && opt != 'Q'; opt = getchar());
		int x = read();
		if (opt == 'C') update(1, id[x], id[x] + size[x] - 1, x); else printf("%d\n", query(1, id[x]));
	}
	return 0;
}
<think>题目:P2824 [HEOI2016/TJOI2016] 排序 题目大意:给出一个1到n的全排列,有m个操作,每个操作将区间[l, r]升序或降序排序。最后询问第q个位置上的数字。 常规思路:直接模拟排序操作会超时,因为m和n最大为10^5。 高效解法:二分答案 + 线段(或类似数据结构)维护01序列。 思路: 1. 注意到最后只询问一个位置的值,考虑二分答案ans。 2. 假设当前二分的值为mid,将原数组中大于等于mid的值设为1,小于mid的值设为0。 3. 这样,排序操作就变成了将区间内的0和1进行排序: - 升序排序:相当于将0全部放到前面,1全部放到后面。 - 降序排序:相当于将1全部放到前面,0全部放到后面。 4. 用线段维护01序列,支持区间赋值和区间求和(即区间内1的个数)。 5. 对于每个操作[l, r]: - 先查询该区间内1的个数(记为cnt)。 - 如果是升序排序,则将区间[l, r-cnt]赋值为0,将区间[r-cnt+1, r]赋值为1。 - 如果是降序排序,则将区间[l, l+cnt-1]赋值为1,将区间[l+cnt, r]赋值为0。 6. 操作完成后,检查位置q的值是否为1(注意:由于我们二分的是答案,如果位置q为1,说明答案至少为mid,否则小于mid)。 7. 不断二分,直到找到最大的mid,使得最后位置q的值为1,这个mid就是答案。 代码实现步骤: - 二分框架:二分答案,范围是1到n。 - 构建线段:支持区间赋值和区间求和。 - 对每个操作进行模拟(区间赋值操作)。 - 最后根据位置q的值调整二分边界。 注意:线段需要支持区间覆盖(set)操作,并且要注意懒标记的处理。 代码示例: ```cpp #include <iostream> #include <cstdio> #include <algorithm> #include <cstring> using namespace std; const int MAXN = 100005; struct Node { int sum; // 区间内1的个数 int set; // 懒标记:-1表示无操作,0表示置0,1表示置1 } tree[MAXN << 2]; struct Operation { int op, l, r; } op[MAXN]; int n, m, q; int a[MAXN]; // 原数组 int b[MAXN]; // 用于01序列的数组 void pushUp(int rt) { tree[rt].sum = tree[rt<<1].sum + tree[rt<<1|1].sum; } void pushDown(int rt, int l, int r) { if (tree[rt].set != -1) { int mid = (l + r) >> 1; // 下传左子 tree[rt<<1].set = tree[rt].set; tree[rt<<1].sum = (mid - l + 1) * tree[rt].set; // 下传右子 tree[rt<<1|1].set = tree[rt].set; tree[rt<<1|1].sum = (r - mid) * tree[rt].set; // 清除标记 tree[rt].set = -1; } } void build(int rt, int l, int r) { tree[rt].set = -1; if (l == r) { tree[rt].sum = b[l]; return; } int mid = (l + r) >> 1; build(rt<<1, l, mid); build(rt<<1|1, mid+1, r); pushUp(rt); } void update(int rt, int l, int r, int L, int R, int val) { if (L <= l && r <= R) { tree[rt].set = val; tree[rt].sum = (r - l + 1) * val; return; } pushDown(rt, l, r); int mid = (l + r) >> 1; if (L <= mid) update(rt<<1, l, mid, L, R, val); if (R > mid) update(rt<<1|1, mid+1, r, L, R, val); pushUp(rt); } int query(int rt, int l, int r, int L, int R) { if (L <= l && r <= R) { return tree[rt].sum; } pushDown(rt, l, r); int mid = (l + r) >> 1; int res = 0; if (L <= mid) res += query(rt<<1, l, mid, L, R); if (R > mid) res += query(rt<<1|1, mid+1, r, L, R); return res; } // 检查二分值mid:构建01序列,然后模拟所有操作 bool check(int mid) { // 构建01序列:>=mid为1,否则为0 for (int i = 1; i <= n; i++) { b[i] = (a[i] >= mid); } // 构建线段 build(1, 1, n); // 模拟m个操作 for (int i = 0; i < m; i++) { int l = op[i].l, r = op[i].r; int cnt = query(1, 1, n, l, r); // 查询区间内1的个数 if (op[i].op == 0) { // 升序:0在前,1在后 if (cnt > 0) update(1, 1, n, r - cnt + 1, r, 1); // 后cnt个置1 if (r - l + 1 - cnt > 0) update(1, 1, n, l, r - cnt, 0); // 前面的置0 } else { // 降序:1在前,0在后 if (cnt > 0) update(1, 1, n, l, l + cnt - 1, 1); // 前cnt个置1 if (r - l + 1 - cnt > 0) update(1, 1, n, l + cnt, r, 0); // 后面的置0 } } // 查询位置q的值 return query(1, 1, n, q, q) == 1; } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); } for (int i = 0; i < m; i++) { scanf("%d%d%d", &op[i].op, &op[i].l, &op[i].r); } scanf("%d", &q); // 二分答案 int L = 1, R = n, ans = 0; while (L <= R) { int mid = (L + R) >> 1; if (check(mid)) { ans = mid; L = mid + 1; } else { R = mid - 1; } } printf("%d\n", ans); return 0; } ``` §§相关问题§§ 1. 为什么二分答案的正确性成立?如何保证最终答案的正确性? 2. 线段在本题中维护的信息是什么?懒标记的作用是什么? 3. 如果题目要求最后多个位置的值,如何修改算法? 4. 如何处理排序操作中的区间覆盖与线段懒标记的冲突? 5. 是否有其他数据结构(如平衡)可以替代线段解决此题?</think>### P2824 "排序" C++ 题解 **题目大意**:给定一个 1~n 的排列,进行 m 次区间排序操作(升序或降序),最后询问第 q 个位置的值。 **核心算法**:二分答案 + 线段维护 01 序列 **解题思路**: 1. **二分答案**:最终答案范围 [1, n],二分检查 mid 是否可能为答案 2. **01 序列转换**:将原数组转为 01 序列(≥mid 为 1,<mid 为 0) 3. **线段操作**: - 排序操作转化为区间赋值(升序:0 在前 1 在后;降序:1 在前 0 在后) - 通过区间求和快速计算 1 的数量 4. **验证答案**:检查位置 q 在最终序列中是否为 1 **代码实现**: ```cpp #include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int MAXN = 1e5 + 5; struct Node { int sum; // 区间内 1 的数量 int set; // 懒标记:-1 无操作,0 置 0,1 置 1 } tree[MAXN << 2]; struct Operation { int op, l, r; // op=0 升序,op=1 降序 } ops[MAXN]; int n, m, q, pos; int a[MAXN], b[MAXN]; // 线段维护 void pushUp(int rt) { tree[rt].sum = tree[rt << 1].sum + tree[rt << 1 | 1].sum; } void pushDown(int rt, int len) { if (tree[rt].set != -1) { int setVal = tree[rt].set; tree[rt << 1].set = setVal; tree[rt << 1 | 1].set = setVal; tree[rt << 1].sum = setVal * (len - (len >> 1)); tree[rt << 1 | 1].sum = setVal * (len >> 1); tree[rt].set = -1; } } void build(int rt, int l, int r) { tree[rt].set = -1; if (l == r) { tree[rt].sum = b[l]; return; } int mid = (l + r) >> 1; build(rt << 1, l, mid); build(rt << 1 | 1, mid + 1, r); pushUp(rt); } void update(int rt, int L, int R, int setVal, int l, int r) { if (L <= l && r <= R) { tree[rt].set = setVal; tree[rt].sum = setVal * (r - l + 1); return; } pushDown(rt, r - l + 1); int mid = (l + r) >> 1; if (L <= mid) update(rt << 1, L, R, setVal, l, mid); if (R > mid) update(rt << 1 | 1, L, R, setVal, mid + 1, r); pushUp(rt); } int query(int rt, int L, int R, int l, int r) { if (L <= l && r <= R) return tree[rt].sum; pushDown(rt, r - l + 1); int mid = (l + r) >> 1, res = 0; if (L <= mid) res += query(rt << 1, L, R, l, mid); if (R > mid) res += query(rt << 1 | 1, L, R, mid + 1, r); return res; } bool check(int mid) { // 构建 01 序列 for (int i = 1; i <= n; i++) b[i] = (a[i] >= mid); build(1, 1, n); // 执行所有排序操作 for (int i = 0; i < m; i++) { int l = ops[i].l, r = ops[i].r; int cnt = query(1, l, r, 1, n); // 查询 1 的数量 if (ops[i].op == 0) { // 升序排序 if (r - cnt + 1 <= r) update(1, r - cnt + 1, r, 1, 1, n); if (l <= r - cnt) update(1, l, r - cnt, 0, 1, n); } else { // 降序排序 if (l <= l + cnt - 1) update(1, l, l + cnt - 1, 1, 1, n); if (l + cnt <= r) update(1, l + cnt, r, 0, 1, n); } } return query(1, q, q, 1, n) == 1; // 检查目标位置 } int main() { scanf("%d%d", &n, &m); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); for (int i = 0; i < m; i++) scanf("%d%d%d", &ops[i].op, &ops[i].l, &ops[i].r); scanf("%d", &q); // 二分答案 int L = 1, R = n, ans; while (L <= R) { int mid = (L + R) >> 1; if (check(mid)) { ans = mid; L = mid + 1; } else { R = mid - 1; } } printf("%d\n", ans); return 0; } ``` **算法优势**: - **时间复杂度**:O(m log² n) - **空间复杂度**:O(n) - **核心技巧**:将排序操作转化为 01 序列的区间赋值,避免直接排序的高复杂度
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值