Lomsat gelral(dsu on tree模板理解)

https://www.luogu.com.cn/problem/CF600E


题意翻译

  • 有一棵 nn 个结点的以 11 号结点为根的有根树
  • 每个结点都有一个颜色,颜色是以编号表示的, ii 号结点的颜色编号为 c_ici​。
  • 如果一种颜色在以 xx 为根的子树内出现次数最多,称其在以 xx 为根的子树中占主导地位。显然,同一子树中可能有多种颜色占主导地位。
  • 你的任务是对于每一个 i\in[1,n]i∈[1,n],求出以 ii 为根的子树中,占主导地位的颜色的编号和。
  • n\le 10^5,c_i\le nn≤105,ci​≤
    思路:最暴力直接的想法也就是O(n^2)的dfs暴力。先考虑对于一个节点,怎么算他的子树的编号和答案。对其dfs先序遍历下去(将这个dfs记为cal函数)。那么对于一颗树全部的节点,在dfs中从上面枚举点进行cal也好,从下面枚举点进行cal也好,都可以。如果想优化每次进行记录的节点信息,可以开个一维数组,跑完cal后记录,然后清空再跑。然而二维数组不用清空,但是开出来是1e5*1e5是MLE的。

那么怎么优化呢?刚才提到dfs中枚举点可以从上面开始cal,也可以从下面开始cal,那么dsu on tree从最底下开始cal达到优化成O(nlogn)的目的。

以图来表示这个dsu on tree 的优化过程。

 

 其中标红色的是重儿子。

模拟一下算法吧

开始先通过dfs找到最底层的儿子,所以开始找到3,然后3没有重儿子,此时对3进行cal,那么此时ans[3]的答案就已经统计出来了。这个时候我们删除在cal(3)过程中保存的子树信息(因为二维空间不够,一维不清空会对其他节点的统计造成影响)

然后这时候2还有重儿子,我们进入4,cal(4),获得了ans[4]的答案,这个时候我们通过dfs的标记keep不删除这个4子树的贡献。

然后这时候我们dfs递归栈退到了2节点。此时cal(2),我们通过flag标记,cal(2)的时候只再去暴力统计一遍3子树,而之前不删除的贡献就是4子树的贡献,直接加上去。这样子ans[2]的答案就统计完了。然后看到2节点并不是重儿子,所以还是要暴力清空。

那么对5的操作和2是一样的。最后到8,暴力cal(8),不清空,然后dfs栈回到1,这时候1再去暴力跑cal(2)和cal(5),加上之前没有删除的8节点的子树。

时间复杂度O(nlogn).证明百度吧(和树链剖分有关)

#include<iostream>
#include<vector>
#include<queue>
#include<cstring>
#include<cmath>
#include<map>
#include<set>
#include<cstdio>
#include<algorithm>
#define debug(a) cout<<#a<<"="<<a<<endl;
using namespace std;
const int maxn=1e5+100;
typedef long long LL;
LL col[maxn],sum,maxc,cnt[maxn];
LL siz[maxn],son[maxn];///度数,重儿子 
LL ans[maxn];
LL flag=-1;
vector<LL>g[maxn];
void predfs(LL u,LL fa)
{
	siz[u]=1;
	for(LL i=0;i<g[u].size();i++){
		LL v=g[u][i];
		if(v==fa) continue;
		predfs(v,u);
		siz[u]+=siz[v];
		if(siz[v]>siz[son[u]]){
			son[u]=v;
		}
	}	
}
void cal(LL u,LL fa,LL val)
{
	cnt[col[u]]+=val;//val为正为负可以控制是增加贡献还是删除贡献
	if(cnt[col[u]]>maxc){
		maxc=cnt[col[u]];
		sum=col[u];
	}
	else if(cnt[col[u]]==maxc){
		sum+=col[u];
	}
	for(LL i=0;i<g[u].size();i++){
		LL v=g[u][i];
		if(v==fa||v==flag) continue; //不能写if(v==f||v==son[u])
		cal(v,u,val);
	}
}
void dfs(LL u,LL fa,bool keep)
{
	//* 第一步:搞轻儿子及其子树算其答案删贡献
	for(LL i=0;i<g[u].size();i++){
		LL v=g[u][i];
		if(v==fa||v==son[u]) continue;
		dfs(v,u,0);
	}
	//* 第二步:搞重儿子及其子树算其答案不删贡献
	if(son[u]){
		dfs(son[u],u,1);
		flag=son[u];
	}
	//* 第三步:暴力统计u及其所有轻儿子的贡献合并到刚算出的重儿子信息里
	cal(u,fa,1);
	flag=0;///别忘了 
	//* 把需要删除贡献的删一删
	ans[u]=sum;
	if(keep==0){
		cal(u,fa,-1);
		sum=0;//这是因为count函数中会改变这两个变量值
		maxc=0;
	}
}
int main(void)
{
  cin.tie(0);std::ios::sync_with_stdio(false);
  LL n;cin>>n;
  for(LL i=1;i<=n;i++){
  	cin>>col[i];
  }
  for(LL i=1;i<n;i++){
  	LL u,v;cin>>u>>v;
  	g[u].push_back(v);
  	g[v].push_back(u);
  }
  predfs(1,-1);
  dfs(1,-1,0);
  for(LL i=1;i<=n;i++){
  	cout<<ans[i]<<" ";
  }
  cout<<endl;
return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值