线段树合并经典例题(1)

最大出现次数的数字和

链接:CF600E Lomsat gelral - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

题意:

  • 有一棵 n 个结点的以 1 号结点为根的有根树
  • 每个结点都有一个颜色,颜色是以编号表示的, i 号结点的颜色编号为 c i c_i ci
  • 如果一种颜色在以 x 为根的子树内出现次数最多,称其在以 x 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
  • 你的任务是对于每一个 i ∈ [ 1 , n ] i∈[1,n] i[1,n],求出以 i 为根的子树中,占主导地位的颜色的编号和。
  • n ≤ 1 0 5 , c i ≤ n ≤ 1 0 5 n\le 10^5,c_i\le n≤10^5 n105,cin105

题解:对于任意一个点都要求出整个子树内最多出现次数的数字和,可以使用启发式式合并来解决这道题,当然也可以使用线段树合并来解决。

前置芝士

动态开点,权值线段树

线段树合并

线段树合并主要是将两棵动态开点的权值线段树进行合并。假设当前合并的线段树为左,右线段树,最后将右线段树合并到左线段树上。合并方式是左线段树或者右线段树为空树时,直接将两棵树的或值给左线段树即可。当两棵树均为非空时,说明都需要继续按左右子树递归合并,直到叶子节点时可以直接赋值结束。虽然看着很不靠谱,但复杂度是 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n)) 的。

实现

了解了线段树合并之后,容易发现对于一个父节点是可以将子节点的线段树直接合并上来的,那么可以先计算子节点再计算父节点。对于线段树上维护的,应该是最多出现次数,及其和。因为是权值线段树,则合并时底层各个权值的出现次数已经累加完,要计算的是最多出现次数及其和。当线段树上的左右子树最大出现次数不一样时,取大的那边,一样时则可以把数字和累加,最后每个节点的答案即为该点线段树头节点的数字和。

#pragma GCC optimize("Ofast")
#pragma GCC optimize("unroll-loops")
#include<iostream>
#include<algorithm>
#include<vector>
#include<cstring>
#include<functional>
#include<queue>
#include<unordered_map>
#include<map>
#include<set>

using namespace std;
using ll=long long;
using P=pair<int,int>;
const ll inf=1e18;

struct Merge{
	static constexpr int N=1e5+5;
	int n,now;
	vector<int>t,ls,rs,mx,va,rt;
	vector<ll>sm;
	Merge(int x=N):n(x+5<<5),t(n),ls(n),rs(n),mx(n),sm(n),va(n),rt(x+5),now(0){}

	void pushup(int k){
		int l=ls[k],r=rs[k];
		if(mx[l]>mx[r])
		{
			sm[k]=sm[l];
			mx[k]=mx[l];
			va[k]=va[l];
		}  
		else if(mx[l]<mx[r])
		{
			sm[k]=sm[r];
			mx[k]=mx[r];
			va[k]=va[r];
		}
		else
		{
			sm[k]=sm[l]+sm[r];
			mx[k]=mx[l];
			va[k]=va[l];
		}
	}

	void update(int&u,int l,int r,int pos){
		if(!u)u=++now;
		if(l==r)
		{
			sm[u]+=l;
			mx[u]++;
			va[u]=l;
			return;
		}
		int mid=l+r>>1;
		if(pos<=mid)update(ls[u],l,mid,pos);
		else update(rs[u],mid+1,r,pos);
		pushup(u);
	}

	void merge(int&u,int&v,int l,int r){
		if(!u||!v){u=u|v; return;}
		if(l==r)
		{
			va[u]=sm[u]=l;
			mx[u]+=mx[v];
			return;
		}
		int mid=l+r>>1;
		merge(ls[u],ls[v],l,mid);
		merge(rs[u],rs[v],mid+1,r);
		pushup(u);
	}

};

void solve()
{
	int n; cin>>n;
	vector<int>a(n+1);
	vector<vector<int>>ed(n+1);
	for(int i=1;i<=n;i++)cin>>a[i];
	for(int i=1,u,v;i<n;i++)
	{
		cin>>u>>v;
		ed[u].push_back(v);
		ed[v].push_back(u);
	}

	Merge tr(n);
	vector<ll>ans(n+1);

	auto dfs=[&](auto dfs,int x,int fa)->void{
		tr.update(tr.rt[x],1,n,a[x]);
		for(auto y:ed[x])
		{
			if(y==fa)continue;
			dfs(dfs,y,x);
			tr.merge(tr.rt[x],tr.rt[y],1,n);
		}
		ans[x]=tr.sm[tr.rt[x]];
	};

	dfs(dfs,1,0);
	for(int i=1;i<=n;i++)
	{
		cout<<ans[i]<<" \n"[i==n];
	}

}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0); cout.tie(0);
	int t=1; //cin>>t;
	while(t--)solve();
	return 0;
} 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
线段(Segment Tree)是一种数据结构,用于高效地支持区间查询和更新操作。在C语言中,线段通常用来处理数组或区间问题,它将原始的区间操作抽象为状结构,使得查询和修改操作的时间复杂度可以降低到O(logn)。 合并线段的操作一般涉及到两个子合并,比如在一个区间合并中,可能需要合并左子和右子,然后更新根节点的值。这通常涉及到递归,首先处理子节点,然后将结果合并。 以下是一个简单的线段合并操作的C语言代码示例,这里假设线段是二维数组,每个元素代表一个区间的值: ```c #include <stdio.h> #define MAXN 100000 // 假设最大节点数 int tree[MAXN << 2]; // 用一个数组存储整个线段 // 合并两个区间 [l, r] 和 [m, n] void merge(int node, int l, int r, int m, int n, int value) { if (l > n || r < m) return; // 如果区间不重叠,直接返回 if (l >= m && r <= n) { // 区间完全包含于[l, r] tree[node] = value; return; } int mid = (l + r) / 2; merge(node * 2, l, mid, m, min(n, mid), value); // 更新左子 merge(node * 2 + 1, mid + 1, r, max(m, mid + 1), n, value); // 更新右子 tree[node] = tree[node * 2] + tree[node * 2 + 1]; // 合并 } // 使用示例 void update(int node, int l, int r, int pos, int val) { merge(node, l, r, pos, pos, val); } // 查询区间[l, r]的和 int query(int node, int l, int r, int L, int R) { if (L <= l && r <= R) return tree[node]; int mid = (l + r) / 2, sum = 0; if (L <= mid) sum += query(node * 2, l, mid, L, R); if (R > mid) sum += query(node * 2 + 1, mid + 1, r, L, R); return sum; } int main() { // 初始化或填充线段后,再使用update和query方法进行操作 // 例如: // update(1, 0, N - 1, 3, 5); // 将索引为3的元素值更新为5 // int sum = query(1, 0, N - 1, 1, 4); // 计算区间[1, 4]的和 return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值