图论——拓扑排序和图上DP

图论------拓扑排序和图上DP

  • 拓扑排序
    • [B3644 【模板】拓扑排序 - 洛谷](#B3644 【模板】拓扑排序 - 洛谷)
    • [P2712 摄像头 - 洛谷](#P2712 摄像头 - 洛谷)
  • DAG上的dp
    • [P4017 最大食物链计数 - 洛谷](#P4017 最大食物链计数 - 洛谷)
    • [P1113 杂务 - 洛谷 DAG的dp](#P1113 杂务 - 洛谷 DAG的dp)
  • OJ参考

拓扑排序

DAG图:若一个有向图中不存在回路 (环路) ,则称为有向无环图 (directed acyclic graph) ,简称 DAG 图。在图中从一个点出发,有办法回到这个点,则说明这个图中存在回路。

AOV网:举一个现实中的例子:课程的学习是有优先次序的,如果规定某些课程必须在其他课程之前学习,那么课程间的先后次序可以用有向图表示:

在这种有向图中,用顶点表示活动 (事件) ,用有向边 < V i , V j > <V_i, V_j> <Vi,Vj> 表示活动 V i V_i Vi 必须先于活动 V j V_j Vj 进行,这种有向图叫做顶点表示活动的网络 (Activity On Vertex Network) ,简称 AOV 网。

AOV 网中不能有回路,否则就不能确定回路中的活动究竟哪个先实施。AOV 网必须是有向无环图。

拓扑排序 的目标是将有向无环图中的所有结点排序 ,使得排在前面的结点不能依赖于排在后面的结点 。AOV 网经过拓扑排序得到的事件序列称为拓扑序列。在课程问题中,相当于就是找到一个排课的合法顺序。具体流程可借助队列进行:

  1. 将图中所有入度为 0 的点,加入到队列中;
  2. 取出队头元素,删除与该点相连的边。如果删除之后的后续结点的入度变为 0 ,加入到队列中;
  3. 重复 2 操作,直到图中没有点或者没有入度为0的点为止。

例如上图,按照拓扑排序可得到一种拓扑序列 { 1 , 2 , 7 , 8 , 3 , 4 , 5 , 6 } \{1,2,7,8,3,4,5,6\} {1,2,7,8,3,4,5,6} 。

拓扑排序判断是否有环:跑一遍拓扑排序算法,如果有结点没有进队,那么就表明有环。

这里的拓扑排序可通过 bfs 的思路改造而来。

B3644 【模板】拓扑排序 - 洛谷

B3644 【模板】拓扑排序 / 家谱树 - 洛谷

测试样例画成 DAG 以及求拓扑排序序列:

拓扑排序可使用 bfs 实现,但在 DAG 的存储上需要对每个结点新增一个入度数信息。这里将邻接表和入度信息进行了封装。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct Node {
    vector<int> edge;       // 邻接表
    int indeg;              // 自身入度信息
    void push_back(int x) { // 邻接表插入结点
        edge.push_back(x);
    }
    // 为使用范围for增加的迭代器
    vi::iterator begin() {
        return edge.begin();
    }
    vi::iterator end() {
        return edge.end();
    }
};
using vN = vector<Node>;

void bfs(vN &vn) {
    queue<int> q;
    for (int i = 1; i < vn.size(); i++)
        if (!vn[i].indeg)
            q.push(i);
    while (!q.empty()) {
        int sp = q.front(); // start point
        q.pop();
        cout << sp << ' ';
        for (auto &x : vn[sp]) {
            vn[x].indeg--;
            if (!vn[x].indeg) // 入度为0则入队
                q.push(x);
        }
    }
}

int main() {
    int n;
    cin >> n;
    vN vn(n + 1);
    for (int i = 1, x; i <= n; i++)
        while (cin >> x, x) {
            vn[i].push_back(x);
            vn[x].indeg++;
        }
    bfs(vn);
    return 0;
}

P2712 摄像头 - 洛谷

P2712 摄像头 - 洛谷

摄像头能监视到的位置看成结点,这些位置可能有摄像头,可能没有。破坏摄像头的顺序可组成一个 AOV 网,所以破坏摄像头的顺序和数量可用拓扑排序求解。

但需要注意的是,不是所有摄像头拍摄到的位置都有别的摄像头,这种位置在进行拓扑排序时不能入队。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using vi = vector<int>;
struct Node {
    vi edge;
    int indeg;
    void push_back(int x) {
        edge.push_back(x);
    }
    vi::iterator begin() {
        return edge.begin();
    }
    vi::iterator end() {
        return edge.end();
    }
};
using vN = vector<Node>;
using vb = vector<bool>;

int bfs(vN &pct, vb &vis) {
    queue<int> q;
    int cnt = 0;
    for (int i = 1; i < pct.size(); i++)
        if (vis[i] && !pct[i].indeg) // 只关心有摄像头的位置
            q.push(i), ++cnt;
    while (!q.empty()) {
        int sp = q.front();
        q.pop();
        for (auto &x : pct[sp]) {
            pct[x].indeg--;
            if (vis[x] && !pct[x].indeg) // 有摄像头且没有监视
                q.push(x), ++cnt;
        }
    }
    return cnt;
}

int main() {
    int n, cnt = 0;
    vN pct;
    vb vis;
    cin >> n;
    pct.resize(501);
    vis.resize(501, 0); // 标记装摄像头的位置
    for (int i = 1; i <= n; i++) {
        int x, m;
        cin >> x >> m;
        vis[x] = 1; // 标记有摄像头的位置
        for (int j = 1; j <= m; j++) {
            int y;
            cin >> y;
            pct[x].push_back(y);
            pct[y].indeg++;
        }
    }
    cnt = bfs(pct, vis);
    if (n == cnt)
        cout << "YES";
    else
        cout << n - cnt;
    return 0;
}

DAG上的dp

与线性 dp 不同,DAG 上的 dp 是在有向无环图的基础上进行状态转移,且填表方式多为拓扑排序。

P4017 最大食物链计数 - 洛谷

P4017 最大食物链计数 - 洛谷

基于 DAG 上的动态规划。

  1. 状态定义

    对每个动植物进行描述,每个动植物都可看成 DAG 上的结点,有它是谁的食谱 可用数组表示,有谁被它吃即结点的入度 ,有从生产者到自身产生的食物链条数 dp

    这个食物网可能有多个子图,所以答案是所有子图的食物链顶端的 dp 之和。

  2. 转移方程

    每个动物的 dp 等于它吃的动物的 dp 叠加。

  3. 初始化 & 填表

    所有生产者即入度为 0 的结点,它的 dp 初始化为 1 。填表时按拓扑排序分离出每层消费者,根据转移方程叠加食物链条数即可。

若严格划分的话,这个题可看成路径 dp 。且只有 DAG 才能做动态规划,否则一旦出现环的话,动态规划就会出错。

这里通过继承的方式将入度信息和食物链条数加入 vector 数组,表示一个结点的特征。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
using vll = vector<LL>;
struct Node : public vll { // 对结点的描述,继承vector
    LL indeg = 0;          // 入度信息
    LL dp = 0;             // 到自身时的食物链条数
};
using vN = vector<Node>;
using vb = vector<bool>;

const LL MOD = 80112002;
vN pct;
LL n;

LL bfs() {
    queue<LL> q;
    LL ans = 0;
    for (int i = 1; i < pct.size(); i++) {
        if (!pct[i].indeg) {
            q.push(i);
            pct[i].dp = 1; // 初始化
        }
    }
    while (!q.empty()) {
        LL np = q.front();
        q.pop();
        for (LL i = 0; i < pct[np].size(); i++) {
            pct[pct[np][i]].indeg--;
            pct[pct[np][i]].dp = (pct[pct[np][i]].dp + pct[np].dp) % MOD;
            if (!pct[pct[np][i]].indeg) {
                q.push(pct[np][i]);
            }
        }
        if (pct[np].empty())
            ans = (ans + pct[np].dp) % MOD;
    }
    return ans;
}

int main() {
    int times;
    cin >> n >> times;
    pct.resize(n + 1);
    for (int i = 1; i <= times; i++) {
        int x, y;
        cin >> x >> y;
        pct[x].push_back(y); // 邻接表
        pct[y].indeg++;      // 入度信息
    }
    cout << bfs() << endl;
    return 0;
}

P1113 杂务 - 洛谷 DAG的dp

P1113 杂务 - 洛谷

同样是 DAG 的 dp。

  1. 状态定义

    dp[i] 表示完成 i 任务的最短时间。

  2. 转移方程。

    dp[i]=max(dp[i],dp[lt]+t[i])lt 是 AOV 网中 i 的前驱, t[i] 是完成 i 任务所需的时间。因题目描述了同一时间可以同时进行多个任务(即并行),所以 dp[i] 是所有前置任务的最大值加上自身耗时。答案就是出度为 0 的任务中耗时最大的那个任务的耗时。

  3. 初始化 & 填表

    当任务 i 的前置工作即入度为 0 时,dp[i]=t[i] 。因为没有负数的时间,所以取余 dp[j] 初始化为 0 即可。

    填表还是使用拓扑排序辅助。每遍历一个结点,就进行状态转移。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

using LL = long long;
using vvll = vector<vector<LL>>;
using vll = vector<LL>;

LL bfs(vvll &pct, vll &indeg, vll &dp, vll &t) {
    queue<LL> q;
    LL ans = 0;
    for (int i = 1; i < pct.size(); i++)
        if (!indeg[i]) {
            q.push(i);
            dp[i] = t[i];
        }
    while (!q.empty()) {
        LL np = q.front();
        q.pop();
        for (auto &x : pct[np]) {
            indeg[x]--;
            dp[x] = max(dp[x], dp[np] + t[x]);
            if (!indeg[x]) 
                q.push(x);
        }
        if (pct[np].empty())
            ans = max(ans, dp[np]);
    }
    return ans;
}

int main() {
    vvll pct;
    vll indeg, dp, t;
    LL n;
    cin >> n;
    pct.resize(n + 1);
    indeg.resize(n + 1, 0);
    dp = t = indeg;
    for (LL i = 1; i <= n; i++) {
        LL id, lt;
        cin >> id >> t[i];
        while (cin >> lt, lt) {
            pct[lt].push_back(id);
            indeg[id]++;
        }
    }
    cout << bfs(pct, indeg, dp, t);
    return 0;
}

OJ参考

B3644 【模板】拓扑排序 / 家谱树 - 洛谷

P2712 摄像头 - 洛谷

P4017 最大食物链计数 - 洛谷

P1113 杂务 - 洛谷

相关推荐
满分观察网友z1 小时前
刷 LeetCode 看不懂题解?我做了一个能"播放"算法的开源可视化平台
前端·算法·leetcode
liu****1 小时前
4.哈希扩展
c++·算法·哈希算法·位图·bitset
70asunflower1 小时前
CUDA基础知识巩固检验练习题【附有参考答案】(6)
c++·人工智能·cuda
Σίσυφος19001 小时前
PCL聚类 之K-Means
算法·kmeans·聚类
Flying pigs~~1 小时前
机器学习之数据挖掘时间序列预测
人工智能·算法·机器学习·数据挖掘·线性回归
仰泳的熊猫1 小时前
题目1882:蓝桥杯2017年第八届真题-k倍区间
数据结构·c++·算法·蓝桥杯
Mikowoo0071 小时前
Visual Studio 2022 下CUDA程序开发
c++·visual studio
有时间要学习1 小时前
面试150——第六周
算法·面试·深度优先
请叫我大虾1 小时前
数据结构与算法-分裂问题,将数字分成0或1,求l到r之间有多少个1.
java·算法·r语言