图论基本概念

一、顶点(vertex)

带数字的点就是顶点,表示某个事物或对象。由于图的术语没有标准化,可以称顶点为点、节点、结点、端点等。

顶点的度=入度+出度

二、边(edge)

顶点之间的线条就是边,表示事物与事物之间的关系。 边表示的是顶点之间的逻辑关系

三、同构(Isomorphism )

image.png

从图(graph)的角度出发,这2个图是一样的,即它们是同构的。

这些图拥有相同的信息,是同一个图。同构的图区别仅在于画法不同。

四、有向/无向图(Directed Graph/ Undirected Graph)

最基本的图通常被定义为“无向图”,与之对应的则被称为“有向图”。两者唯一的区别在于,有向图中的边是有方向性的。 下图即是一个有向图,边的方向分别是:(1->2), (1-> 3), (3-> 1), (1->5), (2->3), (3->4), (3->5), (4->5), (1->6), (4->6)。要注意,图中的边(1->3)和(3->1)是不同的。有向图和无向图的许多原理和算法是相通的。

image.png

五、权重(weight)

边的权重(或者称为权值、开销、长度等),也是一个非常核心的概念,即每条边都有与之对应的值。

有时候为了应对特殊情况,边的权重可以是零或者负数。

六、路径/最短路径(path/shortest path)

在图上任取两顶点,分别作为起点(start vertex)和终点(end vertex),我们可以规划许多条由起点到终点的路线。不会来来回回绕圈子、不会重复经过同一个点和同一条边的路线,就是一条“路径”。两点之间存在路径,则称这2个顶点是连通的(connected)。

路径也有权重。路径经过的每一条边,沿路加权重,权重总和就是路径的权重(通常只加边的权重,而不考虑顶点的权重)。在路网中,路径的权重,可以想象成路径的总长度。在有向图中,路径还必须跟随边的方向。
值得注意的是,一条路径包含了顶点和边,因此路径本身也构成了图结构,只不过是一种特殊的图结构。

七、环(loop)

环,也成为环路,是一个与路径相似的概念。在路径的终点添加一条指向起点的边,就构成一条环路。通俗点说就是绕圈。

八、连通图/连通分量(connected graph/connected component)

如果在图G中,任意2个顶点之间都存在路径,那么称G为连通图。

image.png

上面这张图中,顶点8和顶点2之间就不存在路径,因此它不是一个连通图,当然该图中还有很多顶点之间不存在路径。

上图虽然不是一个连通图,但它有多个连通子图:1,2,3顶点构成一个连通子图,1,2,3,4,5顶点构成的子图是连通图,6,7,8,9顶点构成的子图也是连通图,当然还有很多子图。

一个图的最大连通子图称为它的连通分量。1,2,3,4,5顶点构成的子图就是该图的最大连通子图,也就是连通分量。

连通分量有如下特点:

  • 是子图
  • 子图是连通的
  • 子图含有最大顶点数

注意:最大连通子图(连通分量)是其本身。

图的表示

  1. // 图的接口
  2. public interface Graph {
  3. public int V();
  4. public int E();
  5. public void addEdge(int v, int w);
  6. boolean hasEdge(int v, int w);
  7. void show();
  8. public Iterable<Integer> adj(int v);
  9. }

1. 邻接矩阵

  • 邻接矩阵表示无向图

image.png

  • 邻接矩阵表示有向图

