【数据结构】dfs序

对于一棵树,我们可以通过深度优先搜索记录到达每一个点的时间戳,由这个时间戳构成的序列就是dfs序,每个时间戳代表一个节点。
我们可以通过以下代码实现:

void dfs(int u)
{
	st[u]=++tot;//记录子树开始的访问时间
	for(int re i=f[u];i;i=nxp[i])
	{
		int v=e[i].v;
		if(!st[v])
		{
			dep[v]=dep[u]+1;
			dfs(v)
		};
	}
	ed[u]=tot;//记录子树访问结束的时间
}

对于这样的dfs序,我们就能利用树状数组进行维护单点信息,修改单点,维护子树信息,修改子树,维护路径,修改路径等操作。(如果涉及复杂路径修改或一些复杂的信息维护,最好使用树链剖分与线段树实现)

问题一:单点修改,子树查询

对于这样的问题,我们只需要直接用树状数组进行单点修改,通过树状数组求得区间[st[root]-1,ed[root]]即可求得以root为根的子树和。
例题大意:
给出一个苹果树,每个节点一开始都有苹果

C X,如果X点有苹果,则拿掉,如果没有,则新长出一个

Q X,查询X点与它的所有后代分支一共有几个苹果

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
	int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
	e[++cnt].u=u;
	e[cnt].v=v;
	nxp[cnt]=f[u];
	f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
	while(k<=idx)
	{
		c[k]+=v;
		k+=low(k);
	}
}
inline int ask(int k)
{
	int ret=0;
	while(k>0)
	{
		ret+=c[k];
		k-=low(k);
	}
	return ret;
}
int st[N];
int ed[N];
void dfs(int u)
{
	st[u]=++idx;
	for(int re i=f[u];i;i=nxp[i])
	{
		int v=e[i].v;
		if(!st[v])dfs(v);
	}
	ed[u]=idx;
}
char p;
int main()
{
	scanf("%d",&n);
	for(int re i=1;i<n;i++)
	{
		scanf("%d%d",&a,&b);
		add(a,b);
		add(b,a);
		g[i]=1;
	}
	g[n]=1;
	scanf("%d",&m);
	dfs(1);
	for(int re i=1;i<=n;i++)
		change(st[i],1);
	for(int re i=1;i<=m;i++)
	{
		scanf("\n%c%d",&p,&a);
		if(p=='C')
		{
			if(g[a])change(st[a],-1);
			else change(st[a],1);
			g[a]^=1;
		}
		else printf("%d\n",ask(ed[a])-ask(st[a]-1));
	}
}

问题二:树上路径修改,单点查询

对于这么一个题,我们可以利用树上差分的思想:
修改x->y的路径,就等价于
x->root +v;
y->root +v;
lca(x,y)->root -v;
fa[lca(x,y)]->root -v;
于是问题的修改又可以转换为单点修改。而对于一个节点y的权值,其它节点会对其产生影响,当且仅当其它节点在y的子树内。若y不受x的路径影响,y子树中必有一部分节点+v,一部分节点-v,则对y没有影响。若y受x的路径影响,则y中的子树和就会增大v。这一点可以画图举例理解。所以我们可以维护子树和,修改单点,就能解决这个问题。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x y v:表示将节点x到y最短路径上所有的点的权值+v
2 x:表示查询节点x的权值
开始的时候每个节点的权值是0

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
	int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
	e[++cnt].u=u;
	e[cnt].v=v;
	nxp[cnt]=f[u];
	f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
	while(k<=idx)
	{
		c[k]+=v;
		k+=low(k);
	}
}
inline int ask(int k)
{
	int ret=0;
	while(k>0)
	{
		ret+=c[k];
		k-=low(k);
	}
	return ret;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
	st[u]=++idx;
	for(int re i=1;(1<<i)<=dep[u];i++)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int re i=f[u];i;i=nxp[i])
	{
		int v=e[i].v;
		if(!dep[v])
		{
			dep[v]=dep[u]+1;
			fa[v][0]=u;
			dfs(v);
		}
	}
	ed[u]=idx;
}
int lca(int a,int b)
{
	if(dep[a]<dep[b])swap(a,b);
	int t=dep[a]-dep[b];
	for(int re i=0;(1<<i)<=t;i++)
		if(t&(1<<i))a=fa[a][i];
	if(a==b)return a;
	for(int re i=18;i>=0;i--)
	{
		if(fa[a][i]!=fa[b][i])
		{
			a=fa[a][i];
			b=fa[b][i];
		}
	}
	return fa[a][0];
}
char p;
int main()
{
	scanf("%d",&n);
	for(int re i=1;i<n;i++)
	{
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}
	scanf("%d",&m);
	dep[1]=1;
	dfs(1);
	for(int re i=1;i<=m;i++)
	{
		int q,z;
		scanf("%d",&q);
		if(q==1)
		{
			scanf("%d%d%d",&a,&b,&z);
			int lc=lca(a,b);
			change(st[a],z);
			change(st[b],z);
			change(st[lc],-z);
			if(lc!=1)
			change(st[fa[lc][0]],-z);
			
		}
		else
		{
			scanf("%d",&a);
			printf("%d\n",ask(ed[a])-ask(st[a]-1));
		}
	}
}



