文章目录
1 并查集、图相关算法
1.1 并查集
1.1.1 并查集基本结构和操作
1、有若干个样本a、b、c、d…类型假设是V
2、在并查集中一开始认为每个样本都在单独的集合里
3、用户可以在任何时候调用如下两个方法:
boolean isSameSet(V x, V y):查询样本x和样本y是否属于一个集合
void union(V x, V y):把x和y各自所在集合的所有样本合并成一个集合
4、isSameSet和union方法的代价越低越好,最好O(1)
思路:isSameSet方法,我们设计为每个元素有一个指向自己的指针,成为代表点。判断两个元素是否在一个集合中,分别调用这两个元素的向上指针,两个元素最上方的指针如果内存地址相同,那么两个元素在一个集合中,反之不在
思路:union方法,例如将a所在的集合和e所在的集合合并成一个大的集合union(a,e)。a的代表点指针是a,e的代表点指针是e,我们拿较小的集合挂在大的集合下面,比如e小,那么e放在a的下面。链接的方式为小集合e头结点本来指向自己的代表节点,现在要指向a节点
并查集的优化点主要有两个,一个是合并的时候小的集合挂在大的集合下面,第二个优化是找某节点最上方的代表节点,把沿途节点全部拍平,下次再找该沿途节点,都变为O(1)。两种优化的目的都是为了更少的遍历节点。
由于我们加入了优化,如果N个节点,我们调用findFather越频繁,我们的时间复杂度越低,因为第一次调用我们加入了优化。如果findFather调用接近N次或者远远超过N次,我们并查集的时间复杂度就是O(1)。该复杂度只需要记住结论,证明无须掌握。该证明从1964年一直研究到1989年,整整25年才得出证明!算法导论23章,英文版接近50页的证明。
package class10;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
public class Code01_UnionFind {
// 并查集结构中的节点类型
public static class Node<V> {
V value;
public Node(V v) {
value = v;
}
}
public static class UnionSet<V> {
// 记录样本到样本代表点的关系
public HashMap<V, Node<V>> nodes;
// 记录某节点到父亲节点的关系。
// 比如b指向a,c指向a,d指向a,a指向自身
// map中保存的a->a b->a c->a d->a
public HashMap<Node<V>, Node<V>> parents;
// 只有当前点,他是代表点,会在sizeMap中记录该代表点的连通个数
public HashMap<Node<V>, Integer> sizeMap;
// 初始化构造一批样本
public UnionSet(List<V> values) {
// 每个样本的V指向自身的代表节点
// 每个样本当前都是独立的,parent是自身
// 每个样本都是代表节点放入sizeMap
for (V cur : values) {
Node<V> node = new Node<>(cur);
nodes.put(cur, node);
parents.put(node, node);
sizeMap.put(node, 1);
}
}
// 从点cur开始,一直往上找,找到不能再往上的代表点,返回
// 通过把路径上所有节点指向最上方的代表节点,目的是把findFather优化成O(1)的
public Node<V> findFather(Node<V> cur) {
// 在找father的过程中,沿途所有节点加入当前容器,便于后面扁平化处理
Stack<Node<V>> path = new Stack<>();
// 当前节点的父亲不是指向自己,进行循环
while (cur != parents.get(cur)) {
path.push(cur);
cur = parents.get(cur);
}
// 循环结束,cur是最上的代表节点
// 把沿途所有节点拍平,都指向当前最上方的代表节点
while (!path.isEmpty()) {
parents.put(path.pop(), cur);
}
return cur;
}
// isSameSet方法
public boolean isSameSet(V a, V b) {
// 先检查a和b有没有登记
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return false;
}
// 比较a的最上的代表点和b最上的代表点
return findFather(nodes.get(a)) == findFather(nodes.get(b));
}
// union方法
public void union(V a, V b) {
// 先检查a和b有没有都登记过
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return;
}
// 找到a的最上面的代表点
Node<V> aHead = findFather(nodes.get(a));
// 找到b的最上面的代表点
Node<V> bHead = findFather(nodes.get(b));
// 只有两个最上代表点内存地址不相同,需要union
if (aHead != bHead) {
// 由于aHead和bHead都是代表点,那么在sizeMap里可以拿到大小
int aSetSize = sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
// 哪个小,哪个挂在下面
Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
Node<V> small = big == aHead ? bHead : aHead;
// 把小集合直接挂到大集合的最上面的代表节点下面
parents.put(small, big);
// 大集合的代表节点的size要吸收掉小集合的size
sizeMap.put(big, aSetSize + bSetSize);
// 把小的记录删除
sizeMap.remove(small);
}
}
}
}
并查集用来处理连通性的问题特别方便
1.1.2 例题
学生实例有三个属性,身份证信息,B站ID,Github的Id。我们认为,任何两个学生实例,只要身份证一样,或者B站ID一样,或者Github的Id一样,我们都算一个人。给定一打拼学生实例,输出有实质有几个人?
思路:把实例的三个属性建立三张映射表,每个实例去对比,某个实例属性在表中能查的到,需要联通该实例到之前保存该实例属性的头结点下
package class10;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;
public class Code07_MergeUsers {
public static class Node<V> {
V value;
public Node(V v) {
value = v;
}
}
public static class UnionSet<V> {
public HashMap<V, Node<V>> nodes;
public HashMap<Node<V>, Node<V>> parents;
public HashMap<Node<V>, Integer> sizeMap;
public UnionSet(List<V> values) {
for (V cur : values) {
Node<V> node = new Node<>(cur);
nodes.put(cur, node);
parents.put(node, node);
sizeMap.put(node, 1);
}
}
// 从点cur开始,一直往上找,找到不能再往上的代表点,返回
public Node<V> findFather(Node<V> cur) {
Stack<Node<V>> path = new Stack<>();
while (cur != parents.get(cur)) {
path.push(cur);
cur = parents.get(cur);
}
// cur头节点
while (!path.isEmpty()) {
parents.put(path.pop(), cur);
}
return cur;
}
public boolean isSameSet(V a, V b) {
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return false;
}
return findFather(nodes.get(a)) == findFather(nodes.get(b));
}
public void union(V a, V b) {
if (!nodes.containsKey(a) || !nodes.containsKey(b)) {
return;
}
Node<V> aHead = findFather(nodes.get(a));
Node<V> bHead = findFather(nodes.get(b));
if (aHead != bHead) {
int aSetSize = sizeMap.get(aHead);
int bSetSize = sizeMap.get(bHead);
Node<V> big = aSetSize >= bSetSize ? aHead : bHead;
Node<V> small = big == aHead ? bHead : aHead;
parents.put(small, big);
sizeMap.put(big, aSetSize + bSetSize);
sizeMap.remove(small);
}
}
public int getSetNum() {
//sizeMap 里面放着当前还有的集合以及对应的集合长度
return sizeMap.size();
}
}
public stat