• 数据结构-树与并查集


    前言

    ​ 这几天看了一些大学计算机的书籍,才发觉自己以前学的算法竞赛知识确实较为直白TAT且更偏向于应用,以致于有点犹豫要不要继续简单总结数据结构的知识了0.0。不过仔细想想这个系列可以当做整体知识框架与oi知识的复习,之后系统的读完那些“大部头”以后再写一份更加充实的内容总结感觉也不错qwq,所以打算接下来继续进行这个简单的数据结构的总结~。

    ​ 树是一个N个节点(还是结点?随便吧QAQ)和N-1条边(edge)的集合,其中的一个节点(node)叫做根(root)的连通无环图。N-1条边是因为除根节点外的每个节点都有一条边连到他们的父节点(parent)上,对于父节点来说他们叫做子节点(child)。

    ​ 另外几个定义:

    1. 一个节点的子节点总数叫做度(degree),没有子节点的节点叫做叶节点(leaf)。有相同父节点的节点叫做兄弟节点(siblings),类似的有祖父和孙子节点(= =)。

    2. 每个节点的深度(depth)指该节点到根节点唯一路径的长度(即边数,根节点为0),一个树的深度(也叫做高)表示所有节点深度的最大值。

    3. 任一节点v在通往树根沿途所经过的每个节点都是其祖先(ancestor),v是它们的后代(descendant)。

      特别地,v的祖先/后代包括其本身,而v本身以外的祖先/后代称作真祖先(proper ancestor)/真后代(proper descendant)。

    并查集

    ​ 并查集是一种处理不相交集合的合并和查询的树形数据结构。本质就是每个节点记录自己的父节点,当需要合并时将两个节点的集合(树)的根节点设置为父子关系,使两个集合合成一个集合(所有节点的根节点相同)。初始就是每个节点设置自己的父节点为自己。

    代码实现:

    int fa[N]; //每个节点的父节点 N为节点总数
    void init() {for(int i=1;i<=N;i++) fa[i]=i;} //初始化
    int getro(int u) {return fa[u]==u?u:getro(fa[u]);} //递归查询u的根节点(代表所在集合)
    void merge(int u,int v) { //合并集合
    	int ru=getro(u),rv=getro(v);
        fa[ru]=rv;
    }
    

    优化

    ​ 然而树的结构往往会因为数据不同而不同,在特殊的数据下最终构造的树会退化成一个链表(即一条链状),导致每次查询复杂度O(n),所以需要我们需要对其进行优化。

    按秩合并(启发式合并)

    ​ 可以发现getro()的复杂度实际上就是该节点的深度,所以在合并时尽量让小树接到大树上,使整体深度尽量降低

    代码实现:

    int fa[N],dep[N]; //fa每个节点的父节点 dep该树的深度[]内为根节点才有意义 N为节点总数
    void init() {for(int i=1;i<=N;i++) fa[i]=i,dep[i]=0;} //初始化
    int getro(int u) {return fa[u]==u?u:getro(fa[u]);} //递归查询u的根节点(代表所在集合)
    void merge(int u,int v) { //合并集合 小树接大树上,使深度尽量不增加
    	int ru=getro(u),rv=getro(v);
        if(dep[ru]>dep[rv]) fa[rv]=ru,dep[ru]=max(dep[ru],dep[rv]+1);
        else fa[ru]=rv,dep[rv]=max(dep[rv],dep[ru]+1);
    }
    

    路径压缩

    ​ 既然我们的目标是降低树的深度,还可以更直接的让每个节点在查询时直接把自己的父节点设为根节点,使下次查询时间复杂度O(1)

    代码实现:

    int fa[N];
    void init() {for(int i=1;i<=N;i++) fa[i]=i;}
    int getro(int u) {return fa[u]=fa[u]==u?u:getro(fa[u]);} //返回前修改fa的值
    void merge(int u,int v) {
    	int ru=getro(u),rv=getro(v);
        fa[ru]=rv;
    }
    

    ​ 然而这种做法在要求寻找路径时会无法使用,因为树的结构已经被破坏,变成根节点下一堆叶节点。

    带权并查集

    ​ 带权并查集一般指并查集中的点除parent外还保存一个权值用来帮助解决问题,具体操作需要结合问题进行设计,属于并查集的拓展运用。因为是具体问题具体分析就留到之后写题解再说。

    可持久化并查集

    ​ 观察merge操作可以发现每次只会改变一个fa的值,也就是说fa数组是一个每次单点修改其他不动的数组,很明显维护一个可持久化数组就实现了可持久化并查集qwq,而且同样可以路径压缩或按秩合并。那么就用主席树来实现一下按秩合并的可持久化并查集(测试代码题目为洛谷模板题):

    #define mid ((l+r)>>1)
    void build(int &cur,int l,int r) { //正常build
        cur=++cnt; 
        if(l==r) {fa[cur]=l;return ;}
        build(ls[cur],l,mid); build(rs[cur],mid+1,r);
    }
    void merge(int las,int &cur,int l,int r,int u,int v) { //写的是merge其实只是让u的父节点(并查集上)变为v
        cur=++cnt; ls[cur]=ls[las]; rs[cur]=rs[las];
        if(l==r) fa[cur]=v,dep[cur]=dep[las];
        else if(u<=mid) merge(ls[las],ls[cur],l,mid,u,v); //注意las和cur跟着一起跳
        else merge(rs[las],rs[cur],mid+1,r,u,v);
    }
    void update(int cur,int l,int r,int u) { //使dep[]加1
        if(l==r) dep[cur]++;
        else if(u<=mid) update(ls[cur],l,mid,u);
        else update(rs[cur],mid+1,r,u);
    }
    int getnm(int cur,int l,int r,int u) { //获取u代表节点的序号
        if(l==r) return cur;
        else if(u<=mid) return getnm(ls[cur],l,mid,u);
        else return getnm(rs[cur],mid+1,r,u);   
    }
    int getro(int rt,int u) { //找根节点
        int num=getnm(rt,1,n,u);
        return fa[num]==u?num:getro(rt,fa[num]); //注意这里返回的是num,因为对应并查集节点可以由fa[num]得到
    }
    //按秩合并 一点改变下面有解释
    int u=read(),v=read();
    int ru=getro(root[i-1],u),rv=getro(root[i-1],v);
    if(dep[ru]>dep[rv]) swap(ru,rv);
    merge(root[i-1],root[i],1,n,fa[ru],fa[rv]);
    if(dep[ru]==dep[rv]) update(root[i],1,n,fa[rv]);
    //查询是否为同一集合
    int u=read(),v=read();
    root[i]=root[i-1]; //题目要求:需要保存查询后版本(即上一个操作后版本= =)
    int ru=getro(root[i],u),rv=getro(root[i],v); 
    if(ru==rv) cout<<1<<endl;//是同一个集合 这里的ru和rv是两并查集根节点的序号
    else cout<<0<<endl;//不是同一个集合
    //更改版本
    root[i]=root[i-1];
    

    稍作解释:可以发现这回fa[]、dep[]为了实现可持久化对应的值不是并查集节点而是主席树节点序号,这就导致我们要修改他们需要用logn的时间寻找某个并查集节点在当前版本的序号。这个变化体现在代码思路变化上主要有以下几点:

    1. 放弃在合并时同时更新dep,因为前面的dep[v]现在是dep[getnm(rt,1,n,v)],需要logn的时间所以改为只有深度相同时(事实上也只当这个时候)才增加深度。
    2. 找并查集根节点时返回的是根节点对应主席树节点序号,因为fa[序号]=根节点,而根节点找序号需要logn时间,所以返回序号更方便。

    完整版本代码传送门

    后记

    简单的并查集搞完了,有点担心之后的平衡树会不会耗太多时间解释我很清楚的东西(⊙…⊙)(但不解释又感觉总结不完整QAQ)。总之今天的并查集算是比较完整的完成了~(带权并查集感觉只能讲题┐(゚~゚)┌ )

    PS: 写完这篇提交后出事了,因为我放的图片比较多导致博客文件超过了coding的128M限制无法部署= =,而这个博客很多地方都会用到图片......现在只能尽量减少图片数量了,之后还是得想想别的办法Orz

    本文版权归作者所有,未经允许不得转载。
  • 相关阅读:
    实验5 编写调试有多个段的程序
    实验四 [bx]和 loop 的使用
    实验三
    实验二
    第一章
    汇编语言第二章知识梳理
    实验一:查看CPU和内存,用机器指令和汇编指令编程
    实验9
    实验5
    实验4:
  • 原文地址:https://www.cnblogs.com/zuiyumeng/p/13545849.html
Copyright © 2020-2023  润新知