「SDOI2011」 染色 - 树链剖分

本文介绍了一种结合树剖和线段树的数据结构算法,用于解决给定树上的动态操作问题,包括染色和查询路径上颜色段数量。通过两次DFS将树剖成链,然后使用线段树进行高效维护和查询,适用于树形结构的静态操作场景。

题目描述

给定一棵有n个节点的无根树和m个操作,操作有2类:

  1. 将节点a到节点b路径上所有点都染成颜色c;
  2. 询问节点a到节点b路径上的颜色段数量(连续相同颜色被认为是同一段),如“112221”由3段组成:“11”、“222”和“1”。

请你写一个程序依次完成这m个操作。

输入格式

第一行包含2个整数n和m,分别表示节点数和操作数;

第二行包含n个正整数表示n个节点的初始颜色

下面 行每行包含两个整数x和y,表示x和y之间有一条无向边。

下面 行每行描述一个操作:

C a b c表示这是一个染色操作,把节点a到节点b路径上所有点(包括a和b)都染成颜色c;

Q a b表示这是一个询问操作,询问节点a到节点b(包括a和b)路径上的颜色段数量。

输出格式

对于每个询问操作,输出一行答案。

数据范围

数据范围

分析

树上链的操作且为静态的(这里指树的形态为静态的),一般用树剖来做。首先两次Dfs将树剖成链,再用线段树维护。维护过程中记录三个值,lc,rc,sumlc,rc,sumlc,rc,sum,分别为区间左边的颜色,区间右边的颜色,区间内颜色段的总数,PushupPushupPushup时该区间左边颜色就等于左儿子的左边,右边等于右儿子的右边,总数等于左右总数之和,但若左儿子的右边等于右儿子的左边,就会重复计数,所以要减1。

在跑树中链时,用两个变量记录上一次剖得的链的左边颜色,用两个是因为有x和y,两边同时往上跳的点。之后累加完答案后,若当前区间的右边颜色等于上次剖得的链的左边颜色,则重复计数,要减1。到最后在同一条重链时,同理判断。修改时就用懒标记,需要用到时在PushdownPushdownPushdown

要注意以下,在两个点往上跳交换两个点,使top深度最大的点更新时,要将两个变量也交换。

当然,此题可用LCT过。一般来说,凡是树剖能过A的题,LCT都可以A。

代码

