图的基本算法

图的相关的基本算法

主要用来记录和图相关的一些经常用到的算法

DFS与BFS

leetcode 207 for example

DFS

class Soution{
	public boolean canFinish(int numCourses,int[][] prerequisites){
		// inital adj graph 
		List<List<Integer>> adjEdge = new ArrayList<>();
		// inital queue
		for(int i = 0;i<numCourses;i++) adjEdge.add(new ArrayList<Integer>());
		for(int[] info:prerequisites) adjEdge.get(info[1]).add(info[0]);
		int[] flags = new int[numCourses];
		for(int i = 0;i<numCourses;i++) if(!dfs(adjEdge,flags,i)) return false;
		return true;
	}
	// 1 : is under visiting  -1: had been visited 0: not visited
	public boolean dfs(List<List<Integer>> adjEdge,int[] flags,int i){
		if(flags[i] == 1) return false;
		if(flags[i] == -1) return true;
		flags[i] = 1;
		for(int j:adjEdge.get(i)) if(!dfs(adjEdge,flags,j)) return false;
		flags[i] = -1;
		return true;
	}
}

BFS

class Soution{
	public boolean canFinish(int numCourses,int[][] prerequisites){
		// inital the inDegree array
		int[] inDegree = new int[numCourses];
		// inital adj graph 
		List<List<Integer>> adjEdge = new ArrayList<>();
		// inital queue
		Queue<Integer> queue = new LinkedList<>();
		for(int i = 0;i<numCourses;i++) adjEdge.add(new ArrayList<Integer>());
		for(int[] info:prerequisites){
			inDegree[info[0]]++;
			adjEdge.get(info[1]).add(info[0]);
		}
		// get all the courses with the indegree of 0
		for(int i = 0;i<numCourses;i++) if(inDegree[i] == 0) queue.add(i);
		// bfs
		while(!queue.isEmpty()){
			int pre = queue.poll();
			numCourses--;
			for(int cur:adjEdge.get(pre)) if(--inDegree[cur] == 0) queue.add(cur);
		}
		return numCourses == 0;
	}
}

最小生成树

Kruskal算法

kruskal 算法:

  • step1:将边集合的权值按照从小到大的顺序排序
  • step2:每次挑选最小的边,判断如果选取该边之后,是否能够形成一个环(也就是说边的两端的定点不能在同一棵树中),不能形成一个环的情况下就选取这条边,并将这条边加入已经选取的边集合,构成一棵树(或者是森林),若能够形成一个环,就直接跳过这条边,到下一条边。
  • step3:重复step2操作,直到选取了n-1条边,结束
    kruskal算法的出发点是边,所以更适合与点多边少的情况

现在问题出现在怎么判断是否形成了环?
回答的方案就是使用并查集,判断选取的边的两端是否在一个集合中,如果不在,就选取这条边,并将这条边的两个端点加入到同一个集合,直到所有的点都属于同一个集合

 class Solution{
	public static void main(String[] args) {
		int[][] graph = new int[][]{{0,6,1,5,0,0},{6,0,5,0,3,0},{1,5,0,5,6,4},{5,0,5,0,0,2},{0,3,6,0,0,6},{0,0,4,2,6,0}};
        System.out.println(kruskal(graph));
        //System.out.println(prim(graph));
	}
    // kruskal算法
    static class UnionFind{
        int[] parents;
        UnionFind(int size){
            parents = new int[size];
            for(int i = 0;i<parents.length;i++) parents[i] = i;
        }
        public int find(int x){
            return parents[x]==x?x:find(parents[x]);
        }
        public void union(int x,int y){
            int xp = find(x);
            int yp = find(y);
            if(xp == yp) return;
            parents[xp] = yp;
        }
        public boolean isConnect(int x,int y){
            return find(x) == find(y);
        }
    }
    public static int kruskal(int[][] graph){
        // 结果
        int res = 0;
        int n = graph.length;
        // 初始化并查集
        UnionFind uf = new UnionFind(n);
        // 记录选取边的数目
        int flag = 0;
        while(flag < n-1){
            int minEdge = Integer.MAX_VALUE;
            // 代表一条边的起始顶点与终止顶点
            int from = 0;
            int to = 0;
            for(int i = 0;i<n;i++){
                for(int j = 0;j<n;j++){
                    if(graph[i][j] != 0 && graph[i][j] < minEdge){
                        minEdge = graph[i][j];
                        from = i;
                        to = j;
                    }
                }
            }
            // 修改已经找到的最短边
            graph[from][to] = Integer.MAX_VALUE;
            // 找到了最短的边,判断其是否形成了环
            if(!uf.isConnect(from,to)){
                uf.union(from,to);
                flag++;
                res+=minEdge;
            }
        }
        return res;

    }
}

