拓扑排序C++

拓扑排序C++

几个基本概念的介绍

入度和出度

图中的度:所谓顶点的度(degree),就是指和该顶点相关联的边数。在有向图中,度又分为入度和出度。

入度 (in-degree) :以某顶点为弧头,终止于该顶点的边的数目称为该顶点的入度

出度 (out-degree) :以某顶点为弧尾,起始于该顶点的弧的数目称为该顶点的出度

邻接表

邻接表,存储方法跟树的孩子链表示法相类似,是一种顺序分配和链式分配相结合的存储结构。如这个表头结点所对应的顶点存在相邻顶点,则把相邻顶点依次存放于表头结点所指向的单向链表中。

  • 在有向图中,描述每个点向别的节点连的边(点a->点b这种情况)。

  • 在无向图中,描述每个点所有的边(点a-点b这种情况)

LeetCode习题

207. 课程表

解题思路

本题可约化为: 课程安排图是否是有向无环图(DAG)。即课程间规定了前置条件,但不能构成任何环路,否则课程前置条件将不成立。
思路是通过 拓扑排序 判断此课程安排图是否是有向无环图(DAG) 。

拓扑排序原理: 对 DAG 的顶点进行排序,使得对每一条有向边 ( u , v ) (u,v) (u,v),均有 u u u(在排序记录中)比 v v v 先出现。亦可理解为对某点 v v v 而言,只有当 v v v 的所有源点均出现了, v v v 才能出现。

通过课程前置条件列表 prerequisites 可以得到课程安排图的邻接表 adjacency,以降低算法时间复杂度,以下两种方法都会用到邻接表。

方法一:入度表(BFS)

本方法中几个数据结构的含义:

  • vector<vector<int>> prerequisites 题目给出参数,其中每个元素 p 是一个依赖关系 p[0] 依赖于 p[1] ,在有向图中,应该是 p[1]->p[0]

  • vector<int> degress 记录所有节点的入度

  • vector<vector<int>> adjacents 邻接表,长度为总课程数,下标 i i i 的元素存放所有依赖节点 i i i 的节点

  • queue<int> zeros 存放所有目前入度为 0 的顶点

算法流程:

  1. 统计课程安排图中每个节点的入度,生成 入度表 indegrees
  2. 借助一个队列 queue,将所有入度为 0 (没有任何依赖)的节点入队。
  3. queue 非空时,依次将队首节点出队,在课程安排图中删除此节点 pre
    • 并不是真正从邻接表中删除此节点 pre,而是将此节点邻接表对应所有邻接节点 cur,即所有以来该节点的节点的入度 −1,即 indegrees[cur] -= 1
    • 当入度 −1 后邻接节点 cur 的入度为 0,说明 cur 所有的前驱节点(依赖节点)已经被 “删除”,此时将 cur 入队。
  4. 在每次 pre 出队时,执行 numCourses--
    • 若整个课程安排图是有向无环图(即可以安排),则所有节点一定都入队并出队过,即完成拓扑排序。换个角度说,若课程安排图中存在环,一定有节点的入度始终不为 0。
    • 因此,拓扑排序出队次数等于课程个数,返回 numCourses == 0 判断课程是否可以成功安排。
class Solution {
public:
    bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
		vector<int> degrees(numCourses, 0);		// 记录所有顶点的入度,未初始化的为0
		vector<vector<int>> adjacents(numCourses);	// 邻接表
		queue<int> zero;	// 零入度的顶点
		int num = numCourses;
		for (int i=0; i<prerequisites.size(); ++i) {
			degrees[prerequisites[i][0]]++;		// 入顶点
			adjacents[prerequisites[i][1]].push_back(prerequisites[i][0]);		// 出顶点
		}
		for (int i=0; i<numCourses; ++i) {
			if (degrees[i] == 0) {
				zero.push(i);		// 入度为0的先入队列
				--num;
			}
		}
		while (!zero.empty()) {
			int temp = zero.front();
			zero.pop();
			for (int j=0; j<adjacents[temp].size(); ++j) {
				if (--degrees[adjacents[temp][j]] == 0) {
					zero.push(adjacents[temp][j]);
					--num;
				}
			}
		}
		if (num == 0) return true;
		else return false;
    }
};

方法二:DFS

