转载请注明出处:http://blog.csdn.net/c602273091/article/details/55511145
出入度
图(Graph)是由顶点的有穷非空集合和顶点之间边的集合组成,通常表示为:G(V,E),其中,G表示一个图,V是图G中顶点的集合,E是图G中边的集合。
各种图的概念请看:【6】
无向边:若顶点Vi到Vj之间的边没有方向,则称这条边为无向边(Edge),用无序偶(Vi,Vj)来表示。
有向边:若从顶点Vi到Vj的边有方向,则称这条边为有向边,也成为弧(Arc),用有序偶< Vi,Vj>来表示,Vi称为弧尾,Vj称为弧头。
简单图:在图结构中,若不存在顶点到其自身的边,且同一条边不重复出现,则称这样的图为简单图。
无向完全图:在无向图中,如果任意两个顶点之间都存在边,则称该图为无向完全图。含有n个顶点的无向完全图有n*(n-1)/2条边。
有向完全图:在有向图中,如果任意两个顶点之间都存在方向互为相反的两条弧,则称该图为有向完全图。含有n个顶点的有向完全图有n*(n-1)条边。
稀疏图和稠密图:这里的稀疏和稠密是模糊的概念,都是相对而言的,通常认为边或弧数小于n*logn(n是顶点的个数)的图称为稀疏图,反之称为稠密图。
有些图的边或弧带有与它相关的数字,这种与图的边或弧相关的数叫做权(Weight),带权的图通常称为网(Network)。
然后介绍度,以顶点V为头的弧的数目称为V的入度(InDegree),记为ID(V),以V为尾的弧的数目称为V的出度(OutDegree),记为OD(V),因此顶点V的度为TD(V)=ID(V)+OD(V)。一般指的是在有向图(DAG)中,某个顶点,箭头指向它的为入度,从这个顶点出发,指向别的顶点的边就是出度。有几条这样的边,度就是多大。看【7】中的图有详细的介绍。
如果一个有向图恰有一个顶点入度为0,其余顶点的入度均为1,则是一棵有向树。
可以参考【1】中生成出入度的代码。不过【1】中是认为无向图中一条边既是入度、也是出度。所以这里的计算会有些许不同。
邻接表、邻接矩阵
邻接表这个东西也可以看【1】中是怎么写这个邻接矩阵的,我更推荐的是看【2】中鱼c的文章。写得非常好。
其实邻接表和邻接矩阵是离散数学的必修内容,现在就是对它进行一个复习。
首先邻接矩阵的纵轴坐标就是各个边的初始顶点,横坐标就是各个边的箭头的的位置。如果存在i->j这条边,那么就使矩阵M(i, j) = 1,否则为0。
邻接表是另外一种记录图的数据结构。因为图是稀疏的话,那么邻接矩阵就会使得存储很浪费,使用邻接表存储更加节约空间。如果图是稠密的话,使用邻接表是一种不错的方式。
在这里强烈推荐这个博客,写得非常好,非常简单易懂【3】【8】。
在图的存储结构中,还有十字链表、邻接多重表、边集数组。
边集数组:边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成【9】。
另外在【11】中,对图的表示举了比较简单的例子。
拓扑排序
拓扑排序我觉得就是一个有向无环图的问题。有向无环这就是拓扑图的充要条件。
在计算拓扑图方面,有DFS和Kahn算法。这里我主要是参考了【4】
DFS
从wiki上获取它的伪代码为:
L ← Empty list that will contain the sorted nodes
S ← Set of all nodes with no outgoing edges
for each node n in S do
visit(n)
function visit(node n)
if n has not been visited yet then
mark n as visited
for each node m with an edge from m to n do
visit(m)
add n to L
具体的代码实现为:
public class DirectedDepthFirstOrder {
// visited数组,DFS实现需要用到
private boolean[] visited;
// 使用栈来保存最后的结果
private Stack<Integer> reversePost;
/** * Topological Sorting Constructor */
public DirectedDepthFirstOrder(Digraph di, boolean detectCycle)
{
// 这里的DirectedDepthFirstCycleDetection是一个用于检测有向图中是否存在环路的类
DirectedDepthFirstCycleDetection detect = new DirectedDepthFirstCycleDetection(
di);
if (detectCycle && detect.hasCycle())
throw new IllegalArgumentException("Has cycle");
this.visited = new boolean[di.getV()];
this.reversePost = new Stack<Integer>();
for (int i = 0; i < di.getV(); i++)
{
if (!visited[i])
{
dfs(di, i);
}
}
}
private void dfs(Digraph di, int v)
{
visited[v] = true;
for (int w : di.adj(v))
{
if (!visited[w])
{
dfs(di, w);
}
}
// 在即将退出dfs方法的时候,将当前顶点添加到结果集中
reversePost.push(v);
}
public Iterable<Integer> getReversePost()
{
return reversePost;
}
}
Kahn算法
Kahn的伪代码为:
L← Empty list that will contain the sorted elements
S ← Set of all nodes with no incoming edges
while S is non-empty do
remove a node n from S
insert n into L
foreach node m with an edge e from nto m do
remove edge e from thegraph
ifm has no other incoming edges then
insert m into S
if graph has edges then
return error (graph has at least onecycle)
else
return L (a topologically sortedorder)
它的具体实现为:
public class KahnTopological
{
private List<Integer> result; // 用来存储结果集
private Queue<Integer> setOfZeroIndegree; // 用来存储入度为0的顶点
private int[] indegrees; // 记录每个顶点当前的入度
private int edges;
private Digraph di;
public KahnTopological(Digraph di)
{
this.di = di;
this.edges = di.getE();
this.indegrees = new int[di.getV()];
this.result = new ArrayList<Integer>();
this.setOfZeroIndegree = new LinkedList<Integer>();
// 对入度为0的集合进行初始化
Iterable<Integer>[] adjs = di.getAdj();
for(int i = 0; i < adjs.length; i++)
{
// 对每一条边 v -> w
for(int w : adjs[i])
{
indegrees[w]++;
}
}
for(int i = 0; i < indegrees.length; i++)
{
if(0 == indegrees[i])
{
setOfZeroIndegree.enqueue(i);
}
}
process();
}
private void process()
{
while(!setOfZeroIndegree.isEmpty())
{
int v = setOfZeroIndegree.dequeue();
// 将当前顶点添加到结果集中
result.add(v);
// 遍历由v引出的所有边
for(int w : di.adj(v))
{
// 将该边从图中移除,通过减少边的数量来表示
edges--;
if(0 == --indegrees[w]) // 如果入度为0,那么加入入度为0的集合
{
setOfZeroIndegree.enqueue(w);
}
}
}
// 如果此时图中还存在边,那么说明图中含有环路
if(0 != edges)
{
throw new IllegalArgumentException("Has Cycle !");
}
}
public Iterable<Integer> getResult()
{
return result;
}
}
在【4】中还涉及了哈密顿路径,可以看看,写得不错。作者总结了其中DFS和Kahn的算法不同点以及前提条件。DFS是要先证明不存在环才可以使用,Kahn不需要。
DFS的检测闭环和拓扑排序写在一起就是:
public class DirectedDepthFirstTopoWithCircleDetection {
private boolean[] visited;
// 用于记录dfs方法的调用栈,用于环路检测
private boolean[] onStack;
// 用于当环路存在时构造之
private int[] edgeTo;
private Stack<Integer> reversePost;
private Stack<Integer> cycle;
/** * Topological Sorting Constructor */
public DirectedDepthFirstTopoWithCircleDetection(Digraph di)
{
this.visited = new boolean[di.getV()];
this.onStack = new boolean[di.getV()];
this.edgeTo = new int[di.getV()];
this.reversePost = new Stack<Integer>();
for (int i = 0; i < di.getV(); i++)
{
if (!visited[i])
{
dfs(di, i);
}
}
}
private void dfs(Digraph di, int v)
{
visited[v] = true;
// 在调用dfs方法时,将当前顶点记录到调用栈中
onStack[v] = true;
for (int w : di.adj(v))
{
if(hasCycle())
{
return;
}
if (!visited[w])
{
edgeTo[w] = v;
dfs(di, w);
}
else if(onStack[w])
{
// 当w已经被访问,同时w也存在于调用栈中时,即存在环路
cycle = new Stack<Integer>();
cycle.push(w);
for(int start = v; start != w; start = edgeTo[start])
{
cycle.push(v);
}
cycle.push(w);
}
}
// 在即将退出dfs方法时,将顶点添加到拓扑排序结果集中,同时从调用栈中退出
reversePost.push(v);
onStack[v] = false;
}
private boolean hasCycle()
{
return (null != cycle);
}
public Iterable<Integer> getReversePost()
{
if(!hasCycle())
{
return reversePost;
}
else
{
throw new IllegalArgumentException("Has Cycle: " + getCycle());
}
}
public Iterable<Integer> getCycle()
{
return cycle;
}
}
这两种方法的复杂度都是O(E+V)。
最后推荐程序员必须会的10种算法这篇文章:【5】
207. Course Schedule
题目描述
There are a total of n courses you have to take, labeled from 0 to n – 1.
Some courses may have prerequisites, for example to take course 0 you have to first take course 1, which is expressed as a pair: [0,1]
Given the total number of courses and a list of prerequisite pairs, is it possible for you to finish all courses?
For example:
2, [[1,0]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.
2, [[1,0],[0,1]]
There are a total of 2 courses to take. To take course 1 you should have finished course 0, and to take course 0 you should also have finished course 1. So it is impossible.
Note:
The input prerequisites is a graph represented by a list of edges, not adjacency matrices. Read more about how a graph is represented.
You may assume that there are no duplicate edges in the input prerequisites.
代码实现
class Solution {
public:
int findPos(vector<int> rem, int num) {
int remsz = rem.size();
for(int i = 0; i < remsz; i++)
if(rem[i] == num) return i;
return - 1;
}
bool canFinish(int numCourses, vector<pair<int, int>>& pre) {
int num[10000] = {0}, nump = pre.size();
queue<int> stt;
vector<int> rem;
for(int i = 0; i < nump; i++) {
if(num[pre[i].second] == 0) num[pre[i].second] = -1;
if(num[pre[i].first] == -1 || num[pre[i].first] == 0) num[pre[i].first] = 1;
else num[pre[i].first]++;
}
for(int i = 0; i < numCourses; i++) {
if(num[i] == -1) stt.push(i);
else if(num[i] > 0) rem.push_back(i);
}
while(!stt.empty()) {
int remsz = rem.size();
if(!remsz) break;
int tp = stt.front();
stt.pop();
for(int i = 0; i < nump; i++) {
if(pre[i].second == tp) {
num[pre[i].first]--;
if(!num[pre[i].first]) {
stt.push(pre[i].first);
rem.erase(rem.begin() + findPos(rem, pre[i].first));
}
}
}
}
return rem.empty();
}
};
上面使用的就是Kahn算法实现的课程调度。
在【10】里面,提出了BFS和DFS的实现。
BFS:
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph = make_graph(numCourses, prerequisites);
vector<int> degrees = compute_indegree(graph);
for (int i = 0; i < numCourses; i++) {
int j = 0;
for (; j < numCourses; j++)
if (!degrees[j]) break;
if (j == numCourses) return false;
degrees[j] = -1;
for (int neigh : graph[j])
degrees[neigh]--;
}
return true;
}
private:
vector<unordered_set<int>> make_graph(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph(numCourses);
for (auto pre : prerequisites)
graph[pre.second].insert(pre.first);
return graph;
}
vector<int> compute_indegree(vector<unordered_set<int>>& graph) {
vector<int> degrees(graph.size(), 0);
for (auto neighbors : graph)
for (int neigh : neighbors)
degrees[neigh]++;
return degrees;
}
};
DFS:
class Solution {
public:
bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph = make_graph(numCourses, prerequisites);
vector<bool> onpath(numCourses, false), visited(numCourses, false);
for (int i = 0; i < numCourses; i++)
if (!visited[i] && dfs_cycle(graph, i, onpath, visited))
return false;
return true;
}
private:
vector<unordered_set<int>> make_graph(int numCourses, vector<pair<int, int>>& prerequisites) {
vector<unordered_set<int>> graph(numCourses);
for (auto pre : prerequisites)
graph[pre.second].insert(pre.first);
return graph;
}
bool dfs_cycle(vector<unordered_set<int>>& graph, int node, vector<bool>& onpath, vector<bool>& visited) {
if (visited[node]) return false;
onpath[node] = visited[node] = true;
for (int neigh : graph[node])
if (onpath[neigh] || dfs_cycle(graph, neigh, onpath, visited))
return true;
return onpath[node] = false;
}
};
参考链接:
对于各种算法的一个总结:
【1】图的邻接矩阵及出入度的计算方法:http://m.blog.csdn.net/article/details?id=9078919
【2】邻接表和邻接矩阵:http://blog.fishc.com/2523.html
【3】学习数据结构不错的网站:http://blog.fishc.com/category/structure
【4】拓扑排序的介绍:http://m.blog.csdn.net/article/details?id=7714519
【5】程序员十大算法:http://www.wtoutiao.com/p/1c2pI1k.html
【6】图的入门:http://blog.fishc.com/2485.html
【7】图的介绍:http://blog.fishc.com/2499.html
【8】邻接矩阵:http://blog.fishc.com/2514.html
【9】图的特殊的存储结构:http://blog.fishc.com/2535.html
【10】BFS/DFS的课程调度实现:https://discuss.leetcode.com/topic/17273/18-22-lines-c-bfs-dfs-solutions
【11】图的表示:https://www.khanacademy.org/computing/computer-science/algorithms/graph-representation/a/representing-graphs