无向图

作者:disappearedgod
文章出处:http://blog.csdn.net/disappearedgod/article/details/36467923
时间:2014-5-7


4.1 无向图

边(edge)仅仅是两个顶点(vertex)之间的连接。为了和其他图模型相区别,我们将它称为无向图。
定义
图示由一组顶点和一组能够将两个顶点相连接的边组成的。
就定义而言,顶点叫什么名字并不重要,但我们需要一个方法来指代这些顶点。
在绘制一幅图时,用圆圈表示顶点,用连接两个顶点的线段表示边,这样就能直观地看出图的结构。
特殊的图
我们的定义允许出现两种简单而特殊的情况:
自环,即一条连接一个顶点和其自身的边;
连接同一对顶点的两条边成为平行边
数学家常常将含有平行边的图称为多重图,而将没有平行边或自环的图称为简单图

Anomalies.Our definition allows two simple anomalies:
■ A self-loop is an edge that connects a vertex to itself.
■ Two edges that connect the same pair of vertices are parallel.


4.1.1 术语表

When there is an edge connecting two vertices, we say that the vertices are a djacent to one another and that the edge is incident to both vertices. The degree of a vertex is the number of edges incident to it. A subgraph is a subset of a graph’s edges (and associated vertices) that constitutes a graph. Many computational tasks involve identifying subgraphs of various types. Of particular interest are edges that take us through a sequence of vertices in a graph.
当两个顶点通过一条边相连时,我们称为两个顶点是相邻的,并称该连接依附于这两个顶点。某个顶点的度数即为依附于她的边的总数。子图是由一幅图的所有边的一个子集(以及它们所依附的所有顶点)组成的图。

Definition.A path in a graph is a sequence of vertices connected by edges. A simple path is one with no repeated vertices. A cycle is a path with at least one edge whose first and last vertices are the same. A simple cycle is a cycle with no repeated edges or vertices (except the requisite repetition of the first and last vertices). The length of a path or a cycle is its number of edges.
定义
在图中,路径是由边顺序连接的一系列顶点。简单路径是一条没有重复顶点的路径。换是条至少含有一条边且起点和重点相同的路径。简单环是一条(除了起点和重点必须相同之外)不含有重复顶点和边的环。路径或者环的长度为其中所包含的边数。


Definition. A graph is connected if there is a path from every vertex to every other vertex in the graph. A graph that is not connected consists of a set of connected components, which are maximal connected subgraphs.
定义
如果从任意一个顶点都存在一条路径到达另一个任意顶点,我们称为这幅图是连通图。一副非连通的图由若干连通的部分组成,他们都是极大连通子图
一般来说,要处理一张图就需要一个个地处理它的连通分量。


无环图,是一种不包含环的图。


Definition. A tree is an acyclic connected graph. A disjoint set of trees is called a forest. A spanning tree of a connected graph is a subgraph that contains all of that graph’s vertices and is a single tree. A spanning forest of a graph is the union of spanning trees of its connected components.
定义
是一幅无环连通图。互不相连的 树组成的集合称为森林。连通图的生成树是它的一幅子图,它含有途中的所有顶点且是一棵树。图的生成树森林是它的所有连通子图的生成树的集合。


a graph G with V vertices is a tree if and  only if it satisfies any of the following five conditions:
■ G has V1 edges and no cycles.
■ G has V1 edges and is connected.
■ G is connected, but removing any edge disconnects it.
■ G is acyclic, but adding any edge creates a cycle.
■ Exactly one simple path connects each pair of vertices in G.
当且仅当一幅含有V个顶点的图G满足下列5个条件之一时,他就是一棵树:
G有V-1条边且不含有环;
G有V-1条边却是连通的;
G是连通的,但删除任意一条边都会使它不再连通;
G是无环图,但添加任意一对顶点之间仅存在一条简单路径。

The density of a graph is the proportion of possible pairs of vertices that are connected by edges. A sparse graph has relatively few of the possible edges present; a dense graph has relatively few of the possible edges missing. Generally, we think of a graph as being sparse if its number of different edges is within
a small constant factor of V and as being dense otherwise.
图的密度是指已经连接的顶点对占所有可能被连接的顶点对的比例。在稀疏图中,被连接的顶点对很少,而在稠密图中,只有少部分顶点对之间没有边连接。一般来说,如果一幅图中不同的边的数量只占顶点总数V的一小部分,那么我们就认为这幅图是稀疏的,否则则是稠密的