问题三:树上路径修改,子树查询

我们考虑假设X在Y的子树内,那么对于询问点Y,询问值会加
上W[x] * (depth[x] - depth[y]+ 1)。
故整颗子树所受影响即:
∑ x 在 y 子 树 内 w [ x ] ∗ ( d e p [ x ] − d e p [ y ] + 1 ) \sum_{x在y子树内}{w[x]*(dep[x]-dep[y]+1)} xyw[x](dep[x]dep[y]+1)
拆开可以得到:
∑ x 在 y 子 树 内 w [ x ] ∗ ( d e p [ x ] + 1 ) − d e p [ y ] ∗ ∑ x 在 y 子 树 内 w [ x ] \sum_{x在y子树内}{w[x]*(dep[x]+1)}-dep[y]*\sum_{x在y子树内} {w[x]} xyw[x](dep[x]+1)dep[y]xyw[x]
对于每一次修改,我们是可以知道x的,因此我们可以维护两个值w[x]和w[x]*(dep[x]+1)的子树和,询问时再处理回答即可。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x y v:表示将节点x到y最短路径上所有的点的权值+v
2 x:表示查询子树x的权值和
开始的时候每个节点的权值是0
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];

int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
	int u,v;
}e[N];
int idx=0;
inline int low(int x){return x&(-x);}
struct tree{
	int c[N];
	inline void change(int k,int v)
	{
		while(k<=idx)
		{
			c[k]+=v;
			k+=low(k);
		}
	}
	inline int ask(int k)
	{
		int ret=0;
		while(k>0)
		{
			ret+=c[k];
			k-=low(k);
		}
		return ret;
	}
}t1,t2;//t1:w,t2:(dep(x)+1)*w
inline void add(int u,int v)
{
	e[++cnt].u=u;
	e[cnt].v=v;
	nxp[cnt]=f[u];
	f[u]=cnt;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
	st[u]=++idx;
	for(int re i=1;(1<<i)<=dep[u];i++)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int re i=f[u];i;i=nxp[i])
	{
		int v=e[i].v;
		if(!dep[v])
		{
			dep[v]=dep[u]+1;
			fa[v][0]=u;
			dfs(v);
		}
	}
	ed[u]=idx;
}
int lca(int a,int b)
{
	if(dep[a]<dep[b])swap(a,b);
	int t=dep[a]-dep[b];
	for(int re i=0;(1<<i)<=t;i++)
		if(t&(1<<i))a=fa[a][i];
	if(a==b)return a;
	for(int re i=18;i>=0;i--)
	{
		if(fa[a][i]!=fa[b][i])
		{
			a=fa[a][i];
			b=fa[b][i];
		}
	}
	return fa[a][0];
}
char p;
int main()
{
	scanf("%d",&n);
	for(int re i=1;i<n;i++)
	{
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}
	scanf("%d",&m);
	dep[1]=1;
	dfs(1);
	for(int re i=1;i<=m;i++)
	{
		int q,z;
		scanf("%d",&q);
		if(q==1)
		{
			scanf("%d%d%d",&a,&b,&z);
			int lc=lca(a,b);
			t1.change(st[a],z);
			t2.change(st[a],z*(dep[a]+1));
			t1.change(st[b],z);
			t2.change(st[b],z*(dep[b]+1));
			t1.change(st[lc],-z);
			t2.change(st[lc],-z*(dep[lc]+1));
			if(lc!=1)
			t1.change(st[fa[lc][0]],-z),
			t2.change(st[fa[lc][0]],-z*(dep[fa[lc][0]]+1));
		}
		else
		{
			scanf("%d",&a);
			printf("%d\n",t2.ask(ed[a])-t2.ask(st[a]-1)-dep[a]*(t1.ask(ed[a])-t1.ask(st[a]-1)));
		}
	}
}