原理是通过 DFS 判断图中是否有环。

算法流程:

  1. 借助一个标志列表 flag,用于判断每个节点 i (课程)的状态:
    1. 未被 DFS 访问:i == 0
    2. 已被其他节点启动的 DFS 访问:i == -1
    3. 已被当前节点启动的 DFS 访问:i == 1
  2. numCourses 个节点依次执行 DFS,判断每个节点起步 DFS 是否存在环,若存在环直接返回 False。DFS 流程:
    • 终止条件:
      1. flag[i] == -1,说明当前访问节点已被其他节点启动的 DFS 访问,无需再重复搜索,直接返回 True。
      2. flag[i] == 1,说明在本轮 DFS 搜索中节点 i 被第 2 次访问,即 课程安排图有环 ,直接返回 False。
    • 将当前访问节点 i 对应 flag[i] 置 1,即标记其被本轮 DFS 访问过;
    • 递归访问当前节点 i 的所有邻接节点 j,当发现环直接返回 False;
    • 当前节点所有邻接节点已被遍历,并没有发现环,则将当前节点 flag 置为 −1 并返回 True。
  3. 若整个图 DFS 结束并未发现环,返回 True。
class Solution {
public:
	bool dfs(vector<vector<int>>& adjacents, vector<int>& flags, int curr) {
		if (flags[curr] == 1) return false;
		else if (flags[curr] == -1) return true;
		flags[curr] = 1;
		for (int i=0; i<adjacents[curr].size(); ++i) {
			if (!dfs(adjacents, flags, adjacents[curr][i])) return false;
		}
		flags[curr] = -1;
		return true;
	}
  bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
		vector<vector<int>> adjacents(numCourses);
		vector<int> flags(numCourses, 0);
		for (vector<int> p: prerequisites) {
			adjacents[p[1]].push_back(p[0]);
		}
		for (int i=0; i<numCourses; ++i) {
			if (!dfs(adjacents, flags, i))  return false;
		}
		return true;
  }
};
210. 课程表 II

与上题思路一致

BFS

class Solution {
private:
    // 存储有向图
    vector<vector<int>> edges;
    // 存储每个节点的入度
    vector<int> indeg;
    // 存储答案
    vector<int> result;

public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
        edges.resize(numCourses);
        indeg.resize(numCourses);
        for (const auto& info: prerequisites) {
            edges[info[1]].push_back(info[0]);
            ++indeg[info[0]];
        }

        queue<int> q;
        // 将所有入度为 0 的节点放入队列中
        for (int i = 0; i < numCourses; ++i) {
            if (indeg[i] == 0) {
                q.push(i);
            }
        }

        while (!q.empty()) {
            // 从队首取出一个节点
            int u = q.front();
            q.pop();
            // 放入答案中
            result.push_back(u);
            for (int v: edges[u]) {
                --indeg[v];
                // 如果相邻节点 v 的入度为 0,就可以选 v 对应的课程了
                if (indeg[v] == 0) {
                    q.push(v);
                }
            }
        }

        if (result.size() != numCourses) {
            return {};
        }
        return result;
    }
};

DFS

class Solution {
private:
	vector<vector<int>> edges;
	vector<int> visited;
	bool valid = true;
	stack<int> S;

	void dfs(int u) {
		visited[u] = 1;
		for (int v : edges[u]) {
			if ( visited[v] == 1) {
				valid = false;
				return;
			}
			else if (visited[v] == 0) {
				dfs(v);
				if (!valid) return;
			}
		}
		visited[u] = 2;
		S.push(u);
	}

public:
    vector<int> findOrder(int numCourses, vector<vector<int>>& prerequisites) {
		edges.resize(numCourses);
		visited.resize(numCourses);
		for (const auto& v : prerequisites) {
			edges[v[1]].push_back(v[0]);
		}
		for (int i=0; i<numCourses && valid; i++) {
			if (!visited[i]) dfs(i);
		}
		if (!valid) return {};
		else {
			vector<int> res;
			while (!S.empty()) {
				res.push_back(S.top());
				S.pop();
			}
			return res;
		}

解题思路参考:https://leetcode-cn.com/problems/course-schedule/solution/course-schedule-tuo-bu-pai-xu-bfsdfsliang-chong-fa/

  • 5
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值