Kruskal算法一般用来解决最小生成树问题
其中用到并查集。并查集真是个很有用的东西,之前觉得写一个并查集代码太多了,现在看这个方法其实就很容易感觉到并查集在理解题意和解决方面的优点
因为就写了练了两道最小生成树的题,就记录一下简单想法和理解
class DisjoinSetUnion{
int[] f;//f[i] 表示点 i 的父节点。
int[] rank;//用于按秩合并,帮助保持树的扁平化以优化查找操作。
int n;
public DisjoinSetUnion(int n){
this.n = n;
this.rank = new int[n];
Arrays.fill(this.rank,1);
this.f = new int[n];
for(int i = 0;i<n;i++){
this.f[i] = i;//初始化 f 数组,使每个点的父节点为其自身(自环),并将所有的秩初始化 为 1。
}
}
public int find(int x){
while(x!=f[x]){
f[x] = f[f[x]];
x = f[x];
}
return x;
}
public boolean unionSet(int x, int y){
int fx = find(x),fy =find(y);
if(fx==fy) return false;
if(rank[fx] < rank[fy]){
int temp = fx;
fx = fy;
fy = temp;
}
rank[fx] += rank[fy];
f[fy] = fx;
return true;
}
}
先写出并查集经典模板,主要就是找爹和合并爹(寻找父节点和合并父节点)都是同样的套路。
int n = points.length;
DisjoinSetUnion dsu = new DisjoinSetUnion(n);
List<Edge> edges = new ArrayList<Edge>();
for(int i = 0;i<n;i++){
for(int j = i+1;j<n;j++){
edges.add(new Edge(dist(points,i,j),i,j));
}
}
Collections.sort(edges, new Comparator<Edge>(){
public int compare(Edge edge1,Edge edge2){
return edge1.len - edge2.len;
}
});
根据题意创建边并排序边
- 利用
Collections.sort
和自定义比较器,将边按长度升序排序。这是构建最小生成树(MST)时必要的步骤,以确保首先处理最短的边。
int ret = 0,num = 1;
for(Edge edge : edges){
int len = edge.len, x = edge.x, y =edge.y;
if(dsu.unionSet(x,y)){
ret+=len;
num++;
if(num==n){
break;
}
}
}
从最短的边开始一步一步合并边,如果合并到所有节点都包含后就可以退出
- 遍历每条边,调用
unionSet
方法检查这两点是否属于不同的组件(即它们未连接)。如果它们不在同一个组件中,就将这条边加入到 MST 中,并增加总成本ret
。 - 变量
num
用于跟踪已连接的点的数量。一旦所有点都连接(num
达到n
),就终止循环。
同样先写并查集模板,这里多了个setCount用来检验是否找到有效的MST(也就是 setCount
最终为 1)
//并查集模板
class UnionFind {
int[] parent;
int[] size;
int n;
//当前连通分量数目
int setCount;
public UnionFind(int n){
this.n = n;
this.setCount = n;
this.parent = new int[n];
this.size = new int[n];
Arrays.fill(size,1);
for(int i =0;i<n;i++){
this.parent[i]= i;
}
}
public int findset(int x){
return parent[x]==x? x: (parent[x]=findset(parent[x]));
}
public boolean unite(int x, int y){
x = findset(x);
y = findset(y);
if(x== y){
return false;
}
if(size[x]<size[y]){
int temp = x;
x = y;
y = temp;
}
parent[y] = x;
size[x]+=size[y];
--setCount;
return true;
}
public boolean connected(int x, int y){
x = findset(x);
y = findset(y);
return x==y;
}
}
然后主程序中先进行边的重构和排序
-
新数组
newEdges
:- 通过一个新的二维数组,我们将原始边的信息扩展到四个部分:
[u, v, weight, index]
。 index
记录当前边在原始边数组中的位置,后续用于返回结果。
- 通过一个新的二维数组,我们将原始边的信息扩展到四个部分:
-
排序:
- 使用
Arrays.sort
对newEdges
按照权重进行升序排序,为应用 Kruskal 算法做准备。
- 使用
int m = edges.length;
int[][] newEdges = new int[m][4];
for (int i = 0; i < m; ++i) {
for (int j = 0; j < 3; j++) {
newEdges[i][j] = edges[i][j];
}
newEdges[i][3] = i; // 将边的索引加入新数组
}
Arrays.sort(newEdges, Comparator.comparingInt(a -> a[2]));
接受答案的容器
List<List<Integer>> ans = new ArrayList<List<Integer>>();
for (int i = 0; i < 2; ++i) {
ans.add(new ArrayList<Integer>());
}
先计算出MST的权重,这部分是经典的Kruskal算法
- 计算总权重
value
:- 遍历排序后的边,利用
unite
方法连接边的两个端点:- 如果连接成功,累加边的权重到
value
中。 - 当遍历完所有边后,
value
保存了当前构造出的 MST 的总权重。
- 如果连接成功,累加边的权重到
- 遍历排序后的边,利用
UnionFind ufStd = new UnionFind(n);
int value = 0;
for (int i = 0; i < m; i++) {
if (ufStd.unite(newEdges[i][0], newEdges[i][1])) {
value += newEdges[i][2];
}
}
然后就是本题中判断关键边和伪关键边的部分,其实就是判断去掉某条边结果变不变;
-
初始并查集重建:
- 对于每一条边(
i
),我们创建一个新的并查集实例,准备判断当前边是否为关键边。
- 对于每一条边(
-
遍历边判断关键边:
- 遍历所有边,看看在不选中当前边的情况下能否构建出一个生成树。
if (uf.setCount != 1 || (uf.setCount == 1 && v > value))
:- 如果最终得到的组件数量不为 1,或者生成树的权重超过了原始的 MST 权重,则当前边是关键边,索引被添加到
ans.get(0)
。
- 如果最终得到的组件数量不为 1,或者生成树的权重超过了原始的 MST 权重,则当前边是关键边,索引被添加到
-
判断伪关键边:
- 重新初始化并查集,并强制将当前边(
newEdges[i][0]
和newEdges[i][1]
)放入生成树中,然后再遍历其他所有边。 - 如果最终生成的构造的 MST 权重与原始的
value
相同,则当前边是伪关键边,索引添加到ans.get(1)
。
- 重新初始化并查集,并强制将当前边(
List<List<Integer>> ans = new ArrayList<>();
for (int i = 0; i < 2; ++i) {
ans.add(new ArrayList<>());
}
for (int i = 0; i < m; i++) {
UnionFind uf = new UnionFind(n);
int v = 0;
for (int j = 0; j < m; ++j) {
if (i != j && uf.unite(newEdges[j][0], newEdges[j][1])) {
v += newEdges[j][2];
}
}
// 判断是否是关键边
if (uf.setCount != 1 || (uf.setCount == 1 && v > value)) {
ans.get(0).add(newEdges[i][3]);
continue;
}
// 判断是否是伪关键边
uf = new UnionFind(n);
uf.unite(newEdges[i][0], newEdges[i][1]);
v = newEdges[i][2];
for (int j = 0; j < m; ++j) {
if (i != j && uf.unite(newEdges[j][0], newEdges[j][1])) {
v += newEdges[j][2];
}
}
if (v == value) {
ans.get(1).add(newEdges[i][3]);
}
}