发表于 2017-12-19 17:05:22 | 分类于 算法 |

图的相关概念

  • 图,是网络结构的抽象模型。
  • 图,是一组由边连接的节点(或顶点)。
  • 任何二元关系都可以用图来表示

图,在数学及技术上的概念

  • 一个图 G = ( V, E ) 由以下元素组成
    • V:一组顶点
    • E:一组边,连接 V 中的顶点

术语

  • 相邻顶点
    • 由一条边连接在一起的顶点称为相邻顶点
  • 顶点的度
    • 一个顶点的度,是其相邻顶点的数量
  • 路径
    • 是顶点 v <sub>1</sub> ,v<sub>2</sub> , ...v<sub>k</sub> 的一个连续序列。
    • 其中 v<sub>i</sub> , v<sub>i + 1</sub> 是相邻的。
  • 简单路径
    • 要求不包含重复的顶点
    • 环也是一个简单路径
  • 无环
    • 如果图中不存在环,则称该图是无环的。
  • 连通
    • 如果图中每两个顶点间都存在路径,则该图是连通的。
  • 有向图
    • 有向图的边有一个方向
  • 无向图
    • 图的边没有方向
  • 强连通
    • 如果图中每两个顶点间在双向上都存在路径,则该图是强连通的。
  • 加权、未加权
    • 图还可以是未加权的、或是加权的。

图的表示

邻接矩阵

  • 图最常见的实现是邻接矩阵
  • 每个节点都和一个整数相关联,该整数最为数组的索引。
  • 用一个二维数组来表示顶点之间的链接。
  • 如果索引为 i 的节点和索引为 j 的节点相邻,则array [ i ][ j ] === 1,否则,array [ i ][ j ] === 0
  • 缺点
    • 不是强连通的图(稀疏图),如果用邻接矩阵来表示,则矩阵中将会有很多 0,意味着浪费了存储空间来表示不存在的边。
    • 例如,找给定顶点的相邻顶点,即使该顶点只有一个相邻顶点,也不得不迭代一整行。
    • 图中顶点的数量可能会改变,而 二维数组不太灵活。

邻接表

  • 由图中每个顶点的相邻顶点列表组成。
  • 可用列表(数组)、链表、散列表、字典,来表示相邻顶点列表。
  • 邻接表可能对大多数问题来说都是更好的选择,但以上两种表示法都很有用,且它们有着不同的性质
  • 例如,要找出顶点 v 和 w 是否相等,使用邻接矩阵会比较快。 ​

关联矩阵

  • 矩阵的行表示顶点,列表时便。
  • 如果顶点 v 是边 e 的入射点,则 array [ v ] [ e ] === 1,否则array [ v ][ e ] === 0
  • 关联矩阵通常用于边的数量比顶点多的情况,以节省空间和内存。

图的遍历

图遍历

  • 可用来寻找特定的顶点或寻找两个顶点之间的路径
  • 检检查图是否连通
  • 检查图是否含有环等。

广度优先搜索(Breadth-First Search,BFS)

  • 从指定的一个顶点开始遍历图
  • 先访问其所有的相邻点,就像一次访问图的一层。就是宽后深地访问顶点。

深度优先搜索(Depth-First Search,DFS)

  • 从第一个指定的顶点开始遍历图
  • 沿着路径直到这条路径最后一个顶点被访问了
  • 接着原路退回 。并探索下一跳路径。
  • 先深度后广度地访问顶点。

图遍历算法的思想

  • 必须追踪每个第一次访问的节点
  • 并且追踪哪些节点还没有被完全探索。
  • 对于两种图遍历算法,都需要明确指出第一个被访问的顶点。
  • 完全探索一个顶点
    • 要求查看该顶点的每一条边
    • 对于每一条边所连接的没有被访问过的顶点,将其标注为被发现的,并将其加进待访问顶点列表中。
  • 为保证算法的效率,访问每个顶点至多两次。
  • 当要标注已经访问过的顶点时,用三种颜色反应它们的状态
    • 白色:表示该顶点还未被访问
    • 灰色:表示该顶点被访问过,但还未被探索过
    • 黑色:表示该顶点被访问过且被完全探索过。

广度优先搜索、深度优先搜索对比

