【板子】拓扑排序

1. 术语

a. 有向无环图
如果一个有向图的任一个点都无法通过一些有向边回到自身,则称这个图为 有向无环图 D A G DAG DAG

b. 拓扑排序
如果存在边 (u,v),则拓扑排序中 u 一定在 v 的前面。


2. 拓扑排序判断 D A G DAG DAG

对于一张有向图 ( G , E ) (G, E) (G,E),如果可以将所有的顶点组成一个线性排序,满足 u u u v v v 的前面,其中, ( u , v ) ∈ E (u, v) \in E (u,v)E,即,可以将所有顶点组成一个 拓扑排序,那么图 ( G , E ) (G, E) (G,E)有向无环图 D A G DAG DAG

具体实现:

  • 定义一个队列 q q q,把所有入度为 0 0 0 的结点加入到 q q q
  • 取队首结点,输出。删去所有从他出发的边,令这些边的另一端的结点 入度 − 1 -1 1。如果某个顶点的入度减为 0 0 0,则将其入队 q q q
  • 反复执行,直到 q q q 为空。
  • 如果输出的结点数恰好为 n n n,则拓扑排序成功,图 ( G , E ) (G, E) (G,E) 为有向无环图 D A G DAG DAG,反之,图 ( G , E ) (G, E) (G,E) 中有环。

【注】邻接点 可以用邻接矩阵(耗空间,适用于点数很小的情况) / 邻接表(推荐) 存储。

// 邻接表实现
vector<int> g[maxn];
int n,m,inD[maxn];

bool topologicalSort()
{
	int num=0;
    queue<int> q;
    vector<int> inD(n, 0); 	// 存储每个点的入度
    vector<vector<int> > g(n + 1);

    for(auto x : edges) 	// 邻接表存储边
    {
        g[x[1]].emplace_back(x[0]);
        inD[x[0]]++;
    }
    
    for(int i = 0; i < n; i++)
        if(inD[i] == 0) 	// 入度为 0 的顶点入队列
            q.push(i);
            
    while(!q.empty())
    {
        int u = q.front();
        q.pop();
        num++; 	// 每取出一个入度为 0 的点,num++
        for(int i = 0; i < g[u].size(); i++) 	// 遍历邻接点,修改邻接点的入度
        {
            int v = g[u][i];
            inD[v]--; 	//入度--
            if(inD[v] == 0) 	// 出现了新的入度为 0 的顶点,入队
                q.push(v); 
        }
        // g[u].clear(); 	//清空顶点u出发的所有边,可忽略
    } 
    if(num == n) return true;
    return false;
}


// 邻接矩阵实现
bool topologicalSort(){
    vector<vector<int> > g(n + 1, vector<int>(n + 1, 0));
    vector<int> in(n + 1, 0);
    vector<int> vis(n + 1, 0); 	// 额外建立一个数组记录顶点的访问情况

    for(auto x : edges) 	// 邻接矩阵存储边
    {
        g[x[1]][x[0]] = 1;
        in[x[0]]++;
    }
    
    int cnt = 0;

    while(cnt < n) 	// 当获取 n 个入度为 0 的顶点时,结束循环
    {
        int u = -1;
        for(int i = 0; i < n; i++)
        {
            if(in[i] == 0 && vis[i] == 0) 	// 入度为 0 且未被访问过
            {
                u = i;
                vis[u] = 1;
                break;
            }
        }
        if(u == -1) return false; 	// 没有入度为 0 的节点了

        for(int i = 0; i < numCourses; i++)
        {
            if(g[u][i] == 1) 	//存在边,入度--
                in[i]--;
        }
        cnt++;
    }
    return true;
}

【注】 如果要求有多个入度为 0 0 0 的顶点时选择序号最小的,把queue改成priority_queue,或者set也可以。

【注】 邻接表 + 队列 实现时,出现的入度为 0 0 0 的顶点一定是之前未被访问过的因为被遍历到的邻接点 v v v 至少存在边 ( u , v ) (u, v) (u,v) ,即,入度至少为 1 1 1),所以不需要 vis 数组;而邻接矩阵实现时,每一次都会遍历所有顶点,所以需要记录那些入度为 0 0 0 的顶点已经被计算过了,所以需要 vis 数组,进而防止重复计算。


3. 拓扑排序其他例题

2127. 参加会议的最多员工数

