dfs序和欧拉序

生命不息,学习不止,昨天学了两个算法,总结一下,然而只是略懂,请路过的大佬多多谅解。ios

 

1、dfs序

一、什么是dfs序?算法

其实彻底能够从字面意义上理解,dfs序就是指一棵树被dfs时所通过的节点的顺序数组

原图来源于网络,并通过灵魂画师xhk的一发魔改。网络

 

好的,这张图的dfs序显然为A-B-D-E-G-C-F-H学习

二、dfs序怎么写?优化

首先你得会写dfs(不会的请先自行学习)spa

而后咱们都知道正常的dfs通常是长这样的(以及博主是只蒟蒻)code

咱们只须要多一个辅助数组来记录dfs序就好了blog

 

 代码:string

#include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[100010]; int dfs_[200020],len; void dfs(int u,int fa) { dfs_[++len]=u; int sz=g[u].size(); for(int i=0;i<sz;i++) { if(g[u][i]!=fa) { dfs(g[u][i],u); } } } int main() { int n; scanf("%d",&n); for(int i=1;i<=n;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); for(int i=1;i<=len;i++) { printf("%d ",dfs_[i]); } printf("\n"); }

好的,都应该能够理解了吧。

因而问题来了,咱们要dfs序有什么用呢?

三、dfs序的用处

这得从dfs的优点来探讨了。

dfs是深度优先的,因此对于一个点,它会先遍历完它的全部子节点,再去遍历他的兄弟节点以及其余

因此对于一棵树的dfs序来讲,这个点和他全部的子节点会被存储在连续的区间之中。

仍然是这张图:

原图来源于网络,并通过灵魂画师xhk的一发魔改。

 咱们都知道它的dfs序是:A-B-D-E-G-C-F-H

而后咱们能够发现B字树B-D-E-G,C子树C-F-H都在一段连续的区间中。

那么这有什么好处呢?

好比说如今有一道题:给你一颗树,给出m个x和w,意为将x子树中的全部点加上一个权值w,最后询问全部点的权值

既然dfs序中x和他的全部子节点都在连续的区间上,那么咱们就能够将它简化成差分的问题。

好比说给b节点加2,就能够简化为给b的差分数组+2,c的差分数组-2

也就是说咱们只须要找到第一个不包括在B的子树的位置减掉2,到时候还原回前缀和就能够求解了。

是否是很简单?

那么问题来了,咱们怎么找第一个不在B子树中的点?

这里,须要引进一个新的东西

四、时间戳

时间戳也很好理解,它就比如一个标签,贴在每个点上,记录dfs第一次开始访问这个点的时间以及最后结束访问的时间。

因此它仍是和dfs结合的

不过须要注意,dfs序和1-n是不同的

因此可千万不要像博主同样傻傻地用s[i]来表示点i的起始时间!

那么怎么保存呢?反正博主比较愚昧,只想到用另外一个数组来记录一下(这就是博主自带大常数和大空间的缘由)

因而变成了这样

好的,那么知道了起始时间和终结时间之后咱们该怎么用呢?

由于搜索进下一个点时时间增长,且结束时间逐级传递。

因此说咱们的点的子节点的时间区间必定包含在这个点的时间区间内。

因此若是一个点的起始时间和终结时间被另外一个点包括,这个点确定是另外一个点的子节点。(算导里称之为括号化定理)

所以能够判断一个点是不是另外一个点的子节点。

代码:

#include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[100010]; int dfs_[200020],len,time,s[200020],e[200020],pos[200020]; void dfs(int u,int fa) { int x=len+1; s[++len]=++time; dfs_[len]=u; pos[u]=len; int sz=g[u].size(); for(int i=0;i<sz;i++) { if(g[u][i]!=fa) { dfs(g[u][i],u); } } e[x]=time; } int main() { int n,m; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); for(int i=1;i<=m;i++) { int x,y; scanf("%d%d",&x,&y); x=pos[x]; y=pos[y]; if(s[x]<=s[y]&&e[y]<=e[x]) { printf("YES\n"); } else { printf("NO\n"); } } }

至于如何让找出第一个?

仍是上面那张图,设若是是B的子节点就为1,不然0

嗯,这玩意好像能够二分呢!

因而乎就作好了!log(n)的修改,彷佛还挺可作的!

 好吧伪装代码是这样的:

