(树上启发式合并)dsu on tree 学习报告总结

树上启发式合并:

简介:它是用来解决一类树上询问问题,一般这种问题有两个特征

1、只有对子树的询问

2、没有修改

一般这时候就可以强上dsu on tree了

update:可能特征1不会很显然,就是说题目中不一定明确的问你子树i的答案,可能是把问题转化后需要算子树的答案

妈妈再也不用担心我不会线段树合并了…

流程

1.O(n)计算出每一个点的重儿子

2.对于节点i:

–遍历每一个节点:递归解决所有的轻儿子,同时消除递归产生的影响

–递归重儿子,不消除递归的影响

–统计所有轻儿子对答案的影响

–更新该节点的答案

–删除所有轻儿子对答案的影响

code

void dfs(int x,int fa,int opt)
{
  for(all edge)
  {
   if(to==fa||to==son[x]) continue;
   dfs(to,x,0);//暴力统计轻边的贡献
  }
  if(son[x]) dfs(son[x],x,1);//统计中儿子的贡献,不删除影响
  add(x);//暴力统计所有轻儿子的贡献
  ans[x]=NowAns;//更新答案
  if(!opt) delet(x);//如果需要删除贡献的话就删掉
}

当然这只是一般的dsu on tree 的经典模板代码。

例题1:树上数颜色

n个点的有根树,以1为根,每个点有一种颜色,每种颜色有一个编号。

我们称一种颜色占领了一个子树当且仅当没有其他颜色在这个子树中出现得比它多。

当然,一个子树可以被多种颜色占领。

给出一个树,求出每个节点的子树被占领的颜色的编号和。

代码:

#include
#define ll long long
#define rint register int
using namespace std;
const int N=1e5+7;
int n,col[N],tot=0,ans[N],son[N],sum[N],maxx=0,p=0,first[N],S;
int cnt[N];//表示目前某一种颜色的数量 
struct fuk
{
	int x,next;
}a[N<<1];
inline void add(int x,int to)
{
	tot++;
	a[tot].x=to; a[tot].next=first[x]; first[x]=tot;
}
void dfs(int x,int fa)
{
	sum[x]=1;
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==fa) continue;
		dfs(y,x);
		sum[x]+=sum[y];
		if(sum[son[x]]<sum[y]) son[x]=y;
	}
}
void change(int x,int fa,int v) 
{
	cnt[col[x]]+=v;
	if(cnt[col[x]]>maxx) maxx=cnt[col[x]],p=col[x];
	else if(cnt[col[x]]==maxx) p+=col[x];
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==fa||y==S) continue;
		change(y,x,v);
	}
}
void dsu(int x,int fa,int opt)
{
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==fa||y==son[x]) continue;
		dsu(y,x,0);
	}
	if(son[x]) dsu(son[x],x,1),S=son[x];
	change(x,fa,1); S=0;//统计轻儿子的贡献 
	//S=0,保证撤销信息的时候能全部撤销完 
	ans[x]=p; //记录答案 
	if(!opt) change(x,fa,-1),p=0,maxx=0;//撤销 
}
int main()
{
freopen("dsutree.in","r",stdin);
freopen("dsutree.out","w",stdout);
  scanf("%d",&n);
  for(int i=1;i<=n;i++) scanf("%d",&col[i]);
  for(int i=1;i<n;i++)
  {
  	int x,y;
  	scanf("%d%d",&x,&y);
  	add(x,y); add(y,x);
  }
  dfs(1,0); dsu(1,0,1);
  for(int i=1;i<=n;i++) printf("%d ",ans[i]);
  return 0;
}

例题2:【树的启发式合并】Last mile of the way(树形背包优化)

⼩A从仓库里找出了⼀棵n个点的有根树,1号节点为这棵树的根,树上每个节点的权值 为w[i], ⼤⼩为a[i]。

现在他⼼中产⽣了Q个疑问,每个疑问形如在x的⼦树⾥,选出⼀些⼤⼩不超过s的节点(不可以重复选⼀个节点),最⼤权值可以为多少。

