图论的一些算法

194 阅读2分钟

存图方式

邻接矩阵

  • 使用二维矩阵进行存图
  • 适用于边数较多的稠密图(边数量接近为点的数量的平方)
// 邻接矩阵数组:w[a][b] = c 代表从 ab 有权重为 c 的边
int[][] w = new int[N][N];
// 加边操作
void add(int a, int b, int c) {
    w[a][b] = c;
}

邻接表

  • 用数组存储单链表(头插法)
  • 适用于边数较少的稀疏图(边数量和点数量接近)
Map<Integer,List<int[]>> table;
//初始化
Map<Integer,List<int[]>> table=new HashMap<>();
for(int[] e:edges){
    int a=e[0],b=e[1],v=e[2];
    table.computeIfAbsent(a,k->new ArrayList<>()).add(new int[]{b,v});
    table.computeIfAbsent(b,k->new ArrayList<>()).add(new int[]{a,v});
}

最短路径算法

Floyd(on^3)

  • 多源最短路算法
  • 跑一遍Floyd可以得到从任意点出发,到达任意起点的最短路径
void floyd() {
    // floyd 基本流程为三层循环:
    // 枚举中转点 - 枚举起点 - 枚举终点 - 松弛操作
    for (int p = 1; p <= n; p++) {
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= n; j++) {
                w[i][j] = Math.min(w[i][j], w[i][p] + w[p][j]);
            }
        }
    }
}

dijkstra(on^2)(邻接矩阵)

  • 单源最短路径算法
  • 跑一遍dijkstra可以得到从源点k到其他点x的最短距离
void dijkstra() {
    //vis数组用来记录哪些点被更新过
    // 起始先将所有的点标记为「未更新」和「距离为正无穷」
    Arrays.fill(vis, false);
    //dist数组记录的是源点到任意点的最短距离
    Arrays.fill(dist, INF);
    // 只有起点最短距离为 0
    dist[k] = 0;
    // 迭代 n 次
    for (int p = 1; p <= n; p++) {
        // 每次找到「最短距离最小」且「未被更新」的点 t
        int t = -1;
        for (int i = 1; i <= n; i++) {
            if (!vis[i] && (t == -1 || dist[i] < dist[t])) t = i;
        }
        // 标记点 t 为已更新
        vis[t] = true;
        // 用点 t 的「最小距离」更新其他点
        for (int i = 1; i <= n; i++) {
            dist[i] = Math.min(dist[i], dist[t] + w[t][i]);
        }
    }
}

堆优化dijkstra(onlogn)(邻接表)

  • 单源最短路径算法
  • 边数据范围不算大时使用
pprivate int[] dijkstra(Map<Integer,List<int[]>> table,int start,int len){
        int[] dis=new int[len+1];
        Arrays.fill(dis,Integer.MAX_VALUE);
        dis[start]=0;
        dis[0]=0;
        boolean[] vis=new boolean[len+1];
        PriorityQueue<int[]> pq=new PriorityQueue<>((a,b)->a[1]-b[1]);
        pq.add(new int[]{start,0});
        while(!pq.isEmpty()){
            // 当队列不空,拿出一个源出来
            int temp=pq.poll()[0];
            if(vis[temp]) continue;
            // 标记访问
            vis[temp]=true;
            List<int[]> nodes=table.get(temp);
            for(int[] node:nodes){
                int next=node[0];
                if(vis[next])continue;
                // 更新到这个相邻节点的最短距离,与 poll出来的节点增加的距离 比较
                dis[next]=Math.max(dis[next],dis[temp]+node[1]);
                /堆中新增节点,这里需要手动传入 next节点堆距离值。否则如果next在队列中,将永远无法上浮。
                pq.add(new int[]{next,dis[next]});
            }
        }
        return dis;
    }

Bellman Ford

  • 单源最短路径算法
  • 负权图中求最短路
  • 解决有边数 解决有边数限制的最短路 问题
void bf() {
    // 起始先将所有的点标记为「距离为正无穷」
    Arrays.fill(dist, INF);
    // 只有起点最短距离为 0
    dist[k] = 0;
    // 迭代 n 次
    for (int p = 1; p <= n; p++) {
        int[] prev = dist.clone();
        // 每次都使用上一次迭代的结果,执行松弛操作
        for (int a = 1; a <= n; a++) {
            for (int i = he[a]; i != -1; i = ne[i]) {
                int b = e[i];
                dist[b] = Math.min(dist[b], prev[a] + w[i]);
            }
        }
    }
}

SPFA

  • 单源最短路径算法
  • 使用队列或者栈进行优化Bellman Ford
void spfa() {
    // 起始先将所有的点标记为「未入队」和「距离为正无穷」
    Arrays.fill(vis, false);
    Arrays.fill(dist, INF);
    // 只有起点最短距离为 0
    dist[k] = 0;
    // 使用「双端队列」存储,存储的是点编号
    Deque<Integer> d = new ArrayDeque<>();
    // 将「源点/起点」进行入队,并标记「已入队」
    d.addLast(k);
    vis[k] = true;
    while (!d.isEmpty()) {
        // 每次从「双端队列」中取出,并标记「未入队」
        int poll = d.pollFirst();
        vis[poll] = false;
        // 尝试使用该点,更新其他点的最短距离
        // 如果更新的点,本身「未入队」则加入队列中,并标记「已入队」
        for (int i = he[poll]; i != -1; i = ne[i]) {
            int j = e[i];
            if (dist[j] > dist[poll] + w[i]) {
                dist[j] = dist[poll] + w[i];
                if (vis[j]) continue;
                d.addLast(j);
                vis[j] = true;
            }
        }
    }
}

kruskal

  • 求最优路径中的最大权重值lc1631、778
  • 算法主要思路
    • 我们可以先遍历所有的点,将所有的边加入集合,存储的格式为数组 [a,b,w] ,代表编号为 a的点和编号为 b 的点之间的权重为 w (按照题意, w 为两者的最大高度)。
    • 对集合进行排序,按照 w 进行从小到达排序。
    • 当我们有了所有排好序的候选边集合之后,我们可以对边从前往后处理,每次加入一条边之后,
      • 使用并查集来查询左上角的点和右下角的点是否连通。
      • 当我们的合并了某条边之后,判定左上角和右下角的点联通,那么该边的权重即是答案。
class Solution {
    int[] p;
    int m;
    int n;
    boolean isConnected(int node1,int node2){
        return find(node1)==find(node2);
    }
    void union(int node1,int node2){
        p[find(node1)]=find(node2);
    }
    int find(int node){
        while(node!=p[node]) node=p[node];
        return node;
    }
    public int kruskal(int[][] grid) {
        m=grid.length;
        n=grid[0].length;
        p=new int[m*n];
        for(int i=0;i<m*n;i++) p[i]=i;
        List<int[]> list=new ArrayList<>();
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                int idx=getIdx(i,j);
                if(i+1<m){
                    list.add(new int[]{idx,getIdx(i+1,j),Math.max(grid[i][j],grid[i+1][j])});
                }
                if(j+1<n){
                    list.add(new int[]{idx,getIdx(i,j+1),Math.max(grid[i][j],grid[i][j+1])});
                }
            }
        }
        Collections.sort(list,(a,b)->a[2]-b[2]);
        int start=0,end=getIdx(m-1,n-1);
        for(int i=0;i<list.size();i++){
            int a=list.get(i)[0],b=list.get(i)[1],v=list.get(i)[2];
            union(a,b);
            if(isConnected(start,end)) return v;
        }
        return 0;
    }
    private int getIdx(int x,int y){
        return x*m+y;
    }
}