4.1.2 表示无向图的数据类型



任务实现
计算V的度数
public static int degree(Graph G, int v)
{
    int degree = 0;
    for (int w : G.adj(v)) degree++;
    return degree;
}
计算所有顶点的最大度数
public static int maxDegree(Graph G)
{
    int max = 0;
    for (int v = 0; v < G.V(); v++)
    if (degree(G, v) > max)
        max = degree(G, v);
    return max;
}
计算所有顶点的平均度数
public static int avgDegree(Graph G)
{ return 2 * G.E() / G.V(); }
计算自环的个数
public static int numberOfSelfLoops(Graph G)
{
    int count = 0;
    for (int v = 0; v < G.V(); v++)
    for (int w : G.adj(v))
        if (v == w) count++;
    return count/2; // each edge counted twice
}
图的邻接表的字符串表示(Graph实例)
public String toString()
{
    String s = V + " vertices, " + E + " edges\n";
    for (int v = 0; v < V; v++)
    {
        s += v + ": ";
        for (int w : this.adj(v))
            s += w + " ";
        s += "\n";
    }
    return s;
}




4.1.2.1 图的几种表示方法
图表示法的两个要求
  • 必须为可能的在应用中碰到的各种类型图预留出足够的空间
  • Graph的实例方法的实现一定要快——它们是开发处理图的各种用例基础。
依此图有三种表示的方法:
  • 邻接矩阵:使用V乘以V的布尔矩阵。(True——连接)。不符合第一条原则
  • 边的数组:使用一个Edge类,它含有两个int实例变量。这种表示方法很简洁但不满足第二个条件——要实现adj()需要检查图中的所有边。
  • 邻接表数组:我们可以使用一个以定点为索引的列表数组,其中的每个元素都是和该顶点相邻定点列表。

4.1.2.2 邻接表的数据结构
非稠密图的标准表示为邻接表的数据结构,它将每个定点的所有相邻顶点都保存在该顶点对应的元素所指向的一张链接表。
这种Graph的性质有如下特点:
  • 使用的空间和V+E成正比;
  • 添加一条边所需要的时间为常数;
  • 遍历顶点v的所有相邻顶点所需的时间和v的度数成正比(处理每个相邻顶点所需的时间为常数)



4.1.2.3 图的处理算法的设计模式
我们会为每个任务创建一个相应的类,用例可以创建相应的对象来完成任务。类的构造函数一般会在预处理中构造各种数据结构,以有效地相应用例请求。

public class Search

              Search(Graph G, int s)
find vertices connected to a source vertex s
boolean marked(int v)
is v connected to s?
         int count()
how many vertices are connected to s?

我们起点(source)区分作为参数传递构造函数的定点与图中的其他顶点。
这里作者用这三个函数来让大家了解遍历需要的次数。


4.1.3 深度优先搜索

4.1.3.1 走迷宫
其实我们可以把图看成类似于走迷宫的算法。

Tremaux搜索算法——探索迷宫而不迷路的一种古老的办法(可以追溯到忒修斯和米诺陶的传说)


-----------------------------------------------------------------------------------------------------------------
弥诺斯和雅典人讲和的条件是:雅典人每九年向克里特送入七个童男和七个童女作为贡品,而这些孩子送入后就被关在弥诺斯的迷宫里,任凭怪物弥诺陶洛斯杀害。年轻的忒修斯带着选中的男孩女孩向爱情女神阿佛洛狄忒献礼并祈求护送,她爱上了弥诺斯,并给了他一个线团教他把线的一头栓在迷宫入口,一直牵着线走到弥诺陶洛斯的地方,还给了她一把能杀死弥诺陶洛斯的剑。忒修斯和他的同伴都被送进迷宫,忒修斯杀死了弥诺陶洛斯并靠线团走出来了。然后忒修斯就带着阿里阿德涅逃走了。
-----------------------------------------------------------------------------------------------------------------
探索迷宫中所有的通道:
  • 选择一条没有标记过的通道,在你走过的路上铺一条绳子;
  • 标记所有你第一次路过的路口和通道;
  • 当来到一个标记过的路口和通道时(用绳子)回退到上个路口;
  • 当回退到的路口以没有可走的通道时继续回退。
