图论------拓扑排序和图上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 网经过拓扑排序得到的事件序列称为拓扑序列。在课程问题中,相当于就是找到一个排课的合法顺序。具体流程可借助队列进行:
- 将图中所有入度为 0 的点,加入到队列中;
- 取出队头元素,删除与该点相连的边。如果删除之后的后续结点的入度变为 0 ,加入到队列中;
- 重复 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 【模板】拓扑排序 - 洛谷
测试样例画成 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 摄像头 - 洛谷
摄像头能监视到的位置看成结点,这些位置可能有摄像头,可能没有。破坏摄像头的顺序可组成一个 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 最大食物链计数 - 洛谷
基于 DAG 上的动态规划。
-
状态定义
对每个动植物进行描述,每个动植物都可看成 DAG 上的结点,有它是谁的食谱 可用数组表示,有谁被它吃即结点的入度 ,有从生产者到自身产生的食物链条数
dp。这个食物网可能有多个子图,所以答案是所有子图的食物链顶端的
dp之和。 -
转移方程
每个动物的
dp等于它吃的动物的dp叠加。 -
初始化 & 填表
所有生产者即入度为 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
同样是 DAG 的 dp。
-
状态定义
设
dp[i]表示完成i任务的最短时间。 -
转移方程。
dp[i]=max(dp[i],dp[lt]+t[i]),lt是 AOV 网中i的前驱,t[i]是完成i任务所需的时间。因题目描述了同一时间可以同时进行多个任务(即并行),所以dp[i]是所有前置任务的最大值加上自身耗时。答案就是出度为 0 的任务中耗时最大的那个任务的耗时。 -
初始化 & 填表
当任务
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;
}