#include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[100010]; int dfs_[200020],len,time,s[200020],e[200020],pos[200020],a[200020],b[200020],sub[200020]; void dfs(int u,int fa) { int x=len+1; s[++len]=++time; dfs_[len]=u; b[len]=a[u]; pos[u]=len; int sz=g[u].size(); for(int i=0;i<sz;i++) { if(g[u][i]!=fa) { dfs(g[u][i],u); } } e[x]=time; } int main() { int n,m,t; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); } for(int i=1;i<=n-1;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); sub[1]=b[1]; for(int i=2;i<=len;i++) { sub[i]=b[i]-b[i-1]; } for(int i=1;i<=m;i++) { int x,w; scanf("%d%d",&x,&w); x=pos[x]; sub[x]+=w; int l=x,r=a[len]; while(l<r) { int mid=(l+r)>>1; if(s[x]<=s[mid]&&e[mid]<=e[x]) { l=mid+1; } else { r=mid; } } int y=r; sub[y]-=w; } for(int i=1;i<=n;i++) { sub[i]=sub[i-1]+sub[i]; } for(int i=1;i<=n;i++) { int x=pos[i]; printf("%d ",sub[x]); } }

 而后还能再优化吗?固然能够,咱们只须要记录一下每一个点的dfs结束的位置,这样子就不用二分了,怎么写?本身想一想吧先╮( ̄▽ ̄)╭,若是你能坚持看完欧拉序,也许你能找到差很少的代码哦~

2、欧拉序

 一、什么是欧拉序

就是从根结点出发,按dfs的顺序在绕回原点所通过全部点的顺序

二、欧拉序有怎么写?

(1)dfs到加进,dfs回加进,总共加入度遍。

 

原图来源于网络,并通过灵魂画师xhk的一发魔改。

欧拉序1为A-B-D-B-E-G-E-B-A-C-F-H-F-C-A

同时须要一个辅助数组

代码1:

#include<cmath> #include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[40010]; int len,a[80020]; void dfs(int u,int fa) { a[++len]=u; int sz=g[u].size(); for(int i=0; i<sz; i++) { if(g[u][i]!=fa) { dfs(g[u][i],u); a[++len]=u; } } } int main() { int t; scanf("%d",&t); while(t--) { int n,m; len=0; memset(a,0,sizeof(a)); scanf("%d",&n); for(int i=1; i<=n; i++) { g[i].clear(); } for(int i=1; i<=n-1; i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); for(int i=1;i<=len;i++) { printf("%d ",a[i]); } } }

(2)dfs进加进,dfs最后一次回加进,总共加两遍

 

原图来源于网络,并通过灵魂画师xhk的一发魔改。

欧拉序2为A-B-D-D-E-G-G-E-B-C-F-H-H-F-C-A

代码2:

#include<cmath> #include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[40010]; int len,a[80020]; void dfs(int u,int fa) { a[++len]=u; int sz=g[u].size(); for(int i=0; i<sz; i++) { if(g[u][i]!=fa) dfs(g[u][i],u); } } a[++len]=u; } int main() { int t; scanf("%d",&t); while(t--) { int n,m; len=0; memset(a,0,sizeof(a)); scanf("%d",&n); for(int i=1; i<=n; i++) { g[i].clear(); } for(int i=1; i<=n-1; i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); for(int i=1;i<=len;i++) { printf("%d ",a[i]); } } }

 固然还有几种写法,各有长处,不在介绍了就。

好的,那么咱们来说下这几种欧拉序的用处

3、欧拉序的用处

 一、求LCA

假设咱们使用欧拉序1

则咱们要求的两个点在欧拉序中的第一个位置之间确定包含他们的lca,由于欧拉序1上任意两点之间确定包含从第一个点走到第二个点访问的路径上的全部点

因此只须要记录他们的深度,而后从两个询问子节点x,y第一次出现的位置之间的深度最小值便可,可能不大好理解,看张图吧。

 

代码:

