【算法】无权图相关操作

262 阅读3分钟

1. 图的相关概念

  • 一个图(graph)G = (V,E)顶点(vertex) 集 V 和 边(edge) 集 E 组成

  • 图可以分为无向图有向图;也可以分为无权图有权图

    连通图:在无向图中,若任意两个顶点vi与vj都有路径相通,则称该无向图为连通图

    强连通图:在有向图中,若任意两个顶点vi与vj都有路径相通,则称该有向图为强连通图

    连通网:在连通图中,若图的边具有一定的意义,每一条边都对应着一个数,称为权;权代表着连接连个顶点的代价,称这种连通图叫做连通网

    生成树:一个连通图的生成树是指一个连通子图,它含有图中全部n个顶点,但只有足以构成一棵树的n-1条边。一颗有n个顶点的生成树有且仅有n-1条边,如果生成树中再添加一条边,则必定成环

    最小生成树:在连通网的所有生成树中,所有边的权值和最小的生成树,称为最小生成树

  • 图可以用两种形式来表示,邻接矩阵邻接表,前者适用于稠密图,后者适用于稀疏图

    完全图(complete graph): 是其每一对顶点间都存在一条边的图

    稠密图(dense) :图中 E 的条数接近 V*V,也就是接近任意两点之间相连

    稀疏图(sparse) :图中 E 的条数远小于 V*V

  • 无权图的邻接矩阵和邻接表可以用以下格式来表示

// 有v个顶点
// 邻接矩阵:g[i][j] = 1 表示顶点i和j相连(无权图)
//          g[i][j] = 1 表示顶点i和j相连,且权值为1(有权图)
int[][] g = new int[v][v];

// 邻接表:g[i] = [1,2,3] 表示顶点i和顶点1,2,3相连(无权图)
int[][] g = new int[v][v];

// 邻接表:g.get(i) = [1,2,3] 表示顶点i和顶点1,2,3相连(无权图)
List<Set<Integer>> g;

2. 无权图的相关操作

  • 无权图的结构和基础操作可如下所示
public class Graph {
    // 顶点数
    private int n;
    // 边数
    private int m;
    // 是否为有向图
    private boolean directed;
    // 邻接矩阵
    private boolean[][] g;
    // 邻接表
    private List<Set<Integer>> g2;

    public Graph() {
        this(10, false);
    }

    public Graph(int n, boolean directed) {
        this.n = n;
        this.directed = directed;
        this.m = 0;
        this.g = new boolean[n][n];
        this.g2 = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            this.g2.add(new HashSet<>());
        }
    }

    private boolean validateVertex(int v) {
        return 0 <= v && v < n;
    }

    public int getVertexNum() {
        return this.n;
    }

    public int getEdgeNum() {
        return this.m;
    }
    // 给两个顶点加一条边
    public void addEdge(int v1, int v2) {
        if (validateVertex(v1) && validateVertex(v2)) {
            if (haveEdge(v1, v2)) {
                return;
            }
            g[v1][v2] = true;
            g2.get(v1).add(v2);
            if (!directed) {
                g[v2][v1] = true;
                g2.get(v2).add(v1);
            }
            m++;
        }
    }
    // 判断两个顶点是否有边直接相连
    public boolean haveEdge(int v1, int v2) {
        return validateVertex(v1) && validateVertex(v2) && g[v1][v2];
    }
    // 获取某个顶点的所有邻接顶点
    public Set<Integer> getAdjVertex(int v) {
        return validateVertex(v) ? g2.get(v) : new HashSet<>();
    }
}

2.1 遍历相邻顶点

如果使用领接矩阵,遍历一个顶点的相邻顶点的复杂度是O(v);而使用邻接表的所需要的复杂度是O(1)

2.2 遍历全部顶点

遍历全部顶点可以使用深度优先遍历,遍历的过程中要对顶点做个标记,后续不要再遍历该顶点。这种遍历方式可以求出一个图的连通分量(即这个图有多少个相连通的部分)

