算法_最小生成树

最小生成树

无向图的连通分量和生成树

​ 对于无向图进行遍历(DFS或BFS)的时候,若是连通图,仅需一次遍历过程就可访问到图中所有的点;若是非连通图,需要多次调用遍历过程,每次调用得到的点的集合和边的集合就构成一个连通分量。

​ 一个连通图的生成树是该连通图的一个极小连通子图,它含有该图所有的顶点,只有构成一棵树的(n-1)条边。如果在一棵生成树上添加一条边,则必定构成环,因为这条边使得依附的两个顶点之间有了第二条路径。

一棵有n个点的生成树,有且仅有(n-1)条边,但是有(n-1)条边的图不一定都是生成树。

一个图有n个顶点,如果它的边小于(n-1),则是非连通图。如果多于(n-1)条边,则一定有回路。

​ 按照上述生成树的定义,最小生成树的准则有下:

  • 必须使用该图中的边来构成最小生成树
  • 不许使用且仅使用(n-1)条边来连接图中的n个点
  • 不能使用产生回路的边

Prim算法

​ 普里姆(Prim)算法是一种构造性的贪心算法。假设G=(V,E)是一个具有n个点的带权连通无向图,T=(U,TE)是G的最小生成树,其中U是T的顶点集,TE是T的边集,则由G构造从起点v出发的最小生成树T的步骤如下:

  1. 初始化U=(v),以v到其他顶点的所有边为候选边。
  2. 重复以下步骤(n-1)次,使其他的(n-1)个点被加入到U中:
    1. 从候选边中挑选权重最小的边加入TE,该边在V-U中的顶点时k,将k加入到U;
    2. 判断当前的V-U中的所有顶点j,修改候选边,若边(k,j)的权重值小于原来的顶点j关联的候选边,则用(k,j)取代后者作为候选边。

流程如下:

​ 图G如下图:

图1

对应的邻接矩阵如下:

012345
00615**
1605*3*
2150564
35*50*2
4*36*06
5**4260

步骤如下:

图2

最小边为(0,2),对应的权重为1.

图3

最小边为(2,5),对应的权重为4.

图4

最小边为(3,5),对应的权重为2.

图5

最小边为(1,2),对应的权重为5.

图6

最小边为(1,4),对应的权重为3.

代码如下:

public static void prim(int[][] graph, int startPoint) {

  int[] lowcost = new int[graph.length];
  int[] closest = new int[graph.length];
  int minDis = max;

  for (int i = 0; i < lowcost.length; i++) {//lowcost和closest的初始值
    lowcost[i] = graph[startPoint][i];
    closest[i] = startPoint;
  }

  for (int i = 0; i < lowcost.length - 1; i++) {//除了startPoint之外的n-1个点
    minDis = max;
    int index = startPoint;
    for (int j = 0; j < lowcost.length; j++) {
      if (lowcost[j] != 0 && lowcost[j] < minDis) {
        minDis = lowcost[j];
        index = j;
      }
    }
    System.out.println("边:(" + closest[index] + "," + index + "),权值为:" + minDis);
    lowcost[index] = 0;
    for (int j = 0; j < lowcost.length; j++) {//更新lowcost和closest
      if (graph[index][j] != 0 && graph[index][j] < lowcost[j]) {
        lowcost[j] = graph[index][j];
        closest[j] = index;
      }
    }
  }
}

Kurskal算法

​ 克鲁斯卡尔(Kurakal)算法是一种按照权重的递增次序选择合适的边来构造最小生成树的方法,同样也是贪心的策略。

​ 图G(V,E)是一个有n个点的带权无向图,构造最小生成树T(U,TE)的步骤如下:

  1. 置U的初始值等于V,即包含G中的所有点,TE的初始值为空。即图T中的所有点构成一个分量。
  2. 将图G中的边按照权值从小到大依次选取,如果选取的边没有让图T构成环路,则加入TE,否则舍弃。直至TE中有n-1条边。

流程如下:

​ 图G如下图:

图1

上图中每条边按照从下到大的排列如下:

{0,2,1},{2,0,1},{3,5,2},{5,3,2},{1,4,3},{4,1,3},{2,5,3},

{5,2,4},{1,2,5},{2,1,5},{0,3,5},{3,0,5},{2,3,5},{3,2,5},

{0,1,6},{1,0,6},{2,4,6},{4,2,6},{4,5,6},{5,4,6}

每条边的是那个数组分别代表,起点,终点,权值。

​ Kurakal算法的关键是判断所取的边是否与生成树中已经存在的边形成回路,可以通过这条边对应的两个点所在的连通分量来解决。设置一个辅助数组vset,初始时vset[i] = i;代表每个点所属的连通子图的编号。新增一条边的时候,如果这条边对应的两个不连通,将两个点集合按照其中的一个重新统一编号。当两个点的编号不同时,加入这条边一定不会形成回路。

步骤如下:

图7

加入边(0,2),修改点2对应的连通子图的编号为0.

图8

加入边(3,5),修改点5对应的连通子图的编号为3.

图9

加入边(1,4),修改点4对应的连通子图的编号为1.

图10

加入边(2,5),修改点5对应的连通子图的编号为0.

图11

加入边(1,2),修改点2对应的连通子图的编号为1.

代码如下:

public class Demo {
  private static int max = Integer.MAX_VALUE;

  static class Edge implements Comparable<Edge> {
    private int start;//边的起点
    private int end;//边的终点
    private int w;//权重值

    public int getStart() {
      return start;
    }

    public void setStart(int start) {
      this.start = start;
    }

    public int getEnd() {
      return end;
    }

    public void setEnd(int end) {
      this.end = end;
    }

    public int getW() {
      return w;
    }

    public void setW(int w) {
      this.w = w;
    }

    @Override
    public int compareTo(Edge edge) {
      return this.getW() - edge.getW();
    }
  }

  public static void kruskal(int[][] graph) {
    List<Edge> edges = new ArrayList<Edge>();
    int[] vset = new int[graph.length];
    for (int i = 0; i < graph.length; i++) {//存放所有的边
      for (int j = 0; j < graph.length; j++) {
        if (graph[i][j] != 0 && graph[i][j] != max) {
          Edge edge = new Edge();
          edge.setStart(i);
          edge.setEnd(j);
          edge.setW(graph[i][j]);
          edges.add(edge);
        }
      }
      vset[i] = i;
    }
    Collections.sort(edges);//排序
    for (int i = 0, index = 0; i < graph.length - 1; index++) {//n-1条边
      Edge edge = edges.get(index);
      if (vset[edge.getStart()] != vset[edge.getEnd()]) {//对应边的两个点是否属于同一连通分量
        i++;
        System.out.println("起点:" + edge.getStart() + ",终点:" + edge.getEnd() + ",权重:" + edge.getW());
        for (int j = 0; j < graph.length; j++) {//统一两个集合的编号
          if (vset[j] == vset[edge.getEnd()]) {
            vset[j] = vset[edge.getStart()];
          }
        }
      }
    }
  }

  public static void main(String[] args) {
    int[][] graph = {{0, 6, 1, 5, max, max},
                     {6, 0, 5, max, 3, max},
                     {1, 5, 0, 5, 6, 4},
                     {5, max, 5, 0, max, 2},
                     {max, 3, 6, max, 0, 6},
                     {max, max, 4, 2, 6, 0}};
    kruskal(graph);
  }
}

例子

POJ 1789.

POJ 2485.

POJ 1258.

POJ 3026.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值