算法设计与分析(三)

53.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?

Example1

Input:2, [ [0,1] ]
Output: true
Explanation:There are a total of 2 courses to take.
To take course 0 you should have finished course 1. So it is possible.

Example2

Input:2, [ [1,0] , [0,1] ]
Output: false
Explanation: 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.

思路

这周算法课讲的是图论里面的深度优先搜索算法(Depth-First-Search),这个题就很适合拿来进行练习。如果要用图解出这个问题的话,假设要完成课程0,需要先完成课程1,那么就有一条由0指向1的有向边,把Example1Example2画成图:

2, [ [0,1] ]

01

2, [ [1,0] , [0,1] ]

02

这两个例子还不够直观,再多举两个例子:

3,[ [0,1] , [0,2] , [1,2] ](能完成)

03

3,[ [0,1] , [2,0] , [1,2] ](不能完成)

04

这样我们就能很清楚的看到,当课程中有部分课程互为前置课程,也就是有向图中存在环的时候,我们是不能完成全部课程的,所以思路就很清晰了,从一个节点出发,如果能够再次返回这个节点,那么就证明图中存在着环。下面是我的第一版代码:

class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) 
    {   
        vector<int> mark;	//标记数组,记录这条路径访问过哪些节点
        for (int i = 0; i < numCourses; i++)
        {
            mark.clear();
            if (findCircle(i, prerequisites, mark, arrived)) return false;      
        }
        return true;
    }
    
	//递归函数
    bool findCircle(int num, vector<pair<int, int>>& prerequisites, vector<int>& mark)
    {
      for (int i = 0; i < mark.size(); i++)
        if (mark[i] == num) return true; 	//当前节点已经存在于标记数组,证明有环
      mark.push_back(num);
      for (int i = 0; i < prerequisites.size(); i++)
        if(prerequisites[i].first == num && 
          findCircle(prerequisites[i].second, prerequisites, mark)) return true;
      mark.pop_back();
      return false;
    }
};

这段代码应该是正确的,但提交代码到LeetCode时,不出所料,超出时间限制了。上面的第一版没有任何剪枝,对于一条n个节点的路径,会遍历n次,这就有太多没有必要的重复访问了,课程数为2000时就已经超时。我尝试加入一个最简单的剪枝来解决重复遍历,于是有了下面的第二版代码:

class Solution {
public:
    bool canFinish(int numCourses, vector<pair<int, int>>& prerequisites) 
    {   
        vector<int> arrived;	//标记所有无环路径的节点
        vector<int> mark;
        int i,j;
        for (i = 0; i < numCourses; i++)
        {
          //如果一个节点在一条无环路径上,就不需要从这个节点开始遍历寻找环
          //即使这个节点可能在一个环上,也可以从环上的其他节点开始遍历,依然能够访问到这个节点
          for (j = 0; j < arrived.size(); j++)
            if (arrived[j] == i) break;
          if (j == arrived.size())
          {
            mark.clear();
            if (findCircle(i, prerequisites, mark, arrived)) return false;           
          }
        }
        return true;
    }

    bool findCircle(int num, vector<pair<int, int>>& prerequisites, vector<int>& mark, vector<int>& arrived)
    {
      for (int i = 0; i < mark.size(); i++)
        if (mark[i] == num) return true;
      mark.push_back(num);
      arrived.push_back(num);
      for (int i = 0; i < prerequisites.size(); i++)
        if(prerequisites[i].first == num && 
          findCircle(prerequisites[i].second, prerequisites, mark, arrived)) return true;
      mark.pop_back();
      return false;
    }
};

第二版的代码虽然能够通过审核了,但能够改进的地方还很多。第一,DFS函数写的非常粗糙,很不方便阅读,理解它变得很困难。第二点就是还有很多可以进行剪枝的地方我都没有去考虑,代码的效率一般。DFS是一个很强力的算法工具,原理并不难,但应用于具体问题时非常考验人的算法能力,对于它我也只是一知半解,还有许多需要研究学习的内容,希望下次我能够写出足够优雅的DFS函数。

猜你喜欢

转载自blog.csdn.net/Maple_Lai/article/details/82822566