Prim算法

prim算法:prim算法本质上属于贪心算法,有两个集合,其中一个是已选点集U,另外一个是待选点集V

  • step1:随机选取一个起始点a,这样={a},同时其待选点集就是V=

  • step2:然后选取最小的边,但是这时候选取边的时候是有要求的,要求就是,选取的边的一个顶点在已选点集,另外一个断点在待选点集V中

  • step3:上述操作进行完毕之后,将上述用到的待选点集中用到的那个点加入到已选点集U

  • step4:重复2-3 直到待选点集中没有了顶点

    prim算法的出发点是点,所以更适合与点少边多的情况

 class Solution{
	public static void main(String[] args) {
		int[][] graph = new int[][]{{Integer.MAX_VALUE,4,13,7,7},{4,Integer.MAX_VALUE,9,3,7},{13,9,Integer.MAX_VALUE,10,14},{7,3,10,Integer.MAX_VALUE,4},{7,7,14,4,Integer.MAX_VALUE}};
              System.out.println(prim(graph,1));
	}
    // prim算法
    public static int prim(int[][] graph,int start){
        int res = 0;
        int n = graph.length;
        // 当前prim树顶点的个数
        int treeSize = 1;
        // lowcost[i]表示已选点集U中的顶点i到代选点集V中所有节点距离的最小值
        int[] lowcost = new int[n];
        // nerv[i]代表已选点集U中的顶点i到代选点集V中所有顶点那个顶点最近,-1代表i已经在已选点集合U中了,1代表在代选集合V中
        int[] nerv = new int[n];
        // 初始化lowcost和nerv
        for(int i = 0;i<n;i++){
            lowcost[i] = graph[start][i];
            nerv[i] = i==start?0:1;
        }
        while(treeSize < n){
            int minDistance = Integer.MAX_VALUE;
            // v代表这次循环中,待选点的选取点
            int v = -1;
            // 从U中寻找一个顶点,使之是到V中节点的最小距离
            for(int i =0;i<n;i++){
                // 如果这个顶点在待选集合V中,并且从已选节点结合U到达该节点的距离小于当前循环中的最小距离,更新
                if(nerv[i] == 1 && lowcost[i] < minDistance){
                    minDistance = lowcost[i];
                    v = i;
                }
            }
            // 如果有比当前最短距离要短的待选点集合
            if(v!=-1){
                // 将这个点加入到已选点集
                nerv[v] = -1;
                treeSize++;
                res+=minDistance;
                // 由于已选点集合U中加入了新的点v,所以要更新lowcost中顶点v与待选点集合V中各个顶点的距离
                for(int j = 0;j<n;j++){
                    if(nerv[j] == 1 && graph[v][j] < lowcost[j]){
                        lowcost[j] = graph[v][j];
                        nerv[j] = 1;
                    }
                }
            }
        }
        return res;
    }
}

最短路径

Dijkstra算法

基于贪心算法,时间复杂度 O(V^2)

class Solution{
	public static void main(String[] args) {
		int[][] graph = new int[][]{{0,1,0,1,0,0,1},{1,0,1,1,0,0,0},{0,1,0,0,1,1,0},{1,1,0,0,1,0,0},{0,0,1,1,0,1,0},{0,0,1,0,1,0,0},{1,0,0,0,0,0,0}};
		System.out.println(dijkstra(graph,6,5));
		
	}
	// dijkstra 算法
	public static int dijkstra(int[][] graph,int start,int end){
		int n = graph.length;
		// 记录是否存在由start到数组下标节点位置的最短路径, 初始化start到start为true
		boolean[] visited = new boolean[n];
		visited[start] = true;
		// 记录由start到数组下标节点的最短路径,并初始化
		int[] distance = new int[n];
		for(int i = 0;i<n;i++) if(graph[start][i] != 0) distance[i] = graph[start][i];
		// 遍历除了start本身之外的所有的n-1个节点
		for(int i = 1;i< n;i++){
			// 记录当前的最短距离与最短距离节点的下标
			int minDistance = Integer.MAX_VALUE;
			int minIndex = 1;
			// 遍历所有节点,寻找当前循环中最短距离与最短距离节点的下标
			for(int j = 0;j<n;j++){
				// 如果start到一个j节点不存在最短路径,并且现在start到达j节点的路径存在,并且start到达该j节点的路径小于当前循环的最短路径,重新赋值
				if( !visited[j] && distance[j] != 0 && distance[j] <= minDistance){
					minDistance = distance[j];
					minIndex = j;
				}
			}
			// 找到当前的最近距离节点,与最短路径节点的下标
			// 标记最短路径的节点已经找到
			visited[minIndex] = true;
			// 根据刚刚找到的最短距离节点,更新start到各个节点的距离数组
			for(int j = 0;j<n;j++){
				// 如果刚刚更新的最短距离节点能够到达当前遍历的节点
				if(graph[minIndex][j] != 0){
					// 取之前路径与当前更新路径的较小值
					if(distance[j] != 0) distance[j] = Math.min(distance[j],distance[minIndex] + graph[minIndex][j]);
					// 该节点是第一次被访问,说明之前不存在最短距离,直接更新
					else distance[j] = distance[minIndex] + graph[minIndex][j];
				}
			}

		}
		// 返回start到end的最短距离
		return distance[end];
	}

}

