图论基础入门

一.图论与图

1.什么是图论

在这里插入图片描述
图论(英语:Graph theory),是组合数学的一个分支,和其他数学分支,如群论、矩阵论、拓扑学有着密切关系。

2.什么是图

图是图论的主要研究对象。图是由若干给定的顶点及连接两顶点的边所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系。顶点用于代表事物,连接两顶点的边则用于表示两个事物间具有这种关系。
在这里插入图片描述

3.经典图论问题和应用

1.经典问题
  1. 四色问题
  2. 柯尼斯堡七桥问题
  3. 最小生成树问题
  4. 路径问题
2.应用

在这里插入图片描述

4.经典图论算法

  1. Prim算法
  2. Krusk算法
  3. Dijkstra算法
  4. Bellman-Ford算法

二.图的分类

1.无向图

无向图指的是图的边没有方向的分别,通常可以认为无向图实质上是同时拥有两个方向的有向图。
在这里插入图片描述

2.有向图

有向图指的是图的边有方向的分别。
在这里插入图片描述

3.无权图

无权图指的是图的边不带权重。
在这里插入图片描述

4.带权图

带权图指的是图的边拥有权重。
在这里插入图片描述

5.稀疏图

在这里插入图片描述
稀疏图指的是对于顶点总数来说,每个顶点的边远少于顶点总数。

6.稠密图与完全图

当大部分顶点的边数接近于顶点总数时可被称为稠密图;当每个顶点的边都链接了其余的节点,则被称为完全图。
在这里插入图片描述

三.图的连通性

1.连通性

在这里插入图片描述
上图中,拥有三个图的集合,它们之间并不是联通的,但是自身却是连通的。所以图具有连通性。

2.带环边与平行边

对于简单图来说,顶点与顶点之间只含有一条边;而对于复杂图来说,顶点之间的边有可能会形成自环或平行边:
在这里插入图片描述

自环边可以用来表示一个导向自身的路径,而平行边则可以表示导向其他顶点的多个路径。

四.图的表示

1.邻接矩阵

我们可以使用一个 V 乘 V 的布尔
二维数组来表示图。 当顶点 v 和顶点 w 之间有相连接的边
时, 定义 v 行 w 列的元素值为 true, 否则为
false。
在这里插入图片描述

  • Graph1
/**
 * @Auther: ARong
 * @Date: 2020/2/29 11:34 上午
 * @Description: 使用邻接矩阵实现的图结构
 */
public class Graph1 {
    private int n; // 顶点数
    private int m; // 边数
    private boolean directed; // 是否为有向图
    private boolean[][] graph; // 二维boolean数组表示的图结构

    public Graph1(int n, boolean directed) {
        assert n >= 0;
        this.n = n;
        this.directed = directed;
        this.m = 0;
        graph = new boolean[n][n];
    }

    /*
     * @Author ARong
     * @Description 向节点间添加一条边
     * @Date 2020/2/29 11:39 上午
     * @Param [v, m]
     * @return void
     **/
    public void addEdge(int v, int m) {
        assert v >= 0 && v < n;
        assert m >= 0 && m < n;
        // 如果有这条边则不添加
        if (hasEdge(v, m)) {
            return;
        }
        graph[v][m] = true;
        // 如果是无向图,则还需要在另一方添加关系
        if (!directed) {
            graph[m][v] = true;
        }
        // 边数增加
        m++;
    }

    /*
     * @Author ARong
     * @Description 判断v,m是否存在边
     * @Date 2020/2/29 11:42 上午
     * @Param [v, m]
     * @return boolean
     **/
    private boolean hasEdge(int v, int m) {
        return graph[v][m];
    }
}

使用邻接矩阵来表示图有许多缺点,例如:

  1. 空间需要提前声明,如果图的节点很多,那么需要声明的空间太大,且不能动态拓展
  2. 不能表示平行边和自环关系

2.邻接表

我们可以使用一个以顶点为索引的列表数组, 其中的每个元素都是和该顶点相邻的顶点列表。

使用邻接表可以优化图的空间大小,但是一些操作的时间会比邻接矩阵实现的图耗费时间多。

在这里插入图片描述
在这里插入图片描述

  • Graph2
/**
 * @Auther: ARong
 * @Date: 2020/2/29 11:55 上午
 * @Description: 使用邻接表实现的图结构
 */
public class Graph2 {

    private int n; // 顶点数
    private int m; // 边数
    private boolean directed; // 是否为有向图
    private List<List<Integer>> graph; // 二维链表