算法 描述 数据结构
深度优先搜索 通过将顶点存入栈中,顶点是沿着路径被探索的,存在新的相邻顶点就去访问
广度优先搜索 队列 通过将顶点存入队列中,最先入队列的顶点先被探索。

图的创建

function Grap ( ) {
  	//用一个数组来存储图中所有顶点
  	//用一个字典存储邻接表
  	var vertices = [ ] ;
  	var adjList = new Dictionary ( ) ;
  
  	// 向图中添加一个新顶点
  	this.addVertex = function ( v ) {
      	vertices.push( v ) ;
      	adjList.set ( v, [ ] ) ;s
  	} ;
  
  	// 添加顶点之间的边
  	this.addEdge = function ( v, w ) {
      	adjList.get ( v ).push ( w ) ;
     	adjList.get ( w ).push( v ) ;
  	} ;
  
  
  	this.toString = function ( ) {
      	var s = " " ;
      	for ( var i = 0; i < vertices.length; i++ ) {
          	s += vertices [ i ]  + " -> " ;
          	var neighbors = adjList.get ( vertices [ i ] ) ;
          	for ( var j = 0; j < neighbors.length; j++ ) {
              	s += neighbors [ j ] ;
          	}
          	s += " \n " ;
      	}
  	} ;
  
  // 广度优先搜索
  	 // 创建一个队列 Q
  	 // 将 v 标注为被发现的(灰色),并将 v 入队列 Q
  	 // 如果 Q 非空,则运行一下步骤
  		// 1)将 u 从 Q 中出队列
  		// 2)将标注 u 为被发现的(灰色)
  		// 3)将 u 所有未被访问过的临点(白色)入队列;
  		// 4)将 u 标注为已被探索的(黑色)。
  	 var initializeColor = function ( ) {
       	var color = [ ] ;
       	for ( var i = 0; i < vertices.length; i++ ) {
          	color [ vertices [ i ] ] = "white" ;
       	}
       	return color ;
  	 } ;
  	 
  	 this.bfs = function ( v, callback ) {
       	var color = initializeColor ( ) ,
            queue = new Queue ( ) ;
       queue.enqueue ( v ) ;
       
       while ( ! queue.isEmpty ( ) ) {
         	var u = queue.dequeue ( ) ,
                neighbors = adjList.get ( u ) ;
         	color [ u ] = "grey" ;
         	for ( var i = 0; i < negihbors.length; i++ ) {
              	var w = neighbors [ i ] ;
              	if ( color [ w ] === "white") {
                  	color [ w ] = "grey" ;
                  	queue.enqueue ( w ) ;
              	}
         	}
         	color [ u ] = "black" ;
         	if ( callback ) {
              	callback ( u ) ;
         	}
       }
  	 } ;
  
  
  // 深度优先搜索
  	 // 并不需要一个源顶点
  	 // 若图中顶点 v 未访问,则访问该顶点 v
  	 // 要访问顶点 v ,步骤如下:
  		// 1)标注 v 为被发现的(灰色)
  		// 2)对于 v 的所有未访问的邻点 w:
  			// 访问顶点 w
  		// 3)标注 v 为已探索的(黑色)
  	 this.dfs = function ( callback ) {
       	var color = initializeColor ( ) ;
       	for ( var i = 0; i < vertices.length; i++ ) {
          	if ( colo [ vertices [ i ] ] === "white" ) {
              	dfsVisit ( vertices [ i ], color, callback ) ;
          	} 
       	}
  	 } ;
  
  	// 辅助函数 dfsVisit
  	var dfsVisit = function ( u, color, callback ) {
      	color [ u ] = "grey" ;
      	if ( callback ) {
          	callback ( u ) ;
      	}
      	var neighbors = adjList.get ( u ) ;
      	for ( var i = 0; i < neighbors.length; i++ ) {
          	var w = neighbors [ i ] ;
          	if ( color [ w ] === "white") {
              	dfsVisit ( w, color, callback ) ;
          	}
      	}
      	color [ u ] = "black" ;
  	} ;
  
  
}

