首页 > 编程语言 >言简意赅的 Kahn 和 DFS 拓扑排序 (C++实现)

言简意赅的 Kahn 和 DFS 拓扑排序 (C++实现)

时间:2023-02-02 00:34:12浏览次数:51  
标签:下标 vert C++ DFS Kahn result 顶点 vec

I 邻接表 (Adjacent List) 实现有向图

在类内定义结构体 Vertex (顶点).

Vertex 包含三个成员 :

  • data 表示某顶点内所存放的内容, 类型由模板决定 (默认char)
  • indegree 表示某顶点的入度, 即有向图中有多少条边以该顶点为终点
  • adjList 表示某顶点的邻接表, 即以该顶点为起点接出的所有边 (edge) 的终点的下标 (下标表示一个顶点在内存中存放的位置, 也就是说稍后会用数组存放所有顶点)

adjListstd::list 实现. 对每个顶点来说, 我们并不关注其接出的边的先后顺序. 由于可能的添加/删除边操作较多, 使用链表实现的容器存放目标顶点的下标是合理的.

数据成员

Graph 中唯一定义数据成员 vert_vec, 容器为 std::vector.
vert_vec 存放了有向图中所有的顶点.

举例

若有 Graph 实例 g;
g 中存放了两个顶点, 数据依次为 A 和 B, B 接出一条边至 A, 则有:

  • g.vert_vec[0].data 为 A, g.vert_vec[1].data 为 B;
  • g.vert_vec[0].indegree 为 1, g.vert_vec[1].indegree 为 0;
  • g.vert_vec[0].adjList 为空, g.vert_vec[1].adjList{(Index)0} // A 在 vert_vec 中的下标

其他函数

  1. vertNum() 返回顶点个数.

    STL 容器的 size() 函数的返回值类型为 size_t , 即 unsigned long long. 一般在实现小型算法时我们会强制转换成无符号类型以保留负数区间.

  2. addEdge() 接收两个下标参数, 创建由前者 sbjVert_I 对应顶点连向后者 objVert_I 对应顶点的边;
    具体操作是在 sbjVert_I 对应的 adjList // 邻接表 容器中任意位置插入 objVert_I (也就是下标数值);
    然后将 bjVert_I 对应的 indegree 增加 1 (因为连向它的边增加了一根).

  3. setVert() 就是根据传入下标和数值设置某个顶点的 data.

C++ 实现

template<class _Elem = char>
class Graph
{
public: // Interface
    using Index = long long;
    using Sizet = long long;
    struct Vertex { _Elem data; Sizet indegree; list<Index> adjList; };
    Graph(Sizet vertNum) : vert_vec(vertNum, Vertex {{},0,{}}) {
    }
    constexpr Sizet vertNum() const { 
        return (Sizet)vert_vec.size(); 
    }
    constexpr void addEdge(Index sbjVert_I, Index objVert_I) {
        vert_vec[sbjVert_I].adjList.push_back(objVert_I);
        ++(vert_vec[objVert_I].indegree);
    }
    constexpr void setData(Index vertI, const _Elem& data) {
        vert_vec[vertI].data = data;
    }
    bool topologicalSorting_Kahn(queue<_Elem>& result) { return solution_Kahn(result); }
    bool topologicalSorting_DFS(stack<_Elem>& result) { return solution_DFS(result); }
private:
    bool solution_Kahn(queue<_Elem>& result);
    bool solution_DFS(stack<_Elem>& result);
    void visit_DFS(Index vertI, stack<_Elem>& result,
                vector<bool>& isAdded2Result,
                vector<bool>& isUnderRecursion);
private: // Data Member
    vector<Vertex> vert_vec;
};

II 拓扑排序 (Topological Sorting)

image
简单来说就是将有向图按边的方向排序, 保证任意顶点始终在其所有连接的终点 (如果存在) 的左边.
显而易见, 排序结果不一定唯一.
比如:
A -> B, A -> C
排序结果既可以是 A, B, C, 也可以是 A, C, B
当出现循环, 例如 A -> B, B -> A, 排序是不可能实现的;
因此拓扑排序的一个问题之一就是判断出循环.

III Kahn 算法

流程

建议结合代码看.

  1. 定义两个线性容器 resultzeroIndegreeVertI;
    result 存放排好序的顶点的 data (或者也可以存放顶点下标);
    zeroIndegreeVertI 存放所有入度 (indegree) 为 0 的顶点的下标.
  2. 先遍历一遍 vert_vec, 即存放了所有结点的数组.
    找到所有入度为 0 的结点, 将它们的的下标存放入 zeroIndegreeVertI 中.
  3. zeroIndegreeVertI 不为空, 循环执行以下操作
    a. 从 zeroIndegreeVertI 中取出一个顶点下标 (zeroIndegreeVertI 不再包含被取出的顶点下标).
    b. 遍历被取出的下标对应顶点的邻接表, 将表中指向的所有顶点的入度减 1; 如果某个顶点的入度减 1 后变为 0, 则将该顶点的下标存放入zeroIndegreeVertI.

C++ 实现