Bellman-Ford算法

leetcode 743
其显著特点是可以求取含有负权图的单源最短路径
时间复杂度O(V*E)

class Solution {
    public int networkDelayTime(int[][] times, int N, int K) {
        // 存放 K 到各个点的最短路径,最大的那个最短路径即为结果
        int[] distance = new int[N + 1];
        Arrays.fill(distance, -1);
        distance[K] = 0;

        // 进行 N-1 轮的松弛,因为任意两点间的最短路径最多包含 N-1 条边
        for (int i = 1; i <= N - 1; i++) {
            for (int[] time : times) {
                // 源节点
                int u = time[0];
                // 目标节点
                int v = time[1];
                // 一个信号源从源节点到目标节点的时间
                int w = time[2];
                // 判断能否通过 u->v 缩短 distance[v](松弛)
                if (distance[u] != -1) {
                    if (distance[v] == -1) {
                        distance[v] = distance[u] + w;
                    } else {
                        distance[v] = Math.min(distance[v], distance[u] + w);
                    }
                }
            }
        }

        int maxDistance = 0;
        for (int i = 1; i <= N; i++) {
            if (distance[i] == -1) {
                return -1;
            }
            maxDistance = Math.max(distance[i], maxDistance);
        }

        return maxDistance;
    }
}

SPFA算法

Shortest Path Faster Algrithm
显著特点是:其可以求含有负权图的最短路径,在Bellman-ford算法的基础上,减少了冗余的松弛操作,是一种高效的最短路径法。

class Solution {
    public int networkDelayTime(int[][] times, int N, int K) {
        // 构建邻接表,用于存放各个点到各个点的距离
        int[][] graph = new int[N + 1][N + 1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= N; j++) {
                graph[i][j] = i == j ? 0 : -1;
            }
        }
        // 遍历times填充邻接表
        for (int[] time : times) {
            graph[time[0]][time[1]] = time[2];
        }

        // 存放 K 到各个点的最短路径,最大的那个最短路径即为结果
        int[] distance = new int[N + 1];
        Arrays.fill(distance, -1);
        distance[K] = 0;

        Queue<Integer> queue = new LinkedList<>();
        queue.add(K);

        while (!queue.isEmpty()) {
            // 取出队首节点
            int curr = queue.poll();
            for (int i = 1; i <= N; i++) {
                if (graph[curr][i] != -1 && (distance[i] == -1 || distance[i] > distance[curr] + graph[curr][i])) {
                    // 当最短距离发生变化且不在队列中时,将该节点加入队列
                    distance[i] = distance[curr] + graph[curr][i];
                    if (!queue.contains(i)) {
                        queue.add(i);
                    }
                }
            }
        }

        int maxDistance = 0;
        for (int i = 1; i <= N; i++) {
            if (distance[i] == -1) {
                return -1;
            }
            maxDistance = Math.max(distance[i], maxDistance);
        }

        return maxDistance;
    }
}

Floyd算法

多元最短路径用floyd
基于动态规划,时间复杂度O(V^3)

class Solution{
	public static void main(String[] args) {
		int[][] graph = new int[][]{{0,1,0,1,0,0,1},{1,0,1,1,0,0,0},{0,1,0,0,1,1,0},{1,1,0,0,1,0,0},{0,0,1,1,0,1,0},{0,0,1,0,1,0,0},{1,0,0,0,0,0,0}};
		System.out.println(floyd(graph,6,5));
		
	}
	// Floyd 算法
	public static int floyd(int[][] graph,int start,int end){
		int n = graph.length;
		for(int k = 0;k<n;k++){
			for(int i = 0;i<n;i++){
				for(int j = 0;j<n;j++){
					// 松弛
					if(graph[i][k] != 0 && graph[k][j] != 0){
						if(graph[i][j] != 0) graph[i][j] = Math.min(graph[i][j],graph[i][k] + graph[k][j]);
						else graph[i][j] = graph[i][k] + graph[k][j];
					}
				}
			}
		}
		return graph[start][end];
	}

}