#include<cmath> #include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int > g[40010]; int len,a[80020],dep[80020],pos[80020][17],dp[80020][17],vis[80020],cnt[80020]; void dfs(int u,int fa,int deep) { a[++len]=u; dep[len]=deep+1; if(!vis[u]) { cnt[u]=len; vis[u]=1; } int sz=g[u].size(); for(int i=0; i<sz; i++) { if(g[u][i]!=fa) { dfs(g[u][i],u,deep+1); a[++len]=u; dep[len]=deep+1; } } } int main() { int t; scanf("%d",&t); while(t--) { int n,m; len=0; memset(a,0,sizeof(a)); memset(dep,0,sizeof(dep)); memset(pos,0,sizeof(pos)); memset(dp,0,sizeof(dp)); memset(vis,0,sizeof(vis)); memset(cnt,0,sizeof(cnt)); scanf("%d%d",&n,&m); for(int i=1; i<=n; i++) { g[i].clear(); } for(int i=1; i<=n-1; i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0,0); printf("%d\n",len); for(int i=1; i<=len; i++) { dp[i][0]=dep[i]; pos[i][0]=i; } for(int j=1; j<=17; j++) { for(int i=1; i<=len; i++) { if(i+(1<<(j-1))>=len) { break; } if(dp[i][j-1]>dp[i+(1<<(j-1))][j-1]) { dp[i][j]=dp[i+(1<<(j-1))][j-1]; pos[i][j]=pos[i+(1<<(j-1))][j-1]; } else { dp[i][j]=dp[i][j-1]; pos[i][j]=pos[i][j-1]; } } } for(int i=1; i<=m; i++) { int x,y; scanf("%d%d",&x,&y); int dx=cnt[x]; int dy=cnt[y]; if(dx>dy) { swap(dx,dy); swap(x,y); } int k=(int)(log((double)(dy-dx+1))/log(2.0)); int p; if(dp[dx][k]>dp[dy-(1<<k)+1][k]) { p=pos[dy-(1<<k)+1][k]; } else { p=pos[dx][k]; } printf("%d\n",a[p]); } } }

例题:HDU 2586

二、求子树的权值之和

先来道滑稽题:(因为纯属手糊,若有错误,还请谅解)

给你一棵树,告诉你每一个点的点权,给你m个x和w,表示将子树x中每一个点的权值和加w,而后再给你t个x,表示询问x子树中全部点的权值之和.

样例输入:

7 2 7
6 5 4 2 1 8 7
1 2
2 3
2 4
4 5
1 6
6 7
5 1
3 2
1
2
3
4
5
6
7

样例输出:
36 15 6 4 2 15 7

这道题和dfs序中那道很像不是吗?

好,咱们来看欧拉序2,它有什么优势呢?你能够发现,每一个点都出现了两遍,而这个点第一次出现和第二次出现的位置之间就是他的全部子孙节点.

ちょっと まで!

有没有发现这就是以前记录结束位置的想法?

只不过这个更具体更好懂呢!

而后一样是差分,只不过差分时咱们能够直接经过该点第二次出现的位置+1来得到第一个不是该子树的点,而后,o(1)的修改就实现了.

可是若是这样怎么查询权值和呢?

咱们都知道这个点第一次出现的位置和第二次出现的位置之间是他的子树,那么咱们只须要维护一遍前缀和,用第二个的位置减第一个的位置前的那个位置,就能够获得权值和了.

不过因为每一个子树中的点都被算了两遍,咱们要除以二.

代码:

#include<vector> #include<cstdio> #include<cstring> #include<iostream> #include<algorithm>
using namespace std; vector<int> g[100010]; int euler[200020],pos[100010][2],len,a[100010],sub[200020]; void dfs(int u,int fa) { euler[++len]=u; pos[u][0]=len; int sz=g[u].size(); for(int i=0;i<sz;i++) { if(g[u][i]!=fa) { dfs(g[u][i],u); } } euler[++len]=u; pos[u][1]=len; } int main() { int n,m,t; len=0; scanf("%d%d%d",&n,&m,&t); for(int i=1;i<=n;i++) { scanf("%d",&a[i]); } for(int i=1;i<=n-1;i++) { int from,to; scanf("%d%d",&from,&to); g[from].push_back(to); g[to].push_back(from); } dfs(1,0); sub[1]=a[euler[1]]; for(int i=2;i<=len;i++) { sub[i]=a[euler[i]]-a[euler[i-1]]; } for(int i=1;i<=m;i++) { int x,w; scanf("%d %d",&x,&w); sub[pos[x][0]]+=w; sub[pos[x][1]+1]-=w; } for(int i=1;i<=len;i++) { sub[i]=sub[i-1]+sub[i]; } for(int i=1;i<=len;i++) //若是删去维护前缀和的过程,就是查询单个点的权值 { sub[i]=sub[i-1]+sub[i]; } for(int i=1;i<=t;i++) { int x; scanf("%d",&x); printf("%d\n",(sub[pos[x][1]]-sub[pos[x][0]-1])/2); } }

 

 

 

沉迷学习,没法自拔......

相关文章
相关标签/搜索