bool Graph::solution_Kahn(queue<_Elem>& result) {
    result = {};
    queue<Index> zeroIndegreeVertI_que; // Index of vertex with zero indegree
    for(Index i = 0; i < vertNum(); ++i) {
        if(vert_vec[i].indegree == 0) { zeroIndegreeVertI_que.push(i); }
    }
    while(!zeroIndegreeVertI_que.empty()) {
        Index curVertI = zeroIndegreeVertI_que.front(); zeroIndegreeVertI_que.pop();
        result.push(vert_vec[curVertI].data);
        for(Index linked_I : vert_vec[curVertI].adjList) {
            --(vert_vec[linked_I].indegree);
            if(vert_vec[linked_I].indegree == 0) { zeroIndegreeVertI_que.push(linked_I); }
        }
    }
    if(result.size() == vert_vec.size()) return true; // The graph is DAG.
    else return false; // There is at least one circle.
}

IV DFS 算法

流程

先将所有顶点表记为 Is-Not-Added-to-ResultIs-Not-Under-Recursion.
再将所有被标记为未被添加入结果的顶点调用递归函数 visit_DFS(), 伪代码如下:
----- Persudocode of visit_DFS(Vertex n) -----

if n is marked Is-Added-to-Result
	reutrn
if n is marked Is-Under-Recursion
	stop // There must be at least one circle!
mark n Is-Under-Recursion
for each vertex m in adjacent list of n
	visit_DFS(m)
mark n Is-Not-Under-Recursion
add data of n to result
mark n Is-Added-to-Result

递归只可意会不可言传(不是懒得写了)

C++ 实现

bool Graph::solution_DFS(stack<_Elem>& result) {
    result = {};
    vector<bool> isAdded2Result(vertNum(), false);
    vector<bool> isUnderRecursion(vertNum(), false);
    try {
        for(Index i = 0; i < vertNum(); ++i) {
            if(isAdded2Result[i] == false) { visit_DFS(i, result, isAdded2Result, isUnderRecursion); }
        }
    } catch(int status) { if(status == 1) { result = {}; return false; } }
    return true;
}
void Graph::visit_DFS(Index vertI, stack<_Elem>& result,
                vector<bool>& isAdded2Result,
                vector<bool>& isUnderRecursion) {
    if(isAdded2Result[vertI] == true) return;
    if(isUnderRecursion[vertI] == true) throw 1;
    isUnderRecursion[vertI] = true;
    for(Index linkedI : vert_vec[vertI].adjList) {
        visit_DFS(linkedI, result, isAdded2Result, isUnderRecursion);
    }
    isUnderRecursion[vertI] = false;
    result.push(vert_vec[vertI].data);
    isAdded2Result[vertI] = true;
}

标签:下标,vert,C++,DFS,Kahn,result,顶点,vec
From: https://www.cnblogs.com/jamesnulliu/p/17084581.html

相关文章

  • [C/C++] 简单实现trim函数:删除字符串头尾空格
    记录一下stringtrim(conststring&s){intstart=0,end=s.size()-1;while(start<s.size()&&s[start]==''){start++;}wh......
  • Eclipse c++ 中[Linker error] undefined reference to `WSAStartup@8‘的解决办法
    Eclipsec++中[Linkererror]undefinedreferenceto`WSAStartup@8’的解决办法出现原因:在Eclipse中使用MinGW编译器的API,底层调用的是windows系统的API函数,需要链接win......
  • C/C++ 实现十六进制面值转字符串、字符面值转十六进制、UNICODE与GBK互转,UTF-8与GBK互
    C/C++实现十六进制面值转字符串、字符面值转十六进制、UNICODE与GBK互转,UTF-8与GBK互转(1)ASCII码ASCII码一共规定了128个字符的编码,比如空格“SPACE”是32(二进制00100000),大......
  • C++之Socket简单使用
    在系统目录C:\ProgramFiles(x86)\WindowsKits\10\Lib\10.0.22000.0\um\x64下找到静态库WS2_32.Lib,并将其拷贝至工程lib目录下(这个库里面封装的有socket相关的函数实现......
  • CUDA C++ / 并发CUDA流
    计算与传输重叠工作模式CPU与GPU之间交互有两个引擎:内存复制引擎:负责CPU和GPU之间的数据传输。核函数执行引擎:负责CPU向GPU部署核函数任务。这两个引擎是相......
  • LeetCode合并两个有序链表(/dfs)
    题目将两个升序链表合并为一个新的升序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。约束解法方法一classSolution{public:ListNode......
  • C++日期类[2023-02-01]
    C++日期类[2023-02-01]一、考核题目:设计一个日期类,实现时间的加、减操作。二、基本要求:1.描述设计和相关算法思路;2.类体要定义对日期的初始化构造函数,包含一个重载......
  • C/C++ Socket UDP 广播消息的发送与接收
    C/C++SocketUDP广播消息的发送与接收局域网内全网段广播消息的IP地址为:255.255.255.255,向该IP地址发送广播消息,局域网下的任何网段的客户机都能收到广播。对于发送端,如果......
  • C++ Day11 使用单例模式封装log4cpp
    一、实现log4cpp的封装,使其可以像printf一样使用,测试用例如下: 思路:使用可变模板参数,最终达到的效果是在使用 LogInfo、LogError、LogWarn、LogDebug时候可以传递任意类......
  • 闲散随笔的C++教程(一)——绪
    绪在猴子的世界中,编程语言是很多的。你不一定非要选择C++——没错,这并不是一门非常好学的语言。所以当你看到这个开头时就后悔,还是来得及的。但是看样子你是不准备后悔,或......