zoukankan      html  css  js  c++  java
  • 【暖*墟】 #树与图# 深度优先遍历入门知识点

    【一. 时间戳(dfn)】

    什么是时间戳? 就是每个位置被访问到的次序。

    比如说我们对一棵树进行深搜,在深搜中访问的相应次序就被我们称为时间戳。

    【二. 树的dfs序】

    1.dfs序的作用

    维护一系列树上的问题,解决一棵树上的所有后代结点信息的更改和祖先结点有关改变,

    通过dfs来记录树的每个顶点的出入时间戳,来控制它子树上的所有结点的状态的更新。

    用 L[ i ],R[ i ] 来记录这个祖先结点控制后代结点的区间。

    即dfs序的特点就是:每个节点编号x在序列中恰好出现两次。

    一棵子树的dfs序就变成了一个区间 [ L[ x ] , R[ x ] ]

    树的dfs序也用于深搜解决树的前序、中序、后序遍历问题。

    • 把树上的问题转化成了序列上的问题
    • 辅以各种数据结构(ST表、树状数组、线段树)进行运算
    • 子树求和->区间求和

    2.dfs序的图解

    3.dfs序的代码

    void dfs(int x){
        a[++m]=x; //a数组储存dfs序
        v[x]=1; //记录点x被访问过
        for(int i=head[x];i;i=next[i]){
            int y=ver[i]; //取出点的价值
            if(v[y]) continue; //已经到达过
            dfs(y);
        }
        a[++m]=x; //dfs返回到x
    }

    4.例题:poj3321 Apple Tree 

    【三. 树的深度、直径和重心】

    1.树的深度

    已知根节点深度为0。自顶而下统计信息。

    若节点x的深度为 d[x] ,则它的子节点y的深度就是 d[y]=d[x]+1 。

    结合dfs,可求得每个节点的深度。

    void dfs_deep(int x){
        v[x]=1; //记录x点被访问过
        for(int i=head[x];i;i=next[i]){
            int y=ver[i];
            if(v[y]) continue;
            d[y]=d[x]+1; dfs_deep(y);
        }
    }

    2.树的直径与重心

    1)树的直径  即树上最长的简单路径。

    在树上任选一点w,求距离w最远的点u,求距离u最远的点v,u到v的距离即为树的直径。

    简单证明:

    <1> 如果w在直径上,那么u一定是直径的一个端点。

    反证:若u不是端点,则从直径另一端点到w再到u的距离比直径更长,与假设矛盾。

    <2> 如果w不在直径上,且w到其距最远点u的路径与直径一定有一交点c,

    那么由上一个证明可知,u是直径的一个端点。

    <3> 如果w到最远点u的路径与直径没有交点,设直径的两端为S与T,

    那么(w->u)>(w->c)+(c->T),推出(w->u)+(S->c)+(w->c)>(S->c)+(c->T)=(S->T)与假设矛盾。

    因此w到最远点u的路径与直径必有交点。

    S-----------c-----------T

                    |

                   w------u

    #include<cstdio>
    #include<cstring>
    #define N 4200
     
    struct node{
        int next;
        int to;
    }edge[N];
     
    int num_edge,head[N],dis[N],n,a,b,y;
     
    int add_edge(int from,int to){
        edge[++num_edge].next=head[from];
        edge[num_edge].to=to;
        head[from]=num_edge;
    }
     
    int dfs(int x){ //计算树的深度
        for(int i=head[x];i;i=edge[i].next)
            if(!dis[edge[i].to]){
                dis[edge[i].to]=dis[x]+1;
                dfs(edge[i].to);
            }
    }
     
    int main(){
        scanf("%d",&n);
        for(int i=1;i<n;++i){
            scanf("%d%d",&a,&b);
            add_edge(a,b);
            add_edge(b,a);
        }
        
        dfs(1); //计算树的深度
        for(int i=y=1;i<=n;i++)
            if(dis[i]>dis[y])
                y=i; //寻找与w距离最大的某点u
        memset(dis,0,sizeof(dis));
        
        dfs(y); //以y为根,重新建树
        for(int i=y=1;i<=n;i++)
            if(dis[i]>dis[y])
                y=i; //找到离u最大的v
        printf("%d",dis[y]); //得出树的直径
        
        return 0;
    }
    View Code

    2)树的重心  树上一点,使该点为根的所有子树中最大子树的节点数最少。

    一般的树只有一个重心,有些有偶数个节点的树,有两个节点。

    法一:简单的两次搜索求出。

    分别搜索求出每个结点的子结点数量son[u]、使max{son[u],n-son[u]-1}最小的结点

    实际上这两步操作可以在一次遍历中解决。对结点u的每一个儿子v,递归的处理v,

    求出son[v],然后判断是否是结点数最多的子树,处理完所有子结点后,判断u是否为重心。

    struct CenterTree{
        int n,ans,siz,son[maxn];
        void dfs(int u,int pa){
            son[u]=1;
            int res=0;
            for(int i=head[u];i!=-1;i=edges[i].next){
                int v=edges[i].to;
                if(v==pa) continue;
                if(vis[v]) continue;
                dfs(v,u); son[u]+=son[v];
                res=max(res,son[v]-1);
            }
            res=max(res,n-son[u]);
            if(res<siz){ ans=u; siz=res; }
        }
        int getCenter(int x){
            ans=0; siz=INF;
            dfs(x,-1);
            return ans;
        }
    }Cent;  //树的重心
    View Code

    法二:利用推论求出。

    随意确定一个根节点,先把无根树转化为有根树,dfs求出所有点的子树的节点个数。

    如果有一点满足该点的 子树的节点数的二倍 >= 总结点数 (size[u]*2>=n),

    并且该点儿子都满足 子树节点数二倍 <= 总结点数 (size[son_u]*2<=n),就是树的重心。

    #include<cstdio>
    #define N 42000
     
    int n,a,b,next[N],to[N],head[N],num,size[N],father[N],ans;
     
    void add(int false_from,int false_to){
        next[++num]=head[false_from];
        to[num]=false_to;
        head[false_from]=num;
    }
     
    void dfs(int x){
        size[x]=1;
        for(int i=head[x];i;i=next[i])
            if(father[x]!=to[i]){
                father[to[i]]=x;
                dfs(to[i]);
                size[x]+=size[to[i]];
            }
        if(size[x]*2>=n&&!ans)
            ans=x;
    }
     
    int main(){
        scanf("%d",&n);
        for(int i=1;i<n;++i){
            scanf("%d%d",&a,&b);
            add(a,b); add(b,a);
        }
        dfs(1);
        printf("%d",ans);
        return 0;
    }
    View Code

    【四. 图的连通块划分】

    1.连通块的定义

    连通块(连通图):无向图 G 中,若从顶点 i 到 j 有路径相连,则 i、j 是连通的。

    如果 G 是有向图,那么连接 i 和 j 的路径中所有的边都必须同向

    如果图中任意两点都是连通的,那么图被称作连通图(或连通块)。

    如果此图是有向图,则称为强连通图(注意:需要双向都有路径)。

    2.求图中连通块的个数

    深搜法:

    void dfs_num(int x){
        v[x]=cnt; //记录x点属于的连通块的编号
        for(int i=head[x];i;i=next[i]){
            int y=ver[i];
            if(v[y]) continue; //已经找到归属的连通块
            dfs_num(y);
        }
    }
     
    //在主程序 int main() 中:
    for(int i=1;i<=n;i++){
        if(!v[i]){
            cnt++; //无向图中包含连通块的个数
            dfs_num(i);
        }
    }

    并查集法:

    #include <stdio.h>
    #include <vector>
    using namespace std;
     
    const int maxn = 100010;
    vector<int> G[maxn]; // 邻接表存储图
     
    bool isRoot[maxn] = {false}; // 标记是否访问
    int pre[maxn];
     
    int Find(int x) {
        int r = x;
        while(x != pre[x])  x = pre[x];
        //↑↑路径压缩:此时x已经是老大
        int j; while(r != pre[r]) {
            j = r;
            r = pre[r];
            pre[j] = x;
        }
        return x;
    }
     
    void Union(int a, int b) {
        int preA = Find(a);
        int preB = Find(b);
        if(preA != preB) pre[preA] = preB;
    }
     
    int calculateBlockNum(int n) {
        int block = 0;
        for(int i = 1; i <= n; i++) // 枚举所有顶点
            isRoot[Find(i)] = true;// 这样同样的数字只计算一次
        for(int i = 1; i <= n; i++)
            block += isRoot[i]; //true当1用,false当0用
        return block;
    }
     
    void init(int n) {
        for(int i = 1; i <= n; i++)
            pre[i] = i;
    }
     
    int main() {
        int n, a, b; scanf("%d", &n);
        init(n); // 很重要很重要!
        for(int i = 1; i < n; i++) { // n - 1条边
            scanf("%d%d", &a, &b);
            G[a].push_back(b);
            G[b].push_back(a);
            Union(a,b); // 合并顶点a,b所在的集合
        }
        int block = calculateBlockNum(n);
        printf("%d
    ",block);
    }
    View Code

    在输入边的两个顶点时进行合并+路径压缩,统计pre数组中的元素有多少个不同值即可,

    为了统计,用到标记数组,这里没有统计每个块中的元素数目,所以用布尔型的数组即可。

                                                     ——时间划过风的轨迹,那个少年,还在等你。

  • 相关阅读:
    K近邻(K Nearest Neighbor-KNN)原理讲解及实现
    Bisecting KMeans (二分K均值)算法讲解及实现
    KMeans (K均值)算法讲解及实现
    NodeJs使用async让代码按顺序串行执行
    NodeJs递归删除非空文件夹
    NodeJs之配置文件管理
    NodeJs针对Express框架配置Mysql进行数据库操作
    在Express中使用Multiparty进行文件上传及POST、GET参数获取
    Linux操作命令
    SftpUtil FTP文件上传
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/9366109.html
Copyright © 2011-2022 走看看