代码随想录算法训练营第六十八天 | 并查集理论基础,107.寻找存在的路径,108.冗余连接

并查集理论基础

题目讲解:代码随想录

背景

  • 并查集常用来解决连通性问题
  • 并查集主要有两个功能:
    1.将两个元素添加到一个集合中。
    2.判断两个元素在不在同一个集合

原理讲解

  • 如何将两个元素添加到同一个集合中呢?
    我们将三个元素A,B,C放在同一个集合,其实就是将三个元素连通在一起。只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。
// 将v,u 这条边加入并查集
void join(int u, int v) {
   u = find(u); // 寻找u的根
   v = find(v); // 寻找v的根
   if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
   father[v] = u;
}
  • 只要 A ,B,C 在同一个根下就是同一个集合。
  • find函数是如何实现的呢?
    通过数组下标找到数组元素,一层一层寻根过程
// 并查集里寻根的过程
int find(int u) {
   if (u == father[u]) return u; // 如果根就是自己,直接返回
   else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找
}
  • father数组初始化的时候要 father[i] = i
    我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。
// 并查集初始化
void init() {
   for (int i = 0; i < n; ++i) {
       father[i] = i;
   }
}
  • 如何判断两个元素是否在同一个集合里?
    通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
   u = find(u);
   v = find(v);
   return u == v;
}

路径压缩

  • find 函数,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。
  • 路径压缩:将非根节点的所有节点直接指向根节点
    在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果
// 并查集里寻根的过程,路径压缩
int find(int u) {
   if (u == father[u]) return u;
   else return father[u] = find(father[u]); // 路径压缩
}

代码模板

  • 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个
  • 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上
  • 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
// 并查集初始化
void init() {
   for (int i = 0; i < n; ++i) {
       father[i] = i;
   }
}
// 并查集里寻根的过程
int find(int u) {
   return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
   u = find(u);
   v = find(v);
   return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
   u = find(u); // 寻找u的根
   v = find(v); // 寻找v的根
   if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回
   father[v] = u;
}

常见误区

  • join里不能调用isSame来精简。
join(1, 2);
join(3, 2);
  • 如果join里调用isSame而不是find,生成的图如下:
    在这里插入图片描述
    此时调用 isSame(1, 3),返回false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期
  • 如果join里调用的是find,生成的图如下:
    在这里插入图片描述
    此时调用 isSame(1, 3),find(1) 返回的是3,find(3) 返回的也是3,返回true,即告诉我们 元素 1 和 元素3 是 在同一个集合里的。

模拟过程

  1. join(1, 8);
    在这里插入图片描述
  2. join(3, 8);
    在这里插入图片描述
    为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3?
    因为在join(int u, int v)函数里 要分别对 u 和 v 寻根之后再把根进行关联。
  3. join(1, 7);
    在这里插入图片描述
  4. join(8, 5);
    在这里插入图片描述
    为什么 图中 8 又直接指向了 3 了呢?
    因为在join调用find,find里路经压缩了
  5. join(6, 9);
    在这里插入图片描述
  • isSame(8, 7):true
  • isSame(7, 2):false

拓展

  • 按秩(rank)合并
int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好
vector<int> father = vector<int> (n, 0); // C++里的一种数组结构
vector<int> rank = vector<int> (n, 1); // 初始每棵树的高度都为1
// 并查集初始化
void init() {
   for (int i = 0; i < n; ++i) {
       father[i] = i;
       rank[i] = 1; // 也可以不写
   }
}
// 并查集里寻根的过程
int find(int u) {
   return u == father[u] ? u : find(father[u]);// 注意这里不做路径压缩
}
// 判断 u 和 v是否找到同一个根
bool isSame(int u, int v) {
   u = find(u);
   v = find(v);
   return u == v;
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
   u = find(u); // 寻找u的根
   v = find(v); // 寻找v的根
   if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树
   else father[v] = u;
   if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <=
}
  • 我们在优化并查集查询效率的时候,只用路径压缩的思路就够了,不仅代码实现精简,而且效率足够高。按秩合并的思路并没有将树形结构尽可能的扁平化,所以在整理效率上是没有路径压缩高的。
  • 无论使用并查集模板里哪一个函数(除了init函数),都会有路径压缩的过程,第二次访问相同节点的时候,这个节点就是直连根节点的,即 第一次访问的时候它的路径就被压缩了。