题意

  • 每个员工 有且只有一个 喜欢的员工,且一定不是自己;
  • 员工必须和喜欢的员工坐在一起;
  • 求可参加会议的最多员工数。

解法 拓扑排序 + 动态规划

比较容易想到这是一个图的问题。

需要注意的第一个点是:

  • 一共 n n n 个节点, n n n 条边,并且每个节点的出度为 1 1 1,所以 图里一定有环,并且,会构成一种特殊的结构——基环内向树 / 基环内向森林(环外的点都直接或间接指向环)。
    【注】可以有多个环,但是环之间不可能相连。
    【注】也就是说,这个图应当是由类似下图的结构的若干个(一个或多个)的组合。
    取自 LeetCode 官方题解

需要注意的第二个点是:

  • 大小为 2 2 2 的环是特殊的。

≥ 3 \geq 3 3 的环:

  • 等价于寻找环所在的最长路径,如,a->b->c->环,即 连接到环上的最长子路径的长度 + 环的长度
  • 注意到,指向环的子路径,如 a->b->c,是一条拓扑排序,那么,就可以使用拓扑排序来计算连接到环上的最长子路径的长度
  • 同时应当注意到,在寻找完拓扑排序后,环上的点是一定不会进入队列的,因为环上的点入度不可能为 0 0 0
  • 因此,拓扑排序的最大值 + 环的长度,就是这样一棵基环内向数可以参加会议的最大人数。
  • 由于数据范围很大,所以使用 动态规划 来计算拓扑排序的最大长度,更具体地说,是计算环上节点所在的拓扑排序的最大长度。
    • f [ i ] f[i] f[i] 表示到点 i i i 的拓扑排序的最大长度
    • 初始化 f [ i ] = 1 f[i] = 1 f[i]=1。因为自身节点也算长度。
    • 更新 f [ i ] = m a x ( f [ i ] , f [ j ] + 1 ) f[i] = max(f[i], f[j] + 1) f[i]=max(f[i],f[j]+1),其中 ( j , i ) ∈ E (j, i) \in E (j,i)E
  • 【注】拓扑排序的环上的连接点不要重复算。

= 2 = 2 =2 的环:

  • 由于大小为 2 2 2 的环,员工相互喜欢,所以此时的答案应当是 所有 连接到环上的最长子路径的长度(不包括环上的点) + 环的长度 + 连接到环上的第二场长子路径的长度 之和
  • 所以寻找所有 = 2 = 2 =2 的环,相加。
  • 【注】拓扑排序的环上的连接点不要重复算。
  • 【注】区别在于, ≥ 3 \geq 3 3 的环是出不了圈的,因为喜欢的员工一定在圈内,而 = 2 = 2 =2 的环是可以出圈的
class Solution {
public:
    int maximumInvitations(vector<int>& favorite) {
        int n = favorite.size();

        vector<int> indeq(n, 0), f(n, 1);
        vector<int> vis(n, 0);

        for(int i = 0; i < n; i++)
        {
            indeq[favorite[i]]++;
        }

        queue<int> q;
        for(int i = 0; i < n; i++)
        {
            if(!indeq[i]) q.push(i);
        }

        // 拓扑排序
        while(!q.empty())
        {
            int u = q.front();
            q.pop();
            vis[u] = 1;     // 标记环外的点

            int v = favorite[u];
            f[v] = max(f[v], f[u] + 1);     // 更新最长的拓扑排序
            indeq[v]--;
            if(!indeq[v])   q.push(v);
        }

        // ring: 环大于2的最长游走路径
        // total: 环为2的游走路径之和
        int ring = 0, total = 0;
        for(int i = 0; i < n; i++)
        {
            // 环上的点
            if(!vis[i])
            {
                int j = favorite[i];
                if(favorite[j] == i)    // 相互喜欢
                {
                    total += f[i] + f[j];
                    vis[i] = vis[j] = 1;    // 判断过的点必须标记,否则会重复加
                }
                else
                {
                    int circle_len = 1;
                    int u = i;
                    while(favorite[u] != i)
                    {
                        circle_len++;
                        vis[u] = 1;     // 这个环上的点都被遍历过了
                        u = favorite[u];
                    }
                    ring = max(ring, circle_len);
                }
            }
        }
        return max(total, ring);
    }
};
复杂度

时间复杂度: O ( n ) O(n) O(n),每个点最多遍历一遍。
空间复杂度: O ( n ) O(n) O(n),数组。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值