public class ConnectedComponent {
    
    private int connectedCount = 0;

    /**
     * 得到图的连通分量
     *
     * @param graph 邻接表表示的图
     * @return 图的连通分量
     */
    public int getConnectedCount(List<Set<Integer>> graph) {
        if (graph == null || graph.size() == 0) {
            return 0;
        }
        int size = graph.size();
        // 各顶点的连通分量编号
        int[] id = new int[size];

        boolean[] visit = new boolean[size];
        connectedCount = 0;
        for (int i = 0; i < size; i++) {
            if (!visit[i]) {
                dfs(i, visit, id, graph);
                connectedCount++;
            }
        }
        return connectedCount;
        // 以下结果可以得到两个顶点v1 v2是否相连,相当于并查集的功能
        // return id[v1] == id[v2];
    }

    private void dfs(int v, boolean[] visit, int[] id, List<Set<Integer>> graph) {
        visit[v] = true;
        id[v] = connectedCount;
        for (Integer adj : graph.get(v)) {
            if (!visit[adj]) {
                dfs(adj, visit, id, graph);
            }
        }
    }
}

2.3 寻路

2.3.1 普通路径

使用深度优先遍历,得到的路径并不是最短路径

public class Path {

    public String getPath(List<Set<Integer>> graph, int source, int target) {
        if (graph == null || graph.size() == 0) {
            return "不存在" + source + "到" + target + "的路径";
        }
        int size = graph.size();
        // 若marked[i] = true,则说明存在从顶点source到顶点i的路径
        boolean[] marked = new boolean[size];
        // 若from[i] = j,说明i的前一个顶点是j
        int[] from = new int[size];
        Arrays.fill(from, -1);

        getPathDfs(graph, source, marked, from);

        if (!marked[target]) {
            return "不存在" + source + "到" + target + "的路径";
        }

        StringBuilder path = new StringBuilder();
        int temp = target;
        while (temp != -1) {
            path.insert(0, temp);
            temp = from[temp];
            if (temp != -1) {
                path.insert(0, " -> ");
            }
        }
        return path.toString();
    }

    private void getPathDfs(List<Set<Integer>> graph, int v, boolean[] marked, int[] from) {
        marked[v] = true;

        for (Integer nextV : graph.get(v)) {
            if (!marked[nextV]) {
                from[nextV] = v;
                getPathDfs(graph, nextV, marked, from);
            }
        }
    }
}

2.3.2 无权图最短路径

使用广度优先遍历,可以得到无权图最短路径

public class ShortestPath {

    public String getShortestPath(List<Set<Integer>> graph, int source, int target) {
        if (graph == null || graph.size() == 0) {
            return "不存在" + source + "到" + target + "的路径";
        }
        int size = graph.size();
        // 若marked[i] = true,则说明存在从顶点source到顶点i的路径
        boolean[] marked = new boolean[size];
        // 若from[i] = j,说明i的前一个顶点是j
        int[] from = new int[size];
        Arrays.fill(from, -1);

        Queue<Integer> queue = new ArrayDeque<>();
        queue.offer(source);
        marked[source] = true;
        // 使用广度优先遍历得到最短路径
        while (!queue.isEmpty()) {
            Integer curV = queue.poll();
            for (Integer nextV : graph.get(curV)) {
                if (!marked[nextV]) {
                    marked[nextV] = true;
                    from[nextV] = curV;
                    queue.offer(nextV);
                }
            }
        }

        if (!marked[target]) {
            return "不存在" + source + "到" + target + "的路径";
        }

        StringBuilder path = new StringBuilder();
        int temp = target;
        while (temp != -1) {
            path.insert(0, temp);
            temp = from[temp];
            if (temp != -1) {
                path.insert(0, " -> ");
            }
        }
        return path.toString();
    }
}