915-拓扑排序的几种实现方式以及变形

这里给大家总结一下拓扑排序的几种写法或变形:用栈、用队列、基于宽搜、基于深搜、915-23 真题中的改编 topsort 求 level(DAG的最大高度)。

AcWing. 848. 有向图的拓扑序列
Xnip2023-12-20_23-49-32.png

1.基于宽搜的topsort

下面这段代码使用拓扑排序来解决有向图的问题。拓扑排序是一种对有向无环图进行排序的算法,其基本思想是通过顶点之间的有向边的方向来排序,保证在排序结果中,任意一对有向边的起点在终点之前。

具体步骤如下:

  1. 统计每个节点的入度(in-degree),入度表示有多少条边指向该节点。
  2. 将入度为0的节点加入队列。
  3. 对队列中的节点进行遍历,将其邻接节点的入度减1。如果邻接节点入度为0,则加入队列。
  4. 重复步骤3,直到队列为空。
  5. 判断拓扑排序是否成功,即队列中的节点个数是否等于图的节点个数。如果等于,说明有合法的拓扑序列。

如果拓扑排序成功,输出拓扑排序的结果;否则,输出"-1"表示图中存在环,无法进行拓扑排序。

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int d[N]; // 入度数组,记录每个节点的入度
int q[N], hh = 0, tt = -1; // 队列,hh 表示队头,tt 表示队尾

struct Node
{
    int id;
    Node *next;
    Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息

void add(int a, int b)
{
    Node *p = new Node(b);
    p->next = head[a];
    head[a] = p;
}

bool bfs()
{
    // 将入度为 0 的节点加入队列
    for (int i = 1; i <= n; i++)
        if (!d[i])
            q[++tt] = i;

    while (hh <= tt)
    {
        int t = q[hh++];

        for (Node *p = head[t]; p; p = p->next)
        {
            int j = p->id;
            if (--d[j] == 0)
                q[++tt] = j;
        }
    }

    // 如果队列中的节点个数等于图的节点个数,说明拓扑排序成功
    return tt == n - 1;
}

int main()
{
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        d[b]++; // 记录每个节点的入度
    }

    if (!bfs())
        puts("-1");
    else
    {
        // 输出拓扑排序的结果
        for (int i = 0; i < n; i++)
            cout << q[i] << ' ';
        cout << endl;
    }

    return 0;
}

Xnip2023-12-20_23-24-39.png

2.将队列改为栈进行拓扑排序

有同学说将队列改为栈后再进行拓扑排序输出的是逆拓扑序列,这句话是错的,这里用代码给大家验证一下。

将队列改为栈之后要做的改变:需要新增拓扑排序的结果数组res来保存每次出栈元素。
其余过程一样。最后输出结果仍为拓扑有序序列。

下图来自《数据结构 C语言版 第2版 (严蔚敏)》
Xnip2023-12-20_23-16-59.png
Xnip2023-12-20_23-17-28.png

#include <iostream>
#include <queue>

using namespace std;

const int N = 100010;

int n, m;
int d[N];
int stk[N], top = -1;   // 用栈存储入度为0的节点
int res[N], k;

struct Node
{
    int id;
    Node *next;
    Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息

void add(int a, int b)
{
    Node *p = new Node(b);
    p->next = head[a];
    head[a] = p;
}

// 拓扑排序
bool topsort()
{
    // 将入度为0的节点入栈
    for (int i = 1; i <= n; i++)
        if (!d[i])
            stk[++top] = i;

    while (top > -1)
    {
        int t = stk[top--];
        res[k++] = t; // 将当前节点加入结果数组

        for (Node *p = head[t]; p; p = p->next)
        {
            int j = p->id;
            if (--d[j] == 0)
                stk[++top] = j; // 将入度为0的相邻节点入栈
        }
    }

    return k == n; // 如果结果数组中节点个数等于图中节点个数,说明拓扑排序成功
}

int main()
{
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        d[b]++; // 统计每个节点的入度
    }

    if (!topsort())
        puts("-1"); // 无法进行拓扑排序,图中有环
    else
    {
        for (int i = 0; i < n; i++)
            cout << res[i] << ' ';
        cout << endl;
    }

    return 0;
}

Xnip2023-12-20_23-23-22.png

3.915-23 真题中的改编 topsort 求 level(DAG的最大高度)

Xnip2023-12-20_23-11-36.png

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
int d[N]; // 入度数组,记录每个节点的入度
int q[N], hh = 0, tt = -1; // 队列,hh 表示队头,tt 表示队尾
int level[N];   // 存每个结点的level

