图论基本概念
一、顶点(vertex)
带数字的点就是顶点,表示某个事物或对象。由于图的术语没有标准化,可以称顶点为点、节点、结点、端点等。
顶点的度=入度+出度
二、边(edge)
顶点之间的线条就是边,表示事物与事物之间的关系。 边表示的是顶点之间的逻辑关系
三、同构(Isomorphism )
从图(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)是不同的。有向图和无向图的许多原理和算法是相通的。
五、权重(weight)
边的权重(或者称为权值、开销、长度等),也是一个非常核心的概念,即每条边都有与之对应的值。
有时候为了应对特殊情况,边的权重可以是零或者负数。
六、路径/最短路径(path/shortest path)
在图上任取两顶点,分别作为起点(start vertex)和终点(end vertex),我们可以规划许多条由起点到终点的路线。不会来来回回绕圈子、不会重复经过同一个点和同一条边的路线,就是一条“路径”。两点之间存在路径,则称这2个顶点是连通的(connected)。
路径也有权重。路径经过的每一条边,沿路加权重,权重总和就是路径的权重(通常只加边的权重,而不考虑顶点的权重)。在路网中,路径的权重,可以想象成路径的总长度。在有向图中,路径还必须跟随边的方向。
值得注意的是,一条路径包含了顶点和边,因此路径本身也构成了图结构,只不过是一种特殊的图结构。
七、环(loop)
环,也成为环路,是一个与路径相似的概念。在路径的终点添加一条指向起点的边,就构成一条环路。通俗点说就是绕圈。
八、连通图/连通分量(connected graph/connected component)
如果在图G中,任意2个顶点之间都存在路径,那么称G为连通图。
上面这张图中,顶点8和顶点2之间就不存在路径,因此它不是一个连通图,当然该图中还有很多顶点之间不存在路径。
上图虽然不是一个连通图,但它有多个连通子图:1,2,3顶点构成一个连通子图,1,2,3,4,5顶点构成的子图是连通图,6,7,8,9顶点构成的子图也是连通图,当然还有很多子图。
一个图的最大连通子图称为它的连通分量。1,2,3,4,5顶点构成的子图就是该图的最大连通子图,也就是连通分量。
连通分量有如下特点:
- 是子图
- 子图是连通的
- 子图含有最大顶点数
注意:最大连通子图(连通分量)是其本身。
图的表示
// 图的接口
public interface Graph {
public int V();
public int E();
public void addEdge(int v, int w);
boolean hasEdge(int v, int w);
void show();
public Iterable<Integer> adj(int v);
}
1. 邻接矩阵
- 邻接矩阵表示无向图
- 邻接矩阵表示有向图
/**
* 稠密图-邻接矩阵
*/
public class DenseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private boolean[][] g; // 图的具体数据
// 构造函数
public DenseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n*n的布尔矩阵, 每一个g[i][j]均为false, 表示没有任和边
// false为boolean型变量的默认值
g = new boolean[n][n];
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
if( hasEdge( v , w ) ){
return;
}
g[v][w] = true;
if( !directed ){
g[w][v] = true;
}
m ++;
}
// 验证图中是否有从v到w的边
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
return g[v][w];
}
//遍历邻边
//返回图中一个顶点的所有相邻顶点
public Iterable<Integer> adj(int v) {
assert v >= 0 && v < n;
Vector<Integer> adjV = new Vector<Integer>();
for(int i = 0 ; i < n ; i ++ )
if( g[v][i] )
adjV.add(i);
return adjV;
}
}
2. 邻接表
- 邻接表表示无向图
- 邻接表表示有向图
/**
* 稀松图-领接表
*/
public class SparseGraph {
private int n; // 节点数
private int m; // 边数
private boolean directed; // 是否为有向图
private Vector<Integer>[] g; // 图的具体数据
// 构造函数
public SparseGraph( int n , boolean directed ){
assert n >= 0;
this.n = n;
this.m = 0; // 初始化没有任何边
this.directed = directed;
// g初始化为n个空的vector, 表示每一个g[i]都为空, 即没有任和边
g = (Vector<Integer>[])new Vector[n];
for(int i = 0 ; i < n ; i ++){
g[i] = new Vector<Integer>();
}
}
public int V(){ return n;} // 返回节点个数
public int E(){ return m;} // 返回边的个数
// 向图中添加一个边
public void addEdge( int v, int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
g[v].add(w);
if( v != w && !directed ){
g[w].add(v);
}
m ++;
}
// 验证图中是否有从v到w的边 -->时间复杂度:O(n)
boolean hasEdge( int v , int w ){
assert v >= 0 && v < n ;
assert w >= 0 && w < n ;
for( int i = 0 ; i < g[v].size() ; i ++ ){
if( g[v].elementAt(i) == w ){
return true;
}
}
return false;
}
//遍历邻边
//返回图中一个顶点的所有相邻顶点
public Iterable<Integer> adj(int v){
assert v>=0 && v<n;
return g[v];
}
}
图的深度优先遍历(DFS)
深度优先搜索(DFS):从某个顶点v0出发,访问此顶点,然后依次从v0的未被访问的邻接点出发递归地进行同样的DFS,直至图中和v0有路径相通的顶点都被访问,若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。
/**
* 图的深度优先遍历
*/
public class DepthFirstPaths {
//标记顶点是否被访问过
private boolean[] visited;
//记录路径
private int[] edgeTo;
//起点
private int s;
public DepthFirstPaths(Graph graph,int s){
visited=new boolean[graph.V()];
edgeTo=new int[graph.V()];
for(int i=0;i<graph.V();i++){
edgeTo[i]=-1;
}
this.s=s;
dfs(graph,this.s);
}
//深度优先搜索
private void dfs(Graph graph,int v){
visited[v]=true;
for(int w:graph.adj(v)){
if(!visited[w]){
edgeTo[w]=v; //w--->v的路径
dfs(graph,w);
}
}
}
//是否有到顶点v的路径
private boolean hasPathTo(int v){
return visited[v];
}
//获取从 s->v 的路径
public Iterable<Integer> pathTo(int v){
if(!hasPathTo(v)){
return null;
}
Stack<Integer> path=new Stack<>();
for(int i=v;i!=s;i=edgeTo[i]){
path.push(i);
}
path.push(s);
return path;
}
public void showPath(int v){
Stack<Integer> path= (Stack<Integer>) pathTo(v);
StringBuffer sb=new StringBuffer();
sb.append("[");
while(!path.isEmpty()){
int w=path.peek();
if(path.size()==1){
sb.append(w);
path.pop();
}else{
sb.append(w+"-->");
path.pop();
}
}
sb.append("]");
System.out.println(sb.toString());
}
}
时间复杂度:
- 领接表:O(V+E)
- 邻接矩阵:O(V^2)
统计图的连通分量
/**
* 图的dfs应用之统计图的连通分量
*/
public class Component {
private Graph graph;
//连通分量的个数
private int num;
private boolean[] visited;
public Component(Graph graph){
this.graph=graph;
visited=new boolean[graph.V()];
num=0;
for(int i=0;i<graph.V();i++){
if(!visited[i]){
dfs(i);
num++;
}
}
}
private void dfs(int v){
visited[v]=true;
for(int w:graph.adj(v)){
if(!visited[w]){
dfs(w);
}
}
}
//获取连通分量个数
public int getNum(){
return num;
}
}
图的广度优先遍历(BFS)
广度优先搜索(BFS):从某个顶点v0出发,访问此顶点之后依次访问v0的各个未访问的邻接点,然后分别从这些邻接点出发依次访问它们的邻接点,并使“先被访问的顶点的邻接点”先于“后被访问的顶点的邻接点”被访问,直至图中所有已被访问的顶点的邻接点都被访问到,若此时图中尚有顶点未被访问,则从另一个未被访问的顶点作为起点,重复上述过程,直到图中所有顶点均被访问过为止。
/**
* 图的广度优先遍历
*/
public class BreadthFirstPaths {
//标记顶点是否被访问过
private boolean[] visited;
//记录路径
private int[] edgeTo;
//起点
private int s;
public BreadthFirstPaths(Graph graph, int s){
visited=new boolean[graph.V()];
edgeTo=new int[graph.V()];
for(int i=0;i<graph.V();i++){
edgeTo[i]=-1;
}
this.s=s;
bfs(graph,this.s);
}
//广度优先搜索
private void bfs(Graph graph,int s){
visited[s]=true;
Queue<Integer> queue=new LinkedList<>();
queue.add(s);
while (!queue.isEmpty()){
int v=queue.poll();
for(int w:graph.adj(v)){
if(!visited[w]){
visited[w]=true;
edgeTo[w]=v;
queue.add(w);
}
}
}
}
//是否有到顶点v的路径
private boolean hasPathTo(int v){
return visited[v];
}
//获取从 s->v 的路径
public Iterable<Integer> pathTo(int v){
if(!hasPathTo(v)){
return null;
}
Stack<Integer> path=new Stack<>();
for(int i=v;i!=s;i=edgeTo[i]){
path.push(i);
}
path.push(s);
return path;
}
public void showPath(int v){
Stack<Integer> path= (Stack<Integer>) pathTo(v);
StringBuffer sb=new StringBuffer();
sb.append("[");
while(!path.isEmpty()){
int w=path.peek();
if(path.size()==1){
sb.append(w);
path.pop();
}else{
sb.append(w+"-->");
path.pop();
}
}
sb.append("]");
System.out.println(sb.toString());
}
}