    public Graph2(int n, boolean directed) {
        assert n >= 0;
        this.n = n;
        this.directed = directed;
        this.m = 0;
        graph = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            // 初始化
            graph.add(new ArrayList<>());
        }
    }


    /*
     * @Author ARong
     * @Description 向v,m之间添加一条边
     * @Date 2020/2/29 12:01 下午
     * @Param [v, m]
     * @return void
     **/
    public void addEdge(int v, int m) {
        assert v >= 0 && v < n;
        assert m >= 0 && m < n;
        // 如果有这条边则不添加
        if (hasEdge(v, m)) {
            return;
        }
        graph.get(v).add(m);
        // 如果是无向图,则还需要在另一方添加关系
        if (!hasEdge(v, m) && !directed) {
            graph.get(m).add(n);
        }
        // 边数增加
        m++;
    }

    /*
     * @Author ARong
     * @Description 判断m,v是否存在边
     * @Date 2020/2/29 11:58 上午
     * @Param [v, m]
     * @return boolean
     **/
    private boolean hasEdge(int v, int m) {
        List<Integer> list = graph.get(v);
        for (Integer node : list) {
            if (node == m) {
                return true;
            }
        }
        return false;
    }

}

在这里插入图片描述

五.图的基本算法

0.基本代码框架

以下是无向图结构的基本代码框架,一下算法实现基于该代码框架

/**
 * @Auther: ARong
 * @Date: 2020/3/1 11:27 上午
 * @Description: 使用邻接表实现的无向图
 */
public class UndirectedGraph {
    // 顶点数量
    private int vertexNum;
    // 顶点边数
    private int edgeNum;
    // 邻接表
    private List<List<Integer>> adj;
    // 顶点信息
    private List<String> vertexInfo;

    /*
     * @Author ARong
     * @Description 传入节点信息构造图基本数据
     * @Date 2020/3/1 11:29 上午
     * @Param [vertexInfo]
     * @return
     **/
    public UndirectedGraph(List<String> vertexInfo) {
        this.vertexInfo = vertexInfo;
        this.vertexNum = vertexInfo.size();
        // 初始化图
        adj = new ArrayList<>();
        for (int i = 0; i < vertexNum; i++) {
            // 构造每一个顶点的连接对象
            adj.add(new LinkedList<>());
        }
    }

    public UndirectedGraph(List<String> vertexInfo, int[][] edges) {
        this(vertexInfo);
        // 额外构造边信息
        for (int[] twoVertex : edges) {
            addEdge(twoVertex[0], twoVertex[1]);
        }
    }

    public int vertexNum() {
        return vertexNum;
    }

    public int edgeNum() {
        return edgeNum;
    }

    /*
     * @Author ARong
     * @Description 添加边
     * @Date 2020/3/1 11:31 上午
     * @Param [i, j]
     * @return void
     **/
    public void addEdge(int i, int j) {
        adj.get(i).add(j);
        adj.get(j).add(i);
        edgeNum++;
    }
    // 不需要set,所以不用返回List,返回可迭代对象就够了
    public Iterable<Integer> adj(int i) {
        return adj.get(i);
    }

    /*
     * @Author ARong
     * @Description 获取第i个顶点的数据
     * @Date 2020/3/1 11:32 上午
     * @Param [i]
     * @return java.lang.String
     **/
    public String getVertexInfo(int i) {
        return vertexInfo.get(i);
    }

    /*
     * @Author ARong
     * @Description 获取第i个顶点的边数
     * @Date 2020/3/1 11:32 上午
     * @Param [i]
     * @return int
     **/
    public int degree(int i) {
        return adj.get(i).size();
    }

    /*
     * @Author ARong
     * @Description 获取顶点中最大的边数
     * @Date 2020/3/1 11:33 上午
     * @Param []
     * @return int
     **/
    public int maxDegree() {
        int max = 0;
        for (int i = 0;i < vertexNum;i++) {
            if (degree(i) > max) {
                max = degree(i);
            }
        }
        return max;
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(vertexNum).append("个顶点, ").append(edgeNum).append("条边:\n");
        for (int i = 0; i < vertexNum; i++) {
            sb.append(i).append(": 【").append(adj.get(i)).append("】\n");
        }
        return sb.toString();
    }
}

1.深度优先遍历

  /*
     * @Author ARong
     * @Description 对图进行深度优先遍历
     * @Date 2020/3/1 11:45 上午
     * @Param []
     * @return java.util.List<java.lang.Integer>
     **/
    private void dfs(int i, boolean[] visited) {
        visited[i] = true;
        // 获取到i所连接的节点列表
        Iterator<Integer> iterator = adj(i).iterator();
        while (iterator.hasNext()) {
            Integer v = iterator.next();
            if (!visited[v]) {
                // v没有被访问过,继续搜索
                dfs(v, visited);
            }
        }
    }

2.求解连通量

 /*
     * @Author ARong
     * @Description 获取图的连通分量
     * @Date 2020/3/1 12:23 下午
     * @Param []
     * @return java.util.List<java.lang.Integer>
     **/
    public int ccount() {
        int ccount = 0;
        // 初始化访问数组,默认都是未访问过
        boolean[] visited = new boolean[vertexNum];
        // 获取图的邻接表
        for (int i = 0; i < adj.size(); i++) {
            if (!visited[i]) {
                // 在未访问过的节点开始dfs
                dfs(i, visited);
                ccount++;
            }
        }
        return ccount;
    }