拓扑排序

BFS

class Soution{
	public boolean canFinish(int numCourses,int[][] prerequisites){
		// inital the inDegree array
		int[] inDegree = new int[numCourses];
		// inital adj graph 
		List<List<Integer>> adjEdge = new ArrayList<>();
		// inital queue
		Queue<Integer> queue = new LinkedList<>();
		for(int i = 0;i<numCourses;i++) adjEdge.add(new ArrayList<Integer>());
		for(int[] info:prerequisites){
			inDegree[info[0]]++;
			adjEdge.get(info[1]).add(info[0]);
		}
		// get all the courses with the indegree of 0
		for(int i = 0;i<numCourses;i++) if(inDegree[i] == 0) queue.add(i);
		// bfs
		while(!queue.isEmpty()){
			int pre = queue.poll();
			numCourses--;
			for(int cur:adjEdge.get(pre)) if(--inDegree[cur] == 0) queue.add(cur);
		}
		return numCourses == 0;
	}
}

DFS

class Soution{
	public boolean canFinish(int numCourses,int[][] prerequisites){
		// inital adj graph 
		List<List<Integer>> adjEdge = new ArrayList<>();
		// inital queue
		for(int i = 0;i<numCourses;i++) adjEdge.add(new ArrayList<Integer>());
		for(int[] info:prerequisites) adjEdge.get(info[1]).add(info[0]);
		int[] flags = new int[numCourses];
		for(int i = 0;i<numCourses;i++) if(!dfs(adjEdge,flags,i)) return false;
		return true;
	}
	// 1 : is under visiting  -1: had been visited 0: not visited
	public boolean dfs(List<List<Integer>> adjEdge,int[] flags,int i){
		if(flags[i] == 1) return false;
		if(flags[i] == -1) return true;
		flags[i] = 1;
		for(int j:adjEdge.get(i)) if(!dfs(adjEdge,flags,j)) return false;
		flags[i] = -1;
		return true;
	}
}

关键路径算法

关键路径算法个人觉得比较难,话费了大量的时间记住代码

  1. 根据图的描述建立图的邻接表
  2. 从源点\(V_0\)出发,根据拓扑排序算法求出源点到汇点每一个顶点的最早发生时间etv,如果得到的拓扑排序个数小于网的顶点个数,则网中存在环,无关键路径,结束
  3. 从汇点\(V_n\)出发,且汇点的最晚发生时间ltv[n-1] = etv[n-1],按照逆拓扑排序,计算每一个节点的ltv
  4. 根据每一个顶点的etv与ltv求出每条弧的ete和lte若ete = lte,说明该活动是关键活动
import java.util.*;
public class CriticalPathSort{
	// 边表节点
	public static class EdgeNode{
		// 相邻的邻居节点
		public int adjevex;
		// 与节点相邻的节点的边的权重
		public int weight;
		// 下一个相邻的节点指向
		public EdgeNode next;