image.pngimage.png

  1. /**
  2. * 稠密图-邻接矩阵
  3. */
  4. public class DenseGraph {
  5. private int n; // 节点数
  6. private int m; // 边数
  7. private boolean directed; // 是否为有向图
  8. private boolean[][] g; // 图的具体数据
  9. // 构造函数
  10. public DenseGraph( int n , boolean directed ){
  11. assert n >= 0;
  12. this.n = n;
  13. this.m = 0; // 初始化没有任何边
  14. this.directed = directed;
  15. // g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
  16. // false为boolean型变量的默认值
  17. g = new boolean[n][n];
  18. }
  19. public int V(){ return n;} // 返回节点个数
  20. public int E(){ return m;} // 返回边的个数
  21. // 向图中添加一个边
  22. public void addEdge( int v , int w ){
  23. assert v >= 0 && v < n ;
  24. assert w >= 0 && w < n ;
  25. if( hasEdge( v , w ) ){
  26. return;
  27. }
  28. g[v][w] = true;
  29. if( !directed ){
  30. g[w][v] = true;
  31. }
  32. m ++;
  33. }
  34. // 验证图中是否有从v到w的边
  35. boolean hasEdge( int v , int w ){
  36. assert v >= 0 && v < n ;
  37. assert w >= 0 && w < n ;
  38. return g[v][w];
  39. }
  40. //遍历邻边
  41. //返回图中一个顶点的所有相邻顶点
  42. public Iterable<Integer> adj(int v) {
  43. assert v >= 0 && v < n;
  44. Vector<Integer> adjV = new Vector<Integer>();
  45. for(int i = 0 ; i < n ; i ++ )
  46. if( g[v][i] )
  47. adjV.add(i);
  48. return adjV;
  49. }
  50. }

2. 邻接表

  • 邻接表表示无向图

image.png

  • 邻接表表示有向图

image.pngimage.png
image.png

  1. /**
  2. * 稀松图-领接表
  3. */
  4. public class SparseGraph {
  5. private int n; // 节点数
  6. private int m; // 边数
  7. private boolean directed; // 是否为有向图
  8. private Vector<Integer>[] g; // 图的具体数据
  9. // 构造函数
  10. public SparseGraph( int n , boolean directed ){
  11. assert n >= 0;
  12. this.n = n;
  13. this.m = 0; // 初始化没有任何边
  14. this.directed = directed;
  15. // g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
  16. g = (Vector<Integer>[])new Vector[n];
  17. for(int i = 0 ; i < n ; i ++){
  18. g[i] = new Vector<Integer>();
  19. }
  20. }
  21. public int V(){ return n;} // 返回节点个数
  22. public int E(){ return m;} // 返回边的个数
  23. // 向图中添加一个边
  24. public void addEdge( int v, int w ){
  25. assert v >= 0 && v < n ;
  26. assert w >= 0 && w < n ;
  27. g[v].add(w);
  28. if( v != w && !directed ){
  29. g[w].add(v);
  30. }
  31. m ++;
  32. }
  33. // 验证图中是否有从v到w的边 -->时间复杂度:O(n)
  34. boolean hasEdge( int v , int w ){
  35. assert v >= 0 && v < n ;
  36. assert w >= 0 && w < n ;
  37. for( int i = 0 ; i < g[v].size() ; i ++ ){
  38. if( g[v].elementAt(i) == w ){
  39. return true;
  40. }
  41. }
  42. return false;
  43. }
  44. //遍历邻边
  45. //返回图中一个顶点的所有相邻顶点
  46. public Iterable<Integer> adj(int v){
  47. assert v>=0 && v<n;
  48. return g[v];
  49. }
  50. }

图的深度优先遍历(DFS)

深度优先搜索(DFS):从某个顶点v0出发,访问此顶点,然后依次从v0的未被访问的邻接点出发递归地进行同样的DFS,直至图中和v0有路径相通的顶点都被访问,若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

  1. /**
  2. * 图的深度优先遍历
  3. */
  4. public class DepthFirstPaths {
  5. //标记顶点是否被访问过
  6. private boolean[] visited;
  7. //记录路径
  8. private int[] edgeTo;
  9. //起点
  10. private int s;
  11. public DepthFirstPaths(Graph graph,int s){
  12. visited=new boolean[graph.V()];
  13. edgeTo=new int[graph.V()];
  14. for(int i=0;i<graph.V();i++){
  15. edgeTo[i]=-1;
  16. }
  17. this.s=s;
  18. dfs(graph,this.s);
  19. }
  20. //深度优先搜索
  21. private void dfs(Graph graph,int v){
  22. visited[v]=true;
  23. for(int w:graph.adj(v)){
  24. if(!visited[w]){
  25. edgeTo[w]=v; //w--->v的路径
  26. dfs(graph,w);
  27. }
  28. }
  29. }
  30. //是否有到顶点v的路径
  31. private boolean hasPathTo(int v){
  32. return visited[v];
  33. }
  34. //获取从 s->v 的路径
  35. public Iterable<Integer> pathTo(int v){
  36. if(!hasPathTo(v)){
  37. return null;
  38. }
  39. Stack<Integer> path=new Stack<>();
  40. for(int i=v;i!=s;i=edgeTo[i]){
  41. path.push(i);
  42. }
  43. path.push(s);
  44. return path;
  45. }
  46. public void showPath(int v){
  47. Stack<Integer> path= (Stack<Integer>) pathTo(v);
  48. StringBuffer sb=new StringBuffer();
  49. sb.append("[");
  50. while(!path.isEmpty()){
  51. int w=path.peek();
  52. if(path.size()==1){
  53. sb.append(w);
  54. path.pop();
  55. }else{
  56. sb.append(w+"-->");
  57. path.pop();
  58. }
  59. }
  60. sb.append("]");
  61. System.out.println(sb.toString());
  62. }
  63. }