4.1.3.2 热身
我们先来看看迷宫算法中一些相关的算法吧。由于Graph类中实现了adj这个相邻的概念(也就是爱神阿芙洛狄忒给米诺斯的红绳的概念),所以后两部可以合并,第一部可以省略:
索索一幅图,只需要一个递归方法来遍历所有定点。在访问其中一个顶点时:
  • 将它标记为已访问;
  • 递进地访问它的所有没有被标记过的邻居定点。
这种方法称为深度优先搜索(DFS)。递归方法会标记给定的顶点并调用自己来访问该定点的相邻顶点列表中所有没有被标记过的顶点。

命题A 深度优先搜索标记与起点联通的所有顶点所需的时间和顶点的度数之和成正比。

4.1.3.3 单向通道
在迷宫中会经过一条通道两次(方向不同),在途中我们也会路过每条边两次(在它的两个端点各一次)。

在Tremaux搜索中,要么是第一次访问一条边,要么是沿着它从一个呗标记过的顶点退回。

在无向图的深度优先搜素中,在碰到变v-w时,要么进行递归调用(w没有被标记过),要么跳过这条边(w已经被标记过)。第二次从另一个方向w-v遇到这条边时,总是会忽略它,因为它的另一端v肯定已经被访问过了(在一次遇到这条边的时候)。


4.1.3.4 跟踪深度优先搜索

在跟踪深度优先搜索时,首先要注意的是,算法遍历边和访问顶点的顺序与图的表示是有关的,而不是与图的结构或是算法有关。因为深度优先搜索只会访问和起点连通的顶点。

深度优先搜索中每条边都会被访问两次,且在第二次时总会发现这个顶点已经被标记过。这意味着深度优先搜索的轨迹可能会比你想象的长一倍。

4.1.3.5 深度优先搜素的详细轨迹




4.1.4 寻找路径


public class Search

                            Paths(Graph G, int s)
find paths in G from source s
              boolean hasPathTo(int v)
is there a path from s to v?
Iterable<Integer> pathTo(int v)
path from s to v; null if no such path

4.1.4.1 实现
算法4.1 基于深度优先搜索实现了Paths。它扩展了DepthFirstSearch,添加了一个实例变量edgeTo[] 整型数组来起到Tremaux搜索中绳子的作用。这个数组可以找到从每个与s连通的顶点回到s的路径。他会记住每个顶点到起点的路径,而不是记录当前顶点到起点的路径。 

edgeTo[] 是一棵由父链接表示的树。
public class DepthFirstPaths
{
	private boolean[] marked; // Has dfs() been called for this vertex?
	private int[] edgeTo; // last vertex on known path to this vertex
	private final int s; // source
	public DepthFirstPaths(Graph G, int s)
	{
		marked = new boolean[G.V()];
		edgeTo = new int[G.V()];
		this.s = s;
		dfs(G, s);
	}
	private void dfs(Graph G, int v)
	{
		marked[v] = true;
		for (int w : G.adj(v))
		if (!marked[w])
		{
			edgeTo[w] = v;
			dfs(G, w);
		}
	}
	public boolean hasPathTo(int v)
	{ return marked[v]; }
	public Iterable<Integer> pathTo(int v)
	{
		if (!hasPathTo(v)) return null;
		Stack<Integer> path = new Stack<Integer>();
		for (int x = v; x != s; x = edgeTo[x])
		path.push(x);
		path.push(s);
	return path;
	}
}



4.1.4.2 详细轨迹



命题A(continue)
使用深度优先搜索得到从给定起点到任意标记顶点的路径所需的时间与路径的长度成正比。


4.1.5 广度优先搜索

深度优先搜索得到的路径逼近取决于图的结构,还取决于图的表示和递归调用的性质。我们很自然地还经常对下面这些问题感兴趣。

单点最短路径。给定一幅图G和一个起点S,回到“从s到给定目的顶点v是否存在一条路径?如果有,找出其中最短的那条(所含变数最少)。”等类似问题。

解决这个问题的经典方法加做广度优先搜索(BFS)。

在程序中,在搜索一幅图时遇到有多条边需要遍历的情况时,我们会选择其中一条并将其他通道留到以后再继续搜索(我们用一个可以下压的栈(这是由系统管理的,一直吃递归搜索方法))。使用LIFO(后进先出)的规则来描述压站和走迷宫时先探索相邻的通道类似。

实现







