图论:出、入度,邻接表、邻接矩阵、拓扑排序\207. Course Schedule
2017-02-17 15:37
477 查看
出入度
邻接表邻接矩阵
拓扑排序
DFS
Kahn算法
Course Schedule
题目描述
代码实现
转载请注明出处:http://blog.csdn.net/c602273091/article/details/55511145
各种图的概念请看:【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】中是认为无向图中一条边既是入度、也是出度。所以这里的计算会有些许不同。
其实邻接表和邻接矩阵是离散数学的必修内容,现在就是对它进行一个复习。
首先邻接矩阵的纵轴坐标就是各个边的初始顶点,横坐标就是各个边的箭头的的位置。如果存在i->j这条边,那么就使矩阵M(i, j) = 1,否则为0。
邻接表是另外一种记录图的数据结构。因为图是稀疏的话,那么邻接矩阵就会使得存储很浪费,使用邻接表存储更加节约空间。如果图是稠密的话,使用邻接表是一种不错的方式。
在这里强烈推荐这个博客,写得非常好,非常简单易懂【3】【8】。
在图的存储结构中,还有十字链表、邻接多重表、边集数组。
边集数组:边集数组是由两个一维数组构成,一个是存储顶点的信息,另一个是存储边的信息,这个边数组每个数据元素由一条边的起点下标(begin)、终点下标(end)和权(weight)组成【9】。
另外在【11】中,对图的表示举了比较简单的例子。
在计算拓扑图方面,有DFS和Kahn算法。这里我主要是参考了【4】
具体的代码实现为:
它的具体实现为:
在【4】中还涉及了哈密顿路径,可以看看,写得不错。作者总结了其中DFS和Kahn的算法不同点以及前提条件。DFS是要先证明不存在环才可以使用,Kahn不需要。
DFS的检测闭环和拓扑排序写在一起就是:
这两种方法的复杂度都是O(E+V)。
最后推荐程序员必须会的10种算法这篇文章:【5】
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:
There are a total of 2 courses to take. To take course 1 you should have finished course 0. So it is possible.
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.
上面使用的就是Kahn算法实现的课程调度。
在【10】里面,提出了BFS和DFS的实现。
BFS:
DFS:
参考链接:
对于各种算法的一个总结:
【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
邻接表邻接矩阵
拓扑排序
DFS
Kahn算法
Course Schedule
题目描述
代码实现
转载请注明出处: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
相关文章推荐
- 数据结构实验图论:基于邻接矩阵/邻接表的广度优先搜索遍历(BFS)
- 邻接矩阵求有向图各顶点的入度和出度 (图论基础)
- 邻接表求有向图各顶点的入度和出度 (图论基础)
- 数据结构实验图论:基于邻接矩阵/邻接表的广度优先搜索遍历
- 图算法 图的表示(邻接表和邻接矩阵)和拓扑排序
- 数据结构实验图论一:基于邻接矩阵/邻接表的广度优先搜索遍历
- 有向图与无向图的邻接矩阵,邻接表 计算出度与入度
- leetcode 207. Course Schedule 课程调度 + 拓扑排序
- LeetCode 207. Course Schedule(拓扑排序-求有向图中是否存在环)
- LeetCode OJ 207. Course Schedule 拓扑排序+邻接表
- LeetCode 207. Course Schedule(拓扑排序)
- 拓扑排序-207. Course Schedule[medium]
- 数据结构实验图论一:基于邻接矩阵的广度优先搜索遍历
- leetcode 207. Course Schedule
- 用邻接矩阵存储有向图,并输出各顶点的出入和入度。
- 关于邻接表的拓扑排序
- 邻接矩阵和邻接表表示
- 2142 数据结构实验之图论二:基于邻接表的广度优先搜索遍历
- 数据结构实验之图论二:基于邻接表的广度优先搜索遍历
- 基于邻接矩阵和邻接表的两种方法实现无向图的BFS和DFS