图的基本算法
图的相关的基本算法
主要用来记录和图相关的一些经常用到的算法
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;
}
}
关键路径算法
关键路径算法个人觉得比较难,话费了大量的时间记住代码
- 根据图的描述建立图的邻接表
- 从源点\(V_0\)出发,根据拓扑排序算法求出源点到汇点每一个顶点的最早发生时间etv,如果得到的拓扑排序个数小于网的顶点个数,则网中存在环,无关键路径,结束
- 从汇点\(V_n\)出发,且汇点的最晚发生时间ltv[n-1] = etv[n-1],按照逆拓扑排序,计算每一个节点的ltv
- 根据每一个顶点的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);
}
}
Saying Less Doing More