#include <iostream>
#include <cstdio>
using namespace std;
const int N=100002;
struct Edge {
	int to,nxt;
}e[N<<1];
int h[N],cnt,temp[N];
int n,m,v[N],tag[N<<2],pre[N];
int sum[N<<2],lc[N<<2],rc[N<<2];
int size[N],son[N],prt[N];
int seg[N],top[N],dep[N];
void Add(int x,int y) {
	e[++cnt]=(Edge){y,h[x]};
	h[x]=cnt;
}
void Dfs1(int x,int fa) {
	prt[x]=fa;
	dep[x]=dep[fa]+1;
	size[x]=1;
	for (int i=h[x];i;i=e[i].nxt) {
		int y=e[i].to;
		if (y==fa) continue;
		Dfs1(y,x);
		size[x]+=size[y];
		if (size[son[x]]<size[y]) son[x]=y;
	}
}
void Dfs2(int x,int tp) {//以上两次Dfs将树剖成链 
	seg[x]=++seg[0];
	top[x]=tp;
	pre[seg[0]]=x;
	if (son[x]) Dfs2(son[x],tp);
	for (int i=h[x];i;i=e[i].nxt) {
		int y=e[i].to;
		if (top[y]) continue;
		Dfs2(y,y);
	}
}
void Pushup(int p) {//上传更新 
	sum[p]=sum[p<<1]+sum[p<<1|1];
	lc[p]=lc[p<<1];
	rc[p]=rc[p<<1|1];
	if (rc[p<<1]==lc[p<<1|1]) sum[p]--;
}
void Pushdown(int p) {//下传标记 
	if (!tag[p]) return;
	sum[p<<1]=sum[p<<1|1]=1;
	tag[p<<1]=tag[p<<1|1]=tag[p];
	lc[p<<1]=rc[p<<1]=tag[p];
	lc[p<<1|1]=rc[p<<1|1]=tag[p];
	tag[p]=0;
}
void Build(int p,int l,int r) {//建树 
	if (l==r) {
		sum[p]=1;
		lc[p]=rc[p]=v[pre[l]];
		return;
	}
	int mid=(l+r)>>1;
	Build(p<<1,l,mid);
	Build(p<<1|1,mid+1,r);
	Pushup(p);
}
int Query(int p,int l,int r,int L,int R,int &ll,int &rr) {
//p,l,r,L,R为正常线段树所传参数,ll为该区间内最左边的颜色,rr为最右边的颜色 
	if (L<=l&&r<=R) {
		ll=lc[p];
		rr=rc[p];
		return sum[p];
	}
	Pushdown(p);
	int mid=(l+r)>>1;
	if (R<=mid) return Query(p<<1,l,mid,L,R,ll,rr);//全左 
	else if (L>mid) return Query(p<<1|1,mid+1,r,L,R,ll,rr);//全右 
	else {//交错 
		int llp,lrp,rlp,rrp;//左左,左右,右左,右右
		int sum=Query(p<<1,l,mid,L,R,llp,lrp)+Query(p<<1|1,mid+1,r,L,R,rlp,rrp);
		if (lrp==rlp) sum--;
		ll=llp;
		rr=rrp;
		return sum;
	}
}
void Change(int p,int l,int r,int L,int R,int x) {
	if (L<=l&&r<=R) {
		sum[p]=1;
		lc[p]=rc[p]=x;
		tag[p]=x;
		return;
	}
	Pushdown(p);
	int mid=(l+r)>>1;
	if (L<=mid) Change(p<<1,l,mid,L,R,x);
	if (R>mid) Change(p<<1|1,mid+1,r,L,R,x);
	Pushup(p);
}
int Ask(int x,int y) {
	int xx=0,yy=0,ans=0,ll,rr,t;
	while (top[x]!=top[y]) {
		if (dep[top[x]]<dep[top[y]]) {
			swap(x,y);
			swap(xx,yy);
		}
		t=Query(1,1,seg[0],seg[top[x]],seg[x],ll,rr);
		ans+=t;
		if (rr==xx) ans--;//重复,减1 
		xx=ll;
		x=prt[top[x]];
	}
	if (dep[x]>dep[y]) {
		swap(x,y);
		swap(xx,yy);
	}
	t=Query(1,1,seg[0],seg[x],seg[y],ll,rr);
	ans+=t;
	if (xx==ll) ans--;//重复 
	if (yy==rr) ans--;
	return ans;
}
void Updata(int x,int y,int c) {//正常操作 
	while (top[x]!=top[y]) {
		if (dep[top[x]]<dep[top[y]]) swap(x,y);
		Change(1,1,seg[0],seg[top[x]],seg[x],c);
		x=prt[top[x]];
	}
	if (dep[x]>dep[y]) swap(x,y);
	Change(1,1,seg[0],seg[x],seg[y],c);
}
int main() {
	scanf("%d%d",&n,&m);
	for (int i=1;i<=n;i++)
		scanf("%d",&v[i]);
	for (int i=1;i<n;i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		Add(u,v);
		Add(v,u);
	}
	Dfs1(1,1);
	Dfs2(1,1);
	Build(1,1,seg[0]);
	for (int i=1;i<=m;i++) {
		char op[10];
		int x,y,z;
		scanf("%s%d%d",op,&x,&y);
		if (op[0]=='C') {
			scanf("%d",&z);
			Updata(x,y,z);
		} else {
			printf("%d\n",Ask(x,y));
		}
	}
	return 0;
}
### 树链剖分中的染色算法实现 树链剖分是一种高效的形结构优化方法,常用于处理路径和子上的复杂查询与修改操作。对于涉及染色的操作,通常可以通过树链剖分配合线段树或其他支持区间更新的数据结构来完成。 #### 基本原理 树链剖分的核心思想是将分解为多条重链,并通过 `dfn` 编号将其映射到一维数组上[^1]。这样可以方便地使用线段树等数据结构维护这些重链的属性。具体来说: - **重儿子**:每个节点的儿子中,子大小最大的称为该节点的重儿子。 - **重链**:从某个节点出发沿着重儿子一路向下形成的链条称为重链。 - **轻边**:连接当前节点与其非重儿子的边称为轻边。 通过对进行上述划分后,任意两点间的路径都可以被划分为不超过 \( O(\log n) \) 条重链和轻边[^2]。 #### 染色问题的具体实现 针对题目描述中的染色问题,我们需要设计一种能够高效执行以下两类操作的方法: 1. 修改路径上的所有点的颜色。 2. 查询某路径上的颜色段数。 以下是具体的解决方案及其代码实现。 --- ##### 数据结构的选择 为了快速响应路径上的批量修改以及统计颜色段的数量,可以选择如下组合: - 使用 **线段树** 维护每条重链上的信息。 - 对于颜色段计数问题,可以在每个线段树节点存储额外的状态变量,比如左端点颜色、右端点颜色、总颜色段数目等。 --- ##### 关键状态定义 假设我们已经完成了树链剖分并构建好了对应的线段树,则需要在线段树节点中记录以下几个字段: - `sum`: 当前区间的颜色段总数。 - `left_color`: 当前区间的左端点颜色。 - `right_color`: 当前区间的右端点颜色。 - `lazy_tag`: 表示是否有延迟标记(即整个区间是否已被统一染成某种颜色)。 当存在懒惰标记时,表示整段已经被覆盖为同种颜色,此时可以直接忽略其内部细节而仅保留两端的颜色信息。 --- ##### 更新逻辑 在执行路径染色的过程中,需要注意以下几点: 1. 如果目标路径跨越多个重链,则需分别对各部分单独处理; 2. 若遇到带有懒惰标记的节点,在下推之前应先清除原有标签的影响; 3. 合并两个相邻片段的结果时,要特别注意它们之间是否存在边界效应——即两者的末端颜色是否一致会影响最终计算得到的颜色段数量。 下面给出完整的伪代码实现: ```python class SegmentTreeNode: def __init__(self, l=0, r=0): self.l = l # 左边界 self.r = r # 右边界 self.sum = 0 # 颜色段数 self.left_color = None # 左端点颜色 self.right_color = None # 右端点颜色 self.lazy_tag = -1 # 懒惰标记 (-1 表示无) def push_up(node): """合并左右孩子信息""" if node.left_child.right_color == node.right_child.left_color: node.sum = node.left_child.sum + node.right_child.sum - 1 else: node.sum = node.left_child.sum + node.right_child.sum node.left_color = node.left_child.left_color node.right_color = node.right_child.right_color def build_tree(tree, idx, l, r): tree[idx].l = l tree[idx].r = r if l == r: # 初始化叶子结点 tree[idx].left_color = colors[l] tree[idx].right_color = colors[l] tree[idx].sum = 1 return mid = (l + r) >> 1 build_tree(tree, idx * 2, l, mid) build_tree(tree, idx * 2 + 1, mid + 1, r) push_up(tree[idx]) def update_range(tree, idx, L, R, color): """区间 [L,R] 的全部元素改为指定颜色 'color' """ if tree[idx].l >= L and tree[idx].r <= R: # 完全包含的情况 tree[idx].left_color = tree[idx].right_color = color tree[idx].sum = 1 tree[idx].lazy_tag = color return if tree[idx].lazy_tag != -1: # 下传懒惰标记 propagate_lazy(tree, idx) mid = (tree[idx].l + tree[idx].r) >> 1 if L <= mid: update_range(tree, idx * 2, L, R, color) if R > mid: update_range(tree, idx * 2 + 1, L, R, color) push_up(tree[idx]) # 自底向上重新计算父节点信息 def query_colors(tree, idx, L, R): """查询区间 [L,R] 上的颜色段数""" if tree[idx].l >= L and tree[idx].r <= R: return tree[idx].sum if tree[idx].lazy_tag != -1: # 下传懒惰标记 propagate_lazy(tree, idx) res = 0 mid = (tree[idx].l + tree[idx].r) >> 1 if L <= mid: res += query_colors(tree, idx * 2, L, R) if R > mid: res += query_colors(tree, idx * 2 + 1, L, R) return res def propagate_lazy(tree, idx): """传播懒惰标记至子节点""" lazy_val = tree[idx].lazy_tag if lazy_val != -1: tree[idx * 2].left_color = tree[idx * 2].right_color = lazy_val tree[idx * 2].sum = 1 tree[idx * 2].lazy_tag = lazy_val tree[idx * 2 + 1].left_color = tree[idx * 2 + 1].right_color = lazy_val tree[idx * 2 + 1].sum = 1 tree[idx * 2 + 1].lazy_tag = lazy_val tree[idx].lazy_tag = -1 ``` 以上代码展示了如何基于线段树实现在树链剖分框架下的路径染色功能[^4]。 --- #### 总结 通过结合树链剖分线段树技术,我们可以优雅地解决诸如路径染色等问题。这种方法不仅具有较高的时间效率 (\(O(\log^2 n)\)) ,而且易于扩展以适应更多复杂的场景需求。 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值