问题四:单点修改,路径询问

这一类的题我们可以利用lca把路径询问转换为x到根节点的询问。
d i s ( x , y ) = d i s ( x , r o o t ) + d i s ( y , r o o t ) − d i s ( l c a ( x , y ) , r o o t ) − d i s ( f a [ l c a ( x , y ) ] , r o o t ) dis(x,y)=dis(x,root)+dis(y,root)-dis(lca(x,y),root)-dis(fa[lca(x,y)],root) dis(x,y)=dis(x,root)+dis(y,root)dis(lca(x,y),root)dis(fa[lca(x,y)],root)
可以画图理解一下。
而我们思考什么时候对于x的修改会影响y的询问。显而易见,y到
root的距离被x影响当且仅当y在x的子树内,因此我们可以维护这样一个d数组,每次修改时d[st[x]]+=v,d[ed[x]+1]-=v,前缀和即节点到根的距离。我们可以这么理解,在st[x]左边的区间内,这样的修改不会对前缀和有影响,在st[x]->ed[x]的区间内,前缀和增加了v,在ed[x]右边的区间,显然前缀和也不受到影响,故可以证明这样的修改只会影响子树内节点到root的距离。
例题大意:
有n个节点N-1条边,这是一颗树,有2个操作:
1 x v:表示将节点x的权值+v
2 x y:表示查询x到y的路径权值和
代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
#include<algorithm>
#include<cmath>
#include<queue>
#define re register
using namespace std;
int n,m,a,b,s,t;
const int N=2e5+1;
int f[N];
int c[N];
int g[N];
int nxp[N<<2|1];
int cnt=0,tot=0;
struct ndeo{
	int u,v;
}e[N];
int idx=0;
inline void add(int u,int v)
{
	e[++cnt].u=u;
	e[cnt].v=v;
	nxp[cnt]=f[u];
	f[u]=cnt;
}
inline int low(int x){return x&(-x);}
inline void change(int k,int v)
{
	while(k<=idx)
	{
		c[k]+=v;
		k+=low(k);
	}
}
inline int ask(int k)
{
	int ret=0;
	while(k>0)
	{
		ret+=c[k];
		k-=low(k);
	}
	return ret;
}
int fa[N][20];
int st[N];
int ed[N];
int dep[N];
void dfs(int u)
{
	st[u]=++idx;
	for(int re i=1;(1<<i)<=dep[u];i++)
		fa[u][i]=fa[fa[u][i-1]][i-1];
	for(int re i=f[u];i;i=nxp[i])
	{
		int v=e[i].v;
		if(!dep[v])
		{
			dep[v]=dep[u]+1;
			fa[v][0]=u;
			dfs(v);
		}
	}
	ed[u]=idx;
}
int lca(int a,int b)
{
	if(dep[a]<dep[b])swap(a,b);
	int t=dep[a]-dep[b];
	for(int re i=0;(1<<i)<=t;i++)
		if(t&(1<<i))a=fa[a][i];
	if(a==b)return a;
	for(int re i=18;i>=0;i--)
	{
		if(fa[a][i]!=fa[b][i])
		{
			a=fa[a][i];
			b=fa[b][i];
		}
	}
	return fa[a][0];
}
char p;
int sum[N];
int main()
{
	scanf("%d",&n);
	for(int re i=1;i<=n;i++)scanf("%d",&g[i]);
	for(int re i=1;i<n;i++)
	{
		scanf("%d%d",&a,&b);
		add(a,b);add(b,a);
	}
	scanf("%d",&m);
	dep[1]=1;
	dfs(1);
	for(int re i=1;i<=n;i++)
	{
		change(st[i],g[i]);
		change(ed[i]+1,-g[i]);
	}
	for(int re i=1;i<=m;i++)
	{
		int q,z;
		scanf("%d",&q);
		if(q==1)
		{
			scanf("%d%d",&a,&b);
			change(st[a],b);
			change(ed[a]+1,-b);
		}
		else
		{
			scanf("%d%d",&a,&b);
			int lc=lca(a,b);
			printf("%d\n",ask(st[a])+ask(st[b])-ask(st[lc])-ask(st[fa[lc][0]]));
		}
	}
}

问题五:子树修改,单点查询

我们考虑X对Y 的贡献.显然,当X在Y的子树里才会对Y有贡献,贡献为W。于是转化为修改一个点权,查询点到根的路径的权值和。于是就转化为了问题四。

