126 阅读2分钟

图结构

public class Edge {
   public int weight; // 边的权重
   public Node from;  // 开始节点
   public Node to; // 结束节点

   public Edge(int weight, Node from, Node to) {
      this.weight = weight;
      this.from = from;
      this.to = to;
   }

}
public class Node {
   public int value; // 节点的值
   public int in;  // 入度
   public int out; // 出度
   public ArrayList<Node> nexts; // 邻居点
   public ArrayList<Edge> edges; // 以这个点开头的边

   public Node(int value) {
      this.value = value;
      in = 0;
      out = 0;
      nexts = new ArrayList<>();
      edges = new ArrayList<>();
   }
}
// 边和点的集合,就是图
public class Graph {
   public HashMap<Integer, Node> nodes;
   public HashSet<Edge> edges;
   
   public Graph() {
      nodes = new HashMap<>();
      edges = new HashSet<>();
   }
}

BFS

  1. 利用队列实现
  2. 从源节点开始依次按照宽度进队列,然后弹出
  3. 每弹出一个点,把该节点所有没有进过队列的邻接点放入队列
  4. 直到队列变空
// 从node出发,进行宽度优先遍历
public static void bfs(Node start) {
   if (start == null) {
      return;
   }
   Queue<Node> queue = new LinkedList<>();
   HashSet<Node> set = new HashSet<>();
   queue.add(start);
   set.add(start); // 防止遍历过的节点重复进队列
   while (!queue.isEmpty()) {
      Node cur = queue.poll();
      System.out.println(cur.value);
      for (Node next : cur.nexts) {
         if (!set.contains(next)) {
            set.add(next);
            queue.add(next);
         }
      }
   }
}

DFS

  1. 利用栈实现
  2. 从源节点开始把节点按照深度放入栈,然后弹出
  3. 每弹出一个点,把该节点下一个没有进过栈的邻接点放入栈
  4. 直到栈变空
   // 一直遍历到最深的节点,然后往回走,如果走到的节点的nexts中,还有未遍历过的,就继续遍历nexts