3.判断两个顶点是否连通

/*
     * @Author ARong
     * @Description 判断两个节点是否相互连通
     * @Date 2020/3/1 12:37 下午
     * @Param [a, b]
     * @return boolean
     **/
    private boolean isConnected(int a, int b) {
        // 先对a进行深度优先遍历,标示遍历过的节点,若此时b没有被标示到说明不连通
        boolean[] visited = new boolean[vertexNum];
        dfs(a, visited);
        return visited[b];
    }

4.寻找连通路径

/*
     * @Author ARong
     * @Description 找到a与b之间的一条连通路径
     * @Date 2020/3/2 9:49 上午
     * @Param [a, b]
     * @return java.util.List<java.lang.Integer>
     **/
    public List<Integer> findPath(int a, int b) {
        // 判断是否连通
        if (!isConnected(a, b)) {
            return null;
        }
        // 连通,通过dfs搜索,并使用数组记录路径
        HashMap<Integer, Integer> map = new HashMap<>();
        boolean[] visited = new boolean[vertexNum];
        dfs(a, b, visited, map);
        // 将路径重现
        System.out.println(map.toString());
        LinkedList<Integer> res = new LinkedList<>();
        while (b != a) {
            res.offerFirst(b);
            b = map.get(b);
        }
        res.offerFirst(a);
        return res;
    }

    /*
     * @Author ARong
     * @Description dfs搜索a到b的路径
     * @Date 2020/3/2 9:56 上午
     * @Param [a, b, visited, list]
     * @return void
     **/
    private void dfs(int a, int b, boolean[] visited, HashMap<Integer, Integer> map) {
        // dfs终止条件,找到b
        if (a == b) {
            return;
        }
        visited[a] = true;
        Iterator<Integer> iterator = adj(a).iterator();
        while (iterator.hasNext()) {
            Integer next = iterator.next();
            // 继续dfs
            if(!visited[next]) {
                // 记录来时路径
                map.put(next, a);
                dfs(next, b, visited, map);
            }
        }
    }

6.广度优先遍历

/*
     * @Author ARong
     * @Description 广度优先遍历
     * @Date 2020/3/2 11:46 上午
     * @Param [i, visited]
     * @return void
     **/
     public void bfs(int i, boolean[] visited) {
         LinkedList<Integer> queue = new LinkedList<>();
         queue.offer(i);
         visited[i] = true;
         while (!queue.isEmpty()) {
             Integer poll = queue.poll();
             // 访问标记
             Iterator<Integer> iterator = adj(poll).iterator();
             while (iterator.hasNext()) {
                 // 获取poll的全部连接的顶点,继续bfs
                 Integer next = iterator.next();
                 if (!visited[next]) {
                     visited[next] = true;
                     queue.offer(next);
                 }
             }
         }
     }

7.寻找最短路径

/*
     * @Author ARong
     * @Description 获取a和b之间的最短路径
     * @Date 2020/3/2 11:57 上午
     * @Param [a, b]
     * @return java.util.List<java.lang.Integer>
     **/
    public List<Integer> findShortestPath(int a, int b) {
        // 判断是否连通
        if (!isConnected(a, b)) {
            return null;
        }
        // 连通,通过dfs搜索,并使用数组记录路径
        HashMap<Integer, Integer> from = new HashMap<>();
        boolean[] visited = new boolean[vertexNum];
        bfs(a, b, visited, from);
        // 将路径重现
        LinkedList<Integer> res = new LinkedList<>();
        while (b != a) {
            res.offerFirst(b);
            b = from.get(b);
        }
        res.offerFirst(a);
        return res;
    }

    /*
     * @Author ARong
     * @Description 广度优先遍历寻找最短路径
     * @Date 2020/3/2 11:57 上午
     * @Param [a, b, visited, from]
     * @return void
     **/
    private void bfs(int a, int b, boolean[] visited, HashMap<Integer, Integer> from) {
        LinkedList<Integer> queue = new LinkedList<>();
        queue.offer(a);
        visited[a] = true;
        while (!queue.isEmpty()) {
            Integer poll = queue.poll();
            // 访问标记
            Iterator<Integer> iterator = adj(poll).iterator();
            while (iterator.hasNext()) {
                // 获取poll的全部连接的顶点,继续bfs
                Integer next = iterator.next();
                if (!visited[next]) {
                    // 记录来时路径
                    from.put(next, poll);
                    // 判断是否找到
                    if (next == b) {
                        return;
                    }
                    visited[next] = true;
                    queue.offer(next);
                }
            }
        }
    }
发布了309 篇原创文章 · 获赞 205 · 访问量 30万+

猜你喜欢

转载自blog.csdn.net/pbrlovejava/article/details/104633665