时间复杂度:

  • 领接表:O(V+E)
  • 邻接矩阵:O(V^2)

统计图的连通分量

  1. /**
  2. * 图的dfs应用之统计图的连通分量
  3. */
  4. public class Component {
  5. private Graph graph;
  6. //连通分量的个数
  7. private int num;
  8. private boolean[] visited;
  9. public Component(Graph graph){
  10. this.graph=graph;
  11. visited=new boolean[graph.V()];
  12. num=0;
  13. for(int i=0;i<graph.V();i++){
  14. if(!visited[i]){
  15. dfs(i);
  16. num++;
  17. }
  18. }
  19. }
  20. private void dfs(int v){
  21. visited[v]=true;
  22. for(int w:graph.adj(v)){
  23. if(!visited[w]){
  24. dfs(w);
  25. }
  26. }
  27. }
  28. //获取连通分量个数
  29. public int getNum(){
  30. return num;
  31. }
  32. }

图的广度优先遍历(BFS)

广度优先搜索(BFS):从某个顶点v0出发,访问此顶点之后依次访问v0的各个未访问的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到,若此时图中尚有顶点未被访问,则从另一个未被访问的顶点作为起点,重复上述过程,直到图中所有顶点均被访问过为止。

  1. /**
  2. * 图的广度优先遍历
  3. */
  4. public class BreadthFirstPaths {
  5. //标记顶点是否被访问过
  6. private boolean[] visited;
  7. //记录路径
  8. private int[] edgeTo;
  9. //起点
  10. private int s;
  11. public BreadthFirstPaths(Graph graph, int s){
  12. visited=new boolean[graph.V()];
  13. edgeTo=new int[graph.V()];
  14. for(int i=0;i<graph.V();i++){
  15. edgeTo[i]=-1;
  16. }
  17. this.s=s;
  18. bfs(graph,this.s);
  19. }
  20. //广度优先搜索
  21. private void bfs(Graph graph,int s){
  22. visited[s]=true;
  23. Queue<Integer> queue=new LinkedList<>();
  24. queue.add(s);
  25. while (!queue.isEmpty()){
  26. int v=queue.poll();
  27. for(int w:graph.adj(v)){
  28. if(!visited[w]){
  29. visited[w]=true;
  30. edgeTo[w]=v;
  31. queue.add(w);
  32. }
  33. }
  34. }
  35. }
  36. //是否有到顶点v的路径
  37. private boolean hasPathTo(int v){
  38. return visited[v];
  39. }
  40. //获取从 s->v 的路径
  41. public Iterable<Integer> pathTo(int v){
  42. if(!hasPathTo(v)){
  43. return null;
  44. }
  45. Stack<Integer> path=new Stack<>();
  46. for(int i=v;i!=s;i=edgeTo[i]){
  47. path.push(i);
  48. }
  49. path.push(s);
  50. return path;
  51. }
  52. public void showPath(int v){
  53. Stack<Integer> path= (Stack<Integer>) pathTo(v);
  54. StringBuffer sb=new StringBuffer();
  55. sb.append("[");
  56. while(!path.isEmpty()){
  57. int w=path.peek();
  58. if(path.size()==1){
  59. sb.append(w);
  60. path.pop();
  61. }else{
  62. sb.append(w+"-->");
  63. path.pop();
  64. }
  65. }
  66. sb.append("]");
  67. System.out.println(sb.toString());
  68. }
  69. }

无权图的最短路径