public static void dfs(Node node) {
   if (node == null) {
      return;
   }
   Stack<Node> stack = new Stack<>();
   HashSet<Node> set = new HashSet<>();
   stack.add(node);
   set.add(node);
   System.out.println(node.value); // 第一个节点先遍历
   while (!stack.isEmpty()) {
      Node cur = stack.pop();
      for (Node next : cur.nexts) { // 没下一个节点了,或者nexts都遍历过了,跳出循环
         if (!set.contains(next)) { // 哪个没遍历,哪个入栈
            stack.push(cur);  // 刚刚弹出来的节点要重新入栈
            stack.push(next); 
            set.add(next);
            System.out.println(next.value); // 进栈时候打印
            break; // 只入栈一个节点最多
         }
      }
   }

拓扑排序

  1. 在图中找到所有入度为0的点输出
  2. 把所有入度为0的点在图中删掉,继续找入度为0的点输出,周而复始
  3. 图的所有点都被删除后,依次输出的顺序就是拓扑排序
  4. 要求:有向图且其中没有环
public static List<Node> sortedTopology(Graph graph) {
   HashMap<Node, Integer> inMap = new HashMap<>(); // key 某个节点   value 剩余的入度
   Queue<Node> zeroInQueue = new LinkedList<>();// 只有剩余入度为0的点,才进入这个队列
   for (Node node : graph.nodes.values()) { // 建立节点 入度的映射关系,同时把所有入度为0的点加入队列
      inMap.put(node, node.in);
      if (node.in == 0) {
         zeroInQueue.add(node);
      }
   }
   List<Node> result = new ArrayList<>();
   while (!zeroInQueue.isEmpty()) {
      Node cur = zeroInQueue.poll();
      result.add(cur);
      for (Node next : cur.nexts) {
         inMap.put(next, inMap.get(next) - 1);// nexts所有入度减1
         if (inMap.get(next) == 0) {
            zeroInQueue.add(next);
         }
      }
   }
   return result;
}

Kruskal 算法

  1. 要求:有向图且其中没有环
  2. 总是从权值最小的边开始考虑,依次考察权值依次变大的边
  3. 当前的边要么进入最小生成树的集合,要么丢弃
  4. 如果当前的边进入最小生成树的集合中不会形成环,就要当前边
  5. 如果当前的边进入最小生成树的集合中会形成环,就不要当前边
  6. 考察完所有边之后,最小生成树的集合也得到了
public static class UnionFind {
   // key 某一个节点, value key节点往上的节点
   private HashMap<Node, Node> fatherMap;
   // key 某一个集合的代表节点, value key所在集合的节点个数
   private HashMap<Node, Integer> sizeMap;

   public UnionFind() {
      fatherMap = new HashMap<Node, Node>();
      sizeMap = new HashMap<Node, Integer>();
   }
   
   public void makeSets(Collection<Node> nodes) {
      fatherMap.clear();
      sizeMap.clear();
      for (Node node : nodes) {
         fatherMap.put(node, node);
         sizeMap.put(node, 1);
      }
   }

   private Node findFather(Node n) {
      Stack<Node> path = new Stack<>();
      while(n != fatherMap.get(n)) {
         path.add(n);
         n = fatherMap.get(n);
      }
      while(!path.isEmpty()) {
         fatherMap.put(path.pop(), n);
      }
      return n;
   }

   public boolean isSameSet(Node a, Node b) {
      return findFather(a) == findFather(b);
   }

   public void union(Node a, Node b) {
      if (a == null || b == null) {
         return;
      }
      Node aDai = findFather(a);
      Node bDai = findFather(b);
      if (aDai != bDai) {
         int aSetSize = sizeMap.get(aDai);
         int bSetSize = sizeMap.get(bDai);
         if (aSetSize <= bSetSize) {
            fatherMap.put(aDai, bDai);
            sizeMap.put(bDai, aSetSize + bSetSize);
            sizeMap.remove(aDai);
         } else {
            fatherMap.put(bDai, aDai);
            sizeMap.put(aDai, aSetSize + bSetSize);
            sizeMap.remove(bDai);
         }
      }
   }
}


public static class EdgeComparator implements Comparator<Edge> {

   @Override
   public int compare(Edge o1, Edge o2) {
      return o1.weight - o2.weight;
   }

}

// 给边按照权重比较,放小根堆.每次从小根堆里弹出一个边,检查这个边加上后能不能构成环,不能就算是答案
public static Set<Edge> kruskalMST(Graph graph) {
   UnionFind unionFind = new UnionFind();
   unionFind.makeSets(graph.nodes.values()); // 并查集初始化,所有节点的爹都是自己,同时size为1
   PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
   for (Edge edge : graph.edges) { // M 条边
      priorityQueue.add(edge);  // O(logM)
   }
   Set<Edge> result = new HashSet<>();
   while (!priorityQueue.isEmpty()) { // 边按照权重从小到大取出
      Edge edge = priorityQueue.poll(); // O(logM)
      if (!unionFind.isSameSet(edge.from, edge.to)) { // 边的起点和终点在同一个集合,说明有环
         result.add(edge); // 无环就符合
         unionFind.union(edge.from, edge.to);
      }
   }
   return result;
}

Prim 算法

  1. 可以从任意节点出发来寻找最小生成树
  2. 某个点加入到被选取的点中后,解锁这个点出发的所有新的边
  3. 在所有解锁的边中选最小的边,然后看看这个边会不会形成环
  4. 如果会,不要当前边,继续考察剩下解锁的边中最小的边,重复3)
  5. 如果不会,要当前边,将该边的指向点加入到被选取的点中,重复2)
  6. 当所有点都被选取,最小生成树就得到了
public static Set<Edge> primMST(Graph graph) {
   // 解锁的边进入小根堆
   PriorityQueue<Edge> priorityQueue = new PriorityQueue<>(new EdgeComparator());
   // 哪些点被解锁出来了
   HashSet<Node> nodeSet = new HashSet<>();
   Set<Edge> result = new HashSet<>(); // 依次挑选的的边在result里
   for (Node node : graph.nodes.values()) { // 随便挑了一个点
      // node 是开始点
      if (!nodeSet.contains(node)) {
         nodeSet.add(node); // 先解锁一个点
         for (Edge edge : node.edges) { // 由一个点,解锁所有相连的边
            priorityQueue.add(edge);
         }
         while (!priorityQueue.isEmpty()) { // 开始正式循环
            Edge edge = priorityQueue.poll(); // 弹出解锁的边中,最小的边
            Node toNode = edge.to; // 可能的被解锁的点
            if (!nodeSet.contains(toNode)) { // 不含有的时候,就是新的点
               nodeSet.add(toNode); // 解锁这个点
               result.add(edge);
               for (Edge nextEdge : toNode.edges) { // 继续解锁这个点下面的所有边
                  priorityQueue.add(nextEdge);
               }
            }
         }
      }
      // break;
   }
   return result;
}