代码:

#include
#define ll long long
#define rint register int
using namespace std;
const int N=5010;
int n,h[N],son[N],v[N],sum[N],Q,tot=0,first[N],fa[N];
ll f[N][N];
struct fuk
{
	int x,next;
}a[N<<1];
inline int add(int x,int to)//前向星存图
{
	tot++;
	a[tot].x=to; a[tot].next=first[x]; first[x]=tot;
}
void dfs(int x,int f)//重链剖分
{
	sum[x]=1;
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==f) continue;
		fa[y]=x;
		dfs(y,x);
		sum[x]+=sum[y];
		if(sum[son[x]]<sum[y]) son[x]=y; 
	}
}
void change(int t,int x)//轻儿子子树里的所有点依次考虑加入t的背包中 
{
  for(int i=N-10;i>=h[x];i--) f[t][i]=max(f[t][i],f[t][i-h[x]]+v[x]);
  for(int i=first[x];i;i=a[i].next)
  {
    int y=a[i].x;
    if(y==fa[x]) continue;
	change(t,y);	
  }	
} 
void dp(int x)
{
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==fa[x]) continue;
		dp(y); 
	}
	memcpy(f[x],f[son[x]],sizeof(f[x]));//直接将重儿子拿来用 
	for(int i=first[x];i;i=a[i].next)
	{
		int y=a[i].x;
		if(y==fa[x]||y==son[x]) continue;
		change(x,y);//计算轻儿子的贡献 
	}
	for(int i=N-10;i>=h[x];i--) f[x][i]=max(f[x][i],f[x][i-h[x]]+v[x]);//考虑自己
    /*注意此时并没有一个撤销的操作,因为每一个点背包都是相对独立的,无需撤销*/
}
int main()
{
//freopen("A.in","r",stdin);
//freopen("A.out","w",stdout);
  scanf("%d",&n);
  for(int i=1;i<n;i++)
  {
  	int x,y;
  	scanf("%d%d",&x,&y);
  	add(x,y); add(y,x);
  }
  for(int i=1;i<=n;i++) scanf("%d%d",&v[i],&h[i]);
  dfs(1,0); dp(1);
  scanf("%d",&Q);
  while(Q--)
  {
  	int x,s;
  	scanf("%d%d",&x,&s);
  	printf("%lld\n",f[x][s]);
  }
  return 0;
}

复杂度的相关证明

性质:一个节点到根的路径上轻边个数不会超过 l o g ( n ) log(n) log(n)

证明:

设根到该节点有 x x x条轻边,该节点的大小为 y y y,根据轻重边的定义,轻边所连向的点的大小不会成为该节点总大小的一半。

这样每经过一条轻边, y y y的上限就会/2,因此 y < n / 2 x yy<n/2x

因为 n n n> 2 x 2^x 2x,所以 x x x< l o g ( n ) log(n) log(n)

然而这条性质并不能解决问题。

我们考虑一个点会被访问多少次

一个点被访问到,只有两种情况

1、在暴力统计轻边的时候访问到。

根据前面的性质,该次数 < l o g ( n ) <log(n)

2、通过重边 / 在遍历的时候被访问到显然只有一次

如果统计一个点的贡献的复杂度为 O ( 1 ) O(1) O(1)的话,该算法的复杂度为 O ( n l o g ( n ) ) O(nlog(n)) O(nlog(n))

这可真是妙啊!

优点

1.思维简单,好写,易懂。

2.精准覆盖每种情况。

3.复杂度还是很优秀的。

当然dsu on tree 的更多细节还是要自己多多去摸索,熟能生巧。

参考博客:

https://www.bbsmax.com/A/GBJr6oNWJ0/

https://www.cnblogs.com/zwfymqz/p/9683124.html

祝大家CSP ++RP

你可能感兴趣的:(树的启发式合并,树的启发式合并)