复杂度分析

  • 空间复杂度: O(n) ,申请一个father数组。
  • 时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。

107. 寻找存在的路径

题目讲解:代码随想录
重点:

  1. 并查集的代码模板理解并记住

思路:

  1. 初始化并查集
private static int[] father;
private static void init(int n) {
   father = new int[n + 1];
   for (int i = 0; i < n; i++) {
       father[i + 1] = i + 1;
   }
}
  1. 完成find函数并进行路径压缩,方便后面的join和isSame
private static int find(int u) {
   if (u == father[u]) return u;
   return father[u] = find(father[u]);
}
  1. 把scanner获取到的边用join连接
private static void join(int u, int v) {
   u = find(u);
   v = find(v);
   if (u == v) return;
   father[v] = u;
}
for (int i = 0; i < m; i++) {
   int s = scanner.nextInt();
   int t = scanner.nextInt();
   join(t, s);
}
  1. 用isSame判断是否在同一集合中,在同一集合中的节点都是连通的
private static boolean isSame(int u, int v) {
   u = find(u);
   v = find(v);
   return u == v;
}
if (isSame(source, destination)) System.out.println(1);
else System.out.println(0);
public class FindPathOfExistence {
    // 并查集数组
    private static int[] father;
    // 初始化并查集数组
    private static void init(int n) {
        father = new int[n + 1];
        for (int i = 0; i < n; i++) {
            father[i + 1] = i + 1;
        }
    }
    // 寻根, 并进行路径压缩方便第二次及之后的查找
    private static int find(int u) {
        if (u == father[u]) return u;
        return father[u] = find(father[u]);
    }
    // 把u和v的根连接起来
    private static void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return;
        father[v] = u;
    }
    // 判断u和v是否在同一集合中
    private static boolean isSame(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        // 初始化
        init(n);
        // 连接边
        for (int i = 0; i < m; i++) {
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            join(t, s);
        }
        // 判断是否连接
        int source = scanner.nextInt();
        int destination = scanner.nextInt();
        if (isSame(source, destination)) System.out.println(1);
        else System.out.println(0);
    }
}

108. 冗余连接

题目讲解:代码随想录
重点:

  1. 理解题目是在树的基础上添加一条边,所以冗余边仅仅是一条。

思路:

  1. 初始化并查集数组
private static int[] father;
private static void init(int n) {
   father = new int[n + 1];
   for (int i = 0; i< n + 1; i++) father[i] = i;
}
init(n);
  1. 判断当前要加入的边的起点和终点是否已经连通了。如果已经连通了,则是冗余边。如果没有连通,则执行join
private static int find(int u) {
   if (father[u] == u) return u;
   return father[u] = find(father[u]);
}
private static void join(int u, int v) {
   u = find(u);
   v = find(v);
   if (u == v) return;
   father[v] = u;
}
private static boolean isSame(int u, int v) {
   u = find(u);
   v = find(v);
   return u == v;
}
if (isSame(s, t)) {
   System.out.print(s + " ");
   System.out.println(t);
} else join(t, s);
public class RedundantConnection {
    // 并查集数组
    private static int[] father;
    // 初始化并查集数组
    private static void init(int n) {
        father = new int[n + 1];
        for (int i = 0; i< n + 1; i++) {
            father[i] = i;
        }
    }
    // 寻根, 并路径压缩
    private static int find(int u) {
        if (father[u] == u) return u;
        return father[u] = find(father[u]);
    }
    // 连接u和v
    private static void join(int u, int v) {
        u = find(u);
        v = find(v);
        if (u == v) return;
        father[v] = u;
    }
    // 判断u和v是否连通
    private static boolean isSame(int u, int v) {
        u = find(u);
        v = find(v);
        return u == v;
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        init(n);
        for (int i = 0; i < n; i++) {
            int s = scanner.nextInt();
            int t = scanner.nextInt();
            // 如果已经连通, 说明再加入当前边会形成环, 直接输出
            if (isSame(s, t)) {
                System.out.print(s + " ");
                System.out.println(t);
            } else {
                join(t, s);
            }
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值