使用 BFS 寻找最短路径

  • 给定一个图 G 和源顶点 v ,找出对每个顶点 u ,u 和 v 之间最短路径的距离(以边的数量计)。

  • 对于给定顶点 v ,广度优先算法会访问所有与其距离为 1 的顶点,接着是距离为 2 的顶点,以此类推。

  • 所以,可以用广度优先算法来接这个问题。

  • 修改 bfs 方法以返回一些信息。

    • 从 v 到 u 的距离 d [ u ] ;
    • 前溯点 pred [ u ] ,用来推导出从 v 到其他每个顶点s u 的最短路径。
  this.BFS = function ( v ) {
    	
    	var color = initializeColor ( ) ,
          queue = new Queue ( ) ,
          d = [ ] ,
          pred = [ ] ;
    queue.enqueue ( v ) ;
    
    for ( var i = 0; i < vertices.length; i++ ) {
      	d [ vertices [ i ] ] = 0 ;
      	pred [ vertices [ i ] ] = null ;
    }
    
    while ( ! queue.isEmpty ( ) ) {
      	var u = queue.dequeue ( ) ,
              neighbors = adjList.get ( u ) ;
      color [ u ] = "grey" ;
      for ( i = 0; i < neighbors.length; i ++ ) {
        	var w = neighbors [ i ] ;
        	if ( color [ w ] === "white" ) {
            	color [ w ] = "grey" ;
            	d [ w ] = d [ u ] + 1 ;
            	pred [ w ] = u ;
            	queue.enqueue ( w ) ;
        	}
      }
      color [ u ] = "black" ;
    }
    return {
      	distances : d,
      	predecessors :pred 
    } ;
    
  } ;

深入学习最短路径算法

  • 本章中的图不是加权图
  • 如果计算加权图中的最短路径(例如,城市 A 和城市 B 之间的最短路径——GPS 和 Google Maps 中用到的算法),广度优先搜索未必合适
  • Dijkstra's 算法解决了单源最短路径问题
  • Bellman-Ford 算法解决了边权值为负的单源最短路径问题
  • A* 搜索算法解决了求仅一对顶点间的最短路径问题,它用经验法则来加速搜索过程。
  • Floyd-Warshall 算法解决了求所有顶点对间的最短路径问题

探索深度优先算法

  • 对于给定的图 G ,
    • 希望深度优先搜索算法遍历图 G 的所有节点,构建 ”森林“ (有根树的一个集合)以及一组源顶点(根),
    • 并输出两个数组:
      • 发现时间
      • 完成探索时间
    • 可以修改 dfs 方法来返回一些信息:
      • 顶点 u 的发现事件 d [ u ]
      • 当顶点 u 被标注为黑色时, u 的完成探索实践 f [ u ]
      • 顶点 u 的前溯点 p [ u ]
    var time = 0 ;
    this.DFS = function ( ) {
      	var color = initializeColor ( ) ,
            d = [ ] ,
            f = [ ] ,
            p = [ ] ;
      	time = 0 ;
      
      	for ( var i = 0; i < vertices.length; i++ ) {
          	f [ vertices [ i ] ] = 0 ;
          	d [ vertices [ i ] ] = 0;
          	p [ vertices [ i ] ] = null ;
      	}
      	for ( i = 0; i < vertices.length; i++ ) {
          	if ( color [ vertices [ i ] ] === "white" ) {
              	DFSVisit ( vertices [ i ], color, d, f, p)
          	}
        }  
      	return {
          	discocery : d;
          	finished : f,
          	predecessors : p
      	} ;
    } ;

    // 辅助函数
    var DFSVisit = function ( u, color, d, f, p ) {
      	console.log ( "discoveryed " + u ) ;
      	color [ u ] = "grey" ;
      	d [ u ] = ++time ;
      	var neighbors = adjList.get ( u ) ;
      	for ( var i = 0; i < neighbors.length; i++ ) {
          	var w = neighbors [ i ] ;
          	if ( color [ w ] === "white" ) {
              	p [ w ] = u ;
              	DFSVisit ( w, color, d, f, p ) ;
          	}
      	}
      	color [ u ] = "black" ;
      	f [ u ] = ++time ;
      	console.log ( "explored " + u ) ;
    } ;

拓扑排序

当需要编排一些任务或步骤的执行顺序时,这称为拓扑排序(topological sorting)。

拓扑排序只能用于有向无环图(DAG)