文章目录

  • 课程表(环检测算法)
  • 1. DFS
  • 2. BFS
  • 课程表 II(拓扑序列)
  • 1. DFS
  • 2. BFS
  • 课程表 IV(记忆化搜索)
  • 1. DFS
  • 2. BFS


课程表(环检测算法)

【拓扑排序】课程表系列_有向图

1. DFS

先修课程之间的关系可以用有向图表示,用哈希表存储邻接表,很显然,当有向图成环时无法完成所有的课程

【拓扑排序】课程表系列_数组_02


环检测算法需要使用两个数组,分别是 visited 和 path,visited数组用来记录遍历过的节点,而path用于记录正在遍历的路径且没有回溯到的节点。如果遍历某条支路时又到达某path数组记录为true的节点,说明成环了

比如上图中,根据dfs,从0出发后会遍历到1、3,然后会遍历2(visited[2]=true、path[2]=true),遍历3(visited[3]=true、path[3]=true),遍历4(visited[4]=true、path[4]=true),遍历2,然而此时发现path[2]=true,出现环!

下图描述了遍历的过程,在 visited 中被标记为 true 的节点用灰色表示,在 path 中被标记为 true 的节点用绿色表示

【拓扑排序】课程表系列_深度优先_03


图源

class Solution {
public:
    unordered_map<int, vector<int>> mp;
    vector<bool> visited;
    vector<bool> path;
    bool valid;

    void dfs(int start){
        if(path[start]){
            valid = false;
            return;
        }
        if(visited[start]) return;
        visited[start] = true;
        path[start] = true;
        for(int end : mp[start]){
        	// 判断条件不能是:valid && !visited[end],因为要检测环
            if(valid) dfs(end);
        }
        path[start] = false;
    }

    bool canFinish(int n, vector<vector<int>>& prerequisites) {
        visited.resize(n, false);
        path.resize(n, false);
        valid = true;
        for(auto end_start : prerequisites){
            mp[end_start[1]].push_back(end_start[0]);
        }
        // 由于图不像树一样有根节点,图没有入口节点,需要使用for循环试探每一个节点
        for(int i = 0; i < n; i++){
            if(valid) dfs(i);
        }
        return valid;
    }
};

2. BFS

BFS 算法的思路:

  1. 构建邻接表,数据结构为哈希表 + 数组,unordered_map<int, vector<int>>
  2. 构建一个 indegree 数组记录每个节点的入度,即 indegree[i] 记录节点 i 的入度
  3. 对 BFS 队列进行初始化,入度为0说明没有先修课程,将入度为 0 的节点首先入队
  4. 开始执行 BFS ,不断弹出队列中的节点(表示修完一门课),减少相邻节点的入度(表示先修课程 - 1),并将入度变为 0 的节点加入队列
  5. 如果最终所有节点都被遍历过(count 等于节点数),则说明不存在环,反之则说明存在环。
class Solution {
public:
    bool canFinish(int n, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> mp;
        vector<int> indegree(n, 0);
        for(auto end_start : prerequisites){
            indegree[end_start[0]]++;
            mp[end_start[1]].push_back(end_start[0]);
        }
        queue<int> q;
        // 入度为0,说明不需要依赖其他课程,优先遍历
        // 先将入度为0的节点放入队列
        for(int i = 0; i < n; i++){
            if(indegree[i] == 0){
                q.push(i);
            }
        }
        vector<int> res;
        while(!q.empty()){
            int start = q.front();
            q.pop();
            res.push_back(start); // 出队顺序就是拓扑序
            for(int end : mp[start]){
                // 由于start课程已经修了,将end课程的入度 - 1,表示依赖的课程少一个
                indegree[end]--;
                if(indegree[end] == 0){
                    // 入度为0,说明end课程的先修课程全部学完,可以放入队列遍历
                    q.push(end);
                }
            }
        }
        return res.size() == n;       
    }
};

课程表 II(拓扑序列)

【拓扑排序】课程表系列_数组_04

1. DFS

【拓扑排序】课程表系列_算法_05

后序遍历反转就得到结果

class Solution {
public:
    // 出现环,返回空数组,否则返回拓扑序
    unordered_map<int, vector<int>> mp;
    vector<bool> visited;
    vector<bool> path;
    vector<int> postorder;
    bool valid;

    void dfs(int start){
        if(path[start]){
            valid = false;
            return;
        }
        if(visited[start]) return;
        
        visited[start] = true;
        path[start] = true;
        for(int end : mp[start]){
            if(valid) dfs(end);
        }
        path[start] = false;
        
        postorder.push_back(start);
    }