struct Node
{
    int id;
    Node *next;
    Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息

void add(int a, int b)
{
    Node *p = new Node(b);
    p->next = head[a];
    head[a] = p;
}
bool bfs()
{
    // 将入度为 0 的节点加入队列
    for (int i = 1; i <= n; i++)
        if (!d[i])
            q[++tt] = i;

    while (hh <= tt)
    {
        int t = q[hh++];

        for (Node *p = head[t]; p; p = p->next)
        {
            int j = p->id;
            if (--d[j] == 0)
            {
                // level 为指向该结点的所有结点中最大的 level + 1
                level[j] = max(level[j], level[t] + 1);
                q[++tt] = j;
            }
        }
    }

    // 如果队列中的节点个数等于图的节点个数,说明拓扑排序成功
    return tt == n - 1;
}

int main()
{
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
        d[b]++; // 记录每个节点的入度
    }

    if (!bfs())
        puts("-1");
    else
    {
        // 输出拓扑排序的结果
        for (int i = 0; i < n; i++)
            cout << q[i] << ' ';
        cout << endl;
        // 输出level的结果
        for (int i = 1; i <= n; i++)
            cout << level[i] << ' ';
        cout << endl;
    }

    return 0;
}

4.基于深搜的topsort

4.1求逆拓扑有序序列

该代码的前提是保证图存在拓扑序列。

#include <iostream>

using namespace std;

const int N = 100010;

int n, m;
bool st[N]; // 标记每个点是否被访问过
int d[N]; // 入度数组,记录每个节点的入度

struct Node
{
    int id;
    Node *next;
    Node(int x) : id(x), next(NULL) {}
} *head[N]; // 邻接表存储图的边信息

void add(int a, int b)
{
    Node *p = new Node(b);
    p->next = head[a];
    head[a] = p;
}

void dfs(int u)
{
    st[u] = true;   // 将当前结点标记为已访问
    
    // 遍历当前结点的邻接点
    for (Node *p = head[u]; p; p = p->next)
    {
        int j = p->id;  // 邻接点编号
        if (!st[j])     // 若该点没访问过
            dfs(j);
    }
    
    cout << u << ' ';   // 退出递归时输出
    // 此时得到逆拓扑有序序列
}

int main()
{
    cin >> n >> m;
    while (m--)
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    
    dfs(1);

    return 0;
}

Xnip2023-12-20_23-38-46.png

4.2深度优先遍历判断图中是否有环的算法如下:
  1. 选择一个起始节点,将其标记为"正在访问"状态。

  2. 检查当前节点的所有相邻节点:

    • 如果相邻节点为"未访问"状态,则递归地进行深度优先搜索这个节点

      • 如果在搜索过程中发现这个节点已经被标记为"正在访问",则说明存在环路,返回true
    • 如果相邻节点状态为"已访问",忽略该节点

  3. 所有相邻节点均处理完成后,将当前节点标记为"已访问"状态,并返回上层递归

  4. 如果整个图都搜索完,没有返回true,则说明图中不存在环路,返回false。

主要思路是:

  • 使用"未访问"、"正在访问"和"已访问"三种节点状态进行标记

  • 如果搜索某个节点时发现其状态为"正在访问",则说明从此节点开始一定存在一个环路

  • 整个深度优先搜索结束没有返回true,则该图不存在环路

通过遍历每个节点及其相邻节点,并检查其访问状态就可以判断是否存在环路。时间复杂度为O(V+E),空间复杂度为O(V)。

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int n, m, q[N], top;
int st[N];  // 0表示没有遍历过,1表示在递归当中,2表示已经遍历完了

struct Node
{
    int id;
    Node* next;
    Node(int _id) : id(_id), next(NULL){}
} *head[N];

void add(int a, int b)
{
    Node* p = new Node(b);
    p->next = head[a];      // 头插法
    head[a] = p;
}

bool dfs(int u)     // 如果有环的话返回false
{
    st[u] = 1;      // 标记当前点在递归中
    
    for (Node* p = head[u]; p; p = p->next)
    {
        int j = p->id;
        if (!st[j])     // 如果没有搜过的话(0)就直接搜
        {
            if (!dfs(j)) return false;  // 如果搜的过程中出现了环,直接false
        }
        else if (st[j] == 1) return false;   // 否则如果发现这个点在递归当中,直接false
    }
    
    q[top ++] = u;
    
    st[u] = 2;      // 标记当前点已经遍历完了
    
    return true;
}

// 基于深搜的话要遍历所有点,因为这个图不一定是联通的
bool topsort()
{
    for (int i = 1; i <= n; i ++)
        if (!st[i] && !dfs(i)) // 如果没遍历的话就遍历它,如果发现有环的话直接false
            return false;
    return true;
}

int main()
{
    cin >> n >> m;
    while (m -- )
    {
        int a, b;
        cin >> a >> b;
        add(a, b);
    }
    
    if (topsort())
    {
        // 因为得到的是拓扑排序的逆序序列,所以要倒序输出
        for (int i = n - 1; i >= 0; i -- ) cout << q[i] << ' ';
        cout << endl;
    }
    else puts("-1");
    
    return 0;
}

以上内容来自【xjtu915 冲刺班】

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值