图论:出、入度,邻接表、邻接矩阵、拓扑排序\207. 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

    原文作者:拓扑排序
    原文地址: https://blog.csdn.net/c602273091/article/details/55511145
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