public class BreadthFirstPaths
{
	private boolean[] marked; // Is a shortest path to this vertex known?
	private int[] edgeTo; // last vertex on known path to this vertex
	private final int s; // source
	public BreadthFirstPaths(Graph G, int s)
	{
		marked = new boolean[G.V()];
		edgeTo = new int[G.V()];
		this.s = s;
		bfs(G, s);
	}
	private void bfs(Graph G, int s)
	{
		Queue<Integer> queue = new Queue<Integer>();
		marked[s] = true; // Mark the source
		queue.enqueue(s); // and put it on the queue.
		while (!q.isEmpty())
		{
			int v = queue.dequeue(); // Remove next vertex from the queue.
			for (int w : G.adj(v))
			if (!marked[w]) // For every unmarked adjacent vertex,
			{
				edgeTo[w] = v; // save last edge on a shortest path,
				marked[w] = true; // mark it because path is known,
				queue.enqueue(w); // and add it to the queue.
			}
		}
	}
	public boolean hasPathTo(int v)
	{ return marked[v]; }
	public Iterable<Integer> pathTo(int v)
// Same as for DFS (see page 536).
}

命题B
对于从s可达的任意顶点v,广度优先搜索都能找到一条从s到v的最短路径(没有其他从s到v的路径所含的变比这条路径更少)。
命题B(continue)
官渡优先搜索所需要的时间在最坏情况下和(V+E)成正比。


4.1.6 连通分量

深度优先搜索的下一个直接应用就是找出一幅图的所有连通分量。
public class CC

               CC(Graph G)
preprocessing constructor
boolean connected(int v, int w)
are v and w connected?
         int count()
number of connected components
         int id(int v)
component identifier for v( between 0 and count()-1 )


4.1.6.1 实现
CC的实现使用了marked[] 数组来寻找一个顶点作为每个连通分量中深度优先搜索的起点。

构造函数中的for循环会查找每个没有被标记的顶点并递归调用dfs()来标记和它相邻的所有顶点。

id[],将同一个连通分量中的顶点和连通分量的标识符关联起来(int 值)。


ublic class CC
{
	private boolean[] marked;
	private int[] id;
	private int count;
	public CC(Graph G)
	{
	marked = new boolean[G.V()];
	id = new int[G.V()];
	for (int s = 0; s < G.V(); s++)
	if (!marked[s])
	{
		dfs(G, s);
		count++;
	}
}
private void dfs(Graph G, int v)
{
	marked[v] = true;
	id[v] = count;
	for (int w : G.adj(v))
	if (!marked[w])
	dfs(G, w);
}
public boolean connected(int v, int w)
{ return id[v] == id[w]; }
public int id(int v)
{ return id[v]; }
public int count()
{ return count; }
}

命题C
深度优先搜素的预处理使用的时间和铜件与V+E成正比且可以在常数时间内处理关羽图的连通性查询。

4.1.6.2 union-find 算法


Union-Find 算法解决连通性问题基于深度优先搜索解决连通性问题
优点一种动态的算法(其实更快,源于不需要完整地够制造并表示一幅图),用于只需要判断连通性或者需要完成有大量连通性查询和插入操作混合等类似的任务更适合实现图的抽象数据烈性(利用已有数据结构)
缺点
必须要对图进行预处理

检测环
public class Cycle
{
	private boolean[] marked;
	private boolean hasCycle;
	public Cycle(Graph G)
	{
		marked = new boolean[G.V()];
		for (int s = 0; s < G.V(); s++)
			if (!marked[s])
				dfs(G, s, s);
	}
	private void dfs(Graph G, int v, int u)
	{
		marked[v] = true;
		for (int w : G.adj(v))
			if (!marked[w])
				dfs(G, w, v);
			else if (w != u) hasCycle = true;
	}
	public boolean hasCycle(){ return hasCycle; }
}

二分图

public class TwoColor
{
	private boolean[] marked;
	private boolean[] color;
	private boolean isTwoColorable = true;
	public TwoColor(Graph G)
	{
		marked = new boolean[G.V()];
		color = new boolean[G.V()];
		for (int s = 0; s < G.V(); s++)
			if (!marked[s])	dfs(G, s);
	}
	private void dfs(Graph G, int v)
	{
		marked[v] = true;
		for (int w : G.adj(v))
			if (!marked[w]){
				color[w] = !color[v];
				dfs(G, w);
			}
			else if (color[w] == color[v]) isTwoColorable = false;
	}
	public boolean isBipartite(){ return isTwoColorable; }
}


4.1.7 符号图


4.1.7.1 API



4.1.7.2 测试用例



4.1.7.3 实现



4.1.7.4 间隔的度数



4.1.8 总结


  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值