问题六:子树修改,子树查询

线段树或树状数组即可在dfs序中实现区间修改,区间查询。
(其实上述问题几乎都可以用dfs序加线段树维护。俗话说得好,智商不够,数据结构来凑

问题七:子树修改,路径查询

对子树X的所有权值增加W,查询x到y路径上的权值和把最短路转化为x到根的权值和。考虑修改X对Y的贡献,显然Y在X子树中才有贡献,贡献为wx*(dep(x)-dep(y)+1),分离开
发现与Y无关,照例分为2部分处理。每部分相当于修改一个点权,查询某个点到跟路径和,每部分相当于问题四。

【例题】:「HAOI2015」树上操作
【题目描述】
有一棵点数为 N 的树,以点 1 为根,且树点有边权。然后有 M 个操作,分为三种:
1:把某个节点 x 的点权增加 a 。
2:把某个节点 x 为根的子树中所有点的点权都增加 a 。
3:询问某个节点 x 到根的路径中所有点的点权和。
【输入】
第一行包含两个整数 N,M。表示点数和操作数。
接下来一行 N 个整数,表示树中节点的初始权值。
接下来 N−1 行每行两个正整数 fr,to , 表示该树中存在一条边 (fr,to)
再接下来 M 行,每行分别表示一次操作。其中第一个数表示该操作的种类(1-3) ,之后接这个操作的参数(x 或者 x a) 。

【思路】分别对每一种修改对询问的影响值进行维护,答案即所有影响的和。对于点的初值,可以理解为单点修改。
代码:

#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<string>
#include<vector>
#define re register
using namespace std;
const long long N=1e5+5;
long long n;
inline long long low(long long x){return x&(-x);}
struct tree{
	long long c[100001];
	inline void change(long long k,long long v)
	{
		while(k<=n){
			c[k]+=v;
			k+=low(k);
		}
	}
	inline long long ask(long long k)
	{
		long long ret=0;
		while(k>0)
		{
			ret+=c[k];
			k-=low(k);
		}
		return ret;
	}
}t1,t2,t3;
/*
opt2
t2:(dep(x)-1)*w2
t3:w2
opt1
t1:w1
*/
long long a,b;
long long st[N];
long long ed[N],tot=0,opt;
long long m,x;
long long f[N];
long long nxp[N<<1|1];
long long cnt=0;
struct node{
	long long u,v;
}e[N<<1|1];
inline void add(long long u,long long v)
{
	e[++cnt].u=u;
	e[cnt].v=v;
	nxp[cnt]=f[u];
	f[u]=cnt;
} 
long long val[N];
long long dep[N];
void dfs(long long u)
{
	st[u]=++tot;
	for(long long re i=f[u];i;i=nxp[i])
	{
		long long v=e[i].v;
		if(!st[v])dep[v]=dep[u]+1,dfs(v);
	}
	ed[u]=tot;
}
inline long long query1(long long x)
{
	return t1.ask(st[x]);
}
inline long long query2(long long x)
{
	return dep[x]*t3.ask(st[x])-t2.ask(st[x]);
}
int main()
{
	scanf("%lld%lld",&n,&m);
	for(long long re i=1;i<=n;i++)scanf("%lld",&val[i]);
	for(long long re i=1;i<=n-1;i++)
	{
		scanf("%lld%lld",&a,&b);
		add(a,b);
		add(b,a);
	}
	dep[1]=1;
	dfs(1);
	for(long long re i=1;i<=n;i++)
	{
		t1.change(st[i],val[i]);
		t1.change(ed[i]+1,-val[i]);
	}
	for(long long re i=1;i<=m;i++)
	{
		scanf("%lld",&opt);
		if(opt==1)
		{
			scanf("%lld%lld",&x,&a);
			t1.change(st[x],a);
			t1.change(ed[x]+1,-a);
		}
		else if(opt==2)
			{
				scanf("%lld%lld",&x,&a);
				t2.change(st[x],(dep[x]-1)*a);
				t2.change(ed[x]+1,-(dep[x]-1)*a);
				t3.change(st[x],a);
				t3.change(ed[x]+1,-a);
			}
			else
			{
				scanf("%lld",&x);
				printf("%lld\n",query1(x)+query2(x));
			}
	}
}

【总结】

对于这一类型的题,我们其实需要思考修改对询问值的影响,思考每一次修改对某个点的贡献,再根据式子分离已知进行维护。否则,使用线段树数据结构加以辅助也可。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值