    vector<int> findOrder(int n, vector<vector<int>>& prerequisites) {
        visited.resize(n, false);
        path.resize(n, false);
        valid = true;
        for(auto end_start : prerequisites){
            mp[end_start[1]].push_back(end_start[0]);
        }
        for(int i = 0; i < n; i++){
            if(valid) dfs(i);
        }
        if(!valid) return {};
        reverse(postorder.begin(), postorder.end());
        return postorder;
    }
};

2. BFS

首先使用indegree数组记录每个节点的入度,将入度为0(表示没有先修课程)的节点放入队列,开始BFS

从队列中取出节点加入数组ans(拓扑序),将该节点能直达的节点end对应的indegree[end]减1,表示end的先修课程少了一门,如果indegree[end]为0,表示end对应的先修课程全部学完,可以加入队列遍历

遍历完后ans的长度若为n,则可以修完所有课程,否则有向图存在环,无法修完所有课程

class Solution {
public:
    vector<int> findOrder(int n, vector<vector<int>>& prerequisites) {
        unordered_map<int, vector<int>> mp;
        vector<int> indegree(n, 0);
        for(auto end_start : prerequisites){
            indegree[end_start[0]]++;
            mp[end_start[1]].push_back(end_start[0]);
        }
        queue<int> q;
        // 入度为0,说明不需要依赖其他课程,优先遍历
        // 先将入度为0的节点放入队列
        for(int i = 0; i < n; i++){
            if(indegree[i] == 0){
                q.push(i);
            }
        }
        vector<int> ans;
        while(!q.empty()){
            int start = q.front();
            q.pop();
            ans.push_back(start); // 出队顺序就是拓扑序
            for(int end : mp[start]){
                // 由于start课程已经修了,将end课程的入度 - 1,表示依赖的课程少一个
                indegree[end]--;
                if(indegree[end] == 0){
                    // 入度为0,说明end课程的先修课程全部学完,可以放入队列遍历
                    q.push(end);
                }
            }
        }
        if(ans.size() != n) return {};
        return ans;        
    }
};

课程表 IV(记忆化搜索)

【拓扑排序】课程表系列_有向图_06


【拓扑排序】课程表系列_有向图_07

1. DFS

使用一个二维数组isReach表示start和end之间是否可达,其中1表示可达、-1表示不可达、0表示未定

搜索结果保存在二维数组isReach中,下一次搜索可以利用isReach数组

class Solution {
public:
    unordered_map<int, vector<int>> mp;
    vector<vector<int>> isReach; // 1表示可达、-1表示不可达、0表示未定

    bool dfs(int start, int end){
        if(isReach[start][end] == 1) return true;
        if(isReach[start][end] == -1) return false;
        for(int mid : mp[start]){
            if(dfs(mid, end)){
                isReach[start][end] = 1;
                return true;
            }
        }
        isReach[start][end] = -1;
        return false;
    }

    vector<bool> checkIfPrerequisite(int n, vector<vector<int>>& prerequisites, vector<vector<int>>& queries) {
        isReach.resize(n, vector<int>(n, 0));
        for(auto start_end : prerequisites){
            mp[start_end[0]].push_back(start_end[1]);
            isReach[start_end[0]][start_end[1]] = 1;
        }
        for(int i = 0; i < n; i++){
            isReach[i][i] = 1;
        }
        vector<bool> ans;
        for(auto q : queries){
            ans.push_back(dfs(q[0], q[1]));
        }
        return ans;
    }
};

2. BFS

每个节点都作为源点,把源点src加入队列中开始BFS,遍历到的所有节点mid,都将isReach[src][mid]都置为true

class Solution {
public:
    unordered_map<int, vector<int>> mp;
    vector<vector<int>> isReach;

    bool bfs(int src, int end){
        if(isReach[src][end] == 1) return true;
        if(isReach[src][end] == -1) return false;
        queue<int> q;
        q.push(src);
        while(!q.empty()){
            int start = q.front();
            q.pop();
            for(int mid : mp[start]){
                if(!isReach[src][mid]){
                    isReach[src][mid] = 1;  // src -> start可达,start -> mid可达,则src -> mid可达
                    q.push(mid);
                }
            }
        }
        if(isReach[src][end] == 1) return true;
        isReach[src][end] = -1;  // 如果BFS遍历中,isReach[src][end]没被置为1,则说明不可达
        return false;
    }

    vector<bool> checkIfPrerequisite(int n, vector<vector<int>>& prerequisites, vector<vector<int>>& queries) {
        isReach.resize(n, vector<int>(n, 0));
        for(auto start_end : prerequisites){
            mp[start_end[0]].push_back(start_end[1]);
        }
        vector<bool> ans;
        for(auto q : queries){
            ans.push_back(bfs(q[0], q[1]));
        }
        return ans;
    }
};