		public EdgeNode(int adjevex,EdgeNode next){
			this.adjevex = adjevex;
			this.next = next;
		}
		public EdgeNode(int adjevex,int weight,EdgeNode next){
			this.adjevex = adjevex;
			this.weight = weight;
			this.next = next;
		}
	}
	// 顶点节点
	public static class VertexNode{
		// 节点入度
		public int in;
		// 节点的数据
		public Object data;
		// 指向edgenode的第一个
		public EdgeNode firstedge;
		public VertexNode(Object data,int in,EdgeNode firstedge){
			this.data =data;
			this.in = in;
			this.firstedge = firstedge;
		}
	}
	// 每一个点的最早开始时间数组etv,以及每一个节点的最晚开始时间数组ltv
	int[] etv,ltv;
	// 存储入度为0的顶点,便于每次寻找入度为0的节点时都遍历整个临界表
	Stack<Integer> stack = new Stack<>();
	// 将顶点序号压入拓扑序列的栈
	Stack<Integer> stack2 = new Stack<Integer>();
	static VertexNode[] adjList;
	// 通过拓扑排序求得每一个节点的最早开始时间
	public boolean ToplogicalSort(){
		EdgeNode e;
		int k,gettop;
		int count = 0;
		etv = new int[adjList.length];
		// 将入度为0的节点压入stack
		for(int i = 0;i < adjList.length;i++) if(adjList[i].in == 0) stack.push(i);
		for(int i = 0;i < adjList.length;i++) etv[i] = 0;
		while(!stack.isEmpty()){
			gettop = stack.pop();
			count++;
			stack2.push(gettop);
			for(e = adjList[gettop].firstedge;e!=null;e=e.next){
				k = e.adjevex;
				if((--adjList[k].in) == 0) stack.push(k);
				if(etv[gettop] + e.weight > etv[k]) etv[k] = etv[gettop] + e.weight;
			}
		}
		if(count < adjList.length) return false;
		else return true;


	}
	// 求解关键路径
	public void CriticalPath(){
		EdgeNode e;
		int gettop,k,j;
		// 活动的最早开始时间 就是弧 与活动的最晚开始时间
		int ete,lte;
		if(!this.ToplogicalSort()){
			System.out.println("the AOE has cycle");
			return;
		}
		// 最晚开始时间 
		ltv = new int[adjList.length];
		// 初始化节点的最晚开始时间
		for(int i = 0;i<adjList.length;i++) ltv[i] = etv[etv.length - 1];
		while(!stack2.isEmpty()){
			gettop = stack2.pop();
			for(e = adjList[gettop].firstedge;e!=null;e=e.next){
				k = e.adjevex;
				if(ltv[k] - e.weight < ltv[gettop]) ltv[gettop] = ltv[k] - e.weight;
			}
		}
		for(int i = 0;i<adjList.length;i++){
			for(e = adjList[i].firstedge;e!=null;e=e.next){
				k = e.adjevex;
				ete = etv[i];
				lte = ltv[k] - e.weight;
				if(ete == lte) System.out.print("< " + adjList[i].data+","+adjList[k].data+" > len = "+e.weight+" , ");
			}
		}

	}
	// 得到相邻的节点
	public static EdgeNode getAdjvex(VertexNode node){
		EdgeNode e = node.firstedge;
		while(e != null){
			if(e.next == null) break;
			else e = e.next;
		}
		return e;

	}
	public static void main(String[] args){
		// 入度表
		int[] ins = {0,1,1,2,2,1,1,2,1,2};
		// 临接表结构
		int[][] adjvexs = {{2,1},{4,3},{5,3},{4},{7,6},{7},{9},{8},{9},{}};
		// 边的权重临接链表
		int[][] widths = {{4,3},{6,5},{7,8},{3},{4,9},{6},{2},{5},{3},{}};

		adjList = new VertexNode[ins.length];
		// 初始化节点的邻接表
		for(int i = 0;i<ins.length;i++){
			adjList[i] = new VertexNode("V"+i,ins[i],null);
			if(adjvexs[i].length > 0){
				for(int j = 0;j<adjvexs[i].length;j++){
					if(adjList[i].firstedge == null){
						adjList[i].firstedge = new EdgeNode(adjvexs[i][j],widths[i][j],null);
					}else{
						getAdjvex(adjList[i]).next = new EdgeNode(adjvexs[i][j],widths[i][j],null);
					}
				}
			}
		}
		CriticalPathSort c = new CriticalPathSort();
		c.CriticalPath();

	}

}

染色算法

二分染色问题

leetcode 785

class Solution{
      // 用来记录节点的染色状态,0,代表还没有染色,1代表染成了红色,2代表染成了绿色
      int[] colored;
      // 用来记录最终的结果状态
      boolean res;
      public boolean isBipartite(int[][] graph){
            res = true;
            colored = new int[graph.length];
            for(int i = 0;i<graph.length && res ; i++) if(colored[i] == 0) dfs(i,1,graph);
            return res;

      }
      public void dfs(int n,int color,int[][] graph){
            // 将这个节点进行染色
            colored[n] = color;
            // 与这个节点相邻的节点可选的染色的色彩
            int nNei = color == 1?2:1;
            for(int neighbor:graph[n]){
                  if(colored[neighbor] == 0){
                        dfs(neighbor,nNei,graph);
                        if(!res) return;
                  }else if(colored[neighbor] != nNei){
                        res = false;
                        return;
                  }
            }
      }
}

一般化的染色问题

leetcode 1042

class Solution{
      int[] colored;
      public int[] isRan(int[][] graph){
            colored = new int[n];
            for(int i = 0;i<n;i++) dfs(i);
            return colored;
      }
      public void dfs(int n,int[][] graph){
            if(colored[n] != 0) return;
            int[] record = new int[5];
            for(int neighbor:graph[n]) record[colored[neighbor]] = 1;
            for(int i = 0;i<record.length;i++) if(record[i] == 0) colored[n] = i;
            for(int neighbor:graph[n]) dfs(neighbor);
      }
}
posted @ 2020-11-10 14:05  BOTAK  阅读(215)  评论(0编辑  收藏  举报