[关闭]
@sztom 2019-11-22T05:53:50.000000Z 字数 3748 阅读 104

浅析拓扑排序

图论九讲


by laihaochen

under 图论九讲

1 拓扑排序的定义

对一个有向无环图(Directed Acyclic Graph简称DAG) 进行拓扑排序,是将中所有顶点排成一个线性序列,使得图中任意一对顶点,若边,则u在线性序列中出现在之前。通常,这样的线性序列称为满足拓扑次序(Topological Order)的序列,简称拓扑序列。简单的说,由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。

其实, 就是给一个有向无环图一个遍历的顺序。

但对于任意一条边 我们还要让排在前面, 所以我们可以通过入度来计算。

2 拓扑排序解决的问题

拓扑排序解决类似于必须排在前的问题。
例如:
3必须排在5前;
4必须排在3前;
1必须排在2前;
5必须排在1前;
那么,顺序可能是4 3 5 1 2

3 拓扑排序的实现

后面拓扑要用入度,可以在建图时顺手加上。

  1. for (int i = 1; i <= m; i++) {
  2. scanf ("%d%d", &a, &b);
  3. g[a].push_back(b);
  4. rd[b]++;
  5. }
  1. 我们先把每个入度为零的点入队。

  2. 然后再取出队首, 遍历所有它能到达的边,删除从它出发的边(到达的点入度-1)。

  3. 判断被遍历到的点中有没有入度为零的点。 如果有, 就把它入队。

  4. 如果队列为空, 退出函数

代码实现:

  1. void topu () {
  2. for (int i = 1; i <= n; i++) {
  3. if (rd[i] == 0) {
  4. q.push(i);
  5. }
  6. }
  7. while (!q.empty()) {
  8. int u = q.top();
  9. s.push(u);
  10. q.pop();
  11. for (unsigned i = 0; i < g[u].size(); i++) {
  12. int v = g[u][i];
  13. rd[v]--;
  14. if (rd[v] == 0) {
  15. q.push(v);
  16. }
  17. }
  18. }
  19. }

4 拓扑排序的应用

拓扑排序一般用来给DAG一个遍历的顺序, 有了顺序, 我们就可以跑DP啦!

4.1 逃生(hdu4857)

这很明显是一道差分约束, 啊不, 拓扑的题。

但是,我们还要把编号小的放在前面。

我们可以想到用优先队列解决这个问题。

代码与拓扑的模板很相似, 代码就不放出来了。

4.2 缩点【模板】P3387

这道题看上去只要缩点后, 再跑DP, 但问题是DP的顺序是什么:

我们可以想到用拓扑给缩完点后的图一个遍历的顺序。

缩点:

  1. void Tarjan (int u) {
  2. deep++;
  3. dfn[u] = deep;
  4. low[u] = deep;
  5. s.push(u);
  6. ins[u] = 1;
  7. for (unsigned i = 0; i < g[u].size(); i++) {
  8. int v = g[u][i];
  9. if (!dfn[v]) {
  10. Tarjan (v);
  11. low[u] = min (low[u], low[v]);
  12. } else if (ins[v]) {
  13. low[u] = min (low[u], low[v]);
  14. }
  15. }
  16. if (low[u] == dfn[u]) {
  17. color[u] = u;
  18. dis[u] += val[u];//dis是记环的权值用的, val是每个点自己的权值
  19. while (s.top() != u) {
  20. color[s.top()] = u;//我们可以直接把环的编号记为缩点的编号
  21. dis[u] += val[s.top()];
  22. ins[s.top()] = 0;
  23. s.pop();
  24. }
  25. ins[u] = 0;
  26. s.pop();
  27. }
  28. }

主函数内:

  1. for (int i = 1; i <= n; i++) {
  2. if (!dfn[i]) {
  3. Tarjan(i);
  4. }
  5. }
  6. //缩点
  7. for (int i = 1; i <= n; i++) {
  8. for (unsigned j = 0; j < g[i].size(); j++) {
  9. int v = g[i][j];
  10. if (color[i] != color[v]) {
  11. g1[color[i]].push_back(color[v]);//缩完点后重新建的图
  12. rd[color[v]]++;//入度
  13. }
  14. }
  15. }

这道题的DP比较简洁, 动态转移方程: (有点类似于松弛操作),

所以我们可以直接在拓扑时跑DP, 以减少代码复杂度。

  1. vector <int> g1[N];
  2. queue<int> q;
  3. int rd[N];
  4. int ans;
  5. int f[N];//f[i] 表示终点为i的最长路
  6. void topu () {
  7. //把入度为0的入队列
  8. for(int i = 1; i <= n; i++){
  9. //判断是不是缩点 入度为0
  10. if(!rd[i] && color[i] == i){
  11. q.push(i);
  12. f[i] = dis[i];//初始化,这里的dis就是上面公式里的D
  13. }
  14. }
  15. while(!q.empty()){
  16. int u = q.front(); q.pop();
  17. for (unsigned i = 0; i < g1[u].size(); i++){
  18. int v = g1[u][i];
  19. f[v] = max (f[v], f[u] + dis[v]);
  20. rd[v]--;
  21. if (rd[v] == 0) {
  22. q.push(v);
  23. }
  24. }
  25. }
  26. for (int i = 1; i <= n; i++) {
  27. ans = max(f[i], ans);
  28. }
  29. }

4.3 Ponds(hdu5438)

这道题就难了很多, 要先处理几个子问题。

  1. 怎样把把所有能去掉的池塘都去掉?
    其实拓扑排序就可以解决这个问题, 每次把度数减一, 然后判断度数是否为零或一,
    如果是, 则入队。

  2. 怎样计算所有的连通组件(连通块)的点数?
    可以用dfs或并查集(个人感觉dfs好写)解决。
    把每个连通分量的点数记录下来,顺手记下它们的值的和。

  3. 如果只有一个点的连通组件, 怎么处理?
    依题意, 就可以不用管这个点。(加个特判)

上代码:

  1. #include <cstdio>
  2. #include <vector>
  3. #include <queue>
  4. #include <stack>
  5. #include <algorithm>
  6. #include <cstring>
  7. #include <queue>
  8. using namespace std;
  9. const int N = 10005;
  10. long long t, n, m, cnt, ans;
  11. long long val[N], color[N], col[N], du[N], sum[N];//连通组件的值的和
  12. vector <long long> g[N];
  13. queue <long long> q;
  14. bool vis[N], book[N];//判断这个点是否能用
  15. void dfs (int u) {//dfs
  16. color[u] = cnt;
  17. vis[u] = 1;
  18. for (unsigned i = 0; i < g[u].size(); i++) {
  19. int v = g[u][i];
  20. if (vis[v] || book[v]) {
  21. continue;
  22. }
  23. dfs(v);
  24. }
  25. }
  26. void topu () {//拓扑
  27. for (int i = 1; i <= n; i++) {
  28. if (du[i] == 0 || du[i] == 1) {
  29. vis[i] = 1;
  30. book[i] = 1;
  31. q.push(i);
  32. }
  33. }
  34. while (!q.empty()) {
  35. int u = q.front();
  36. q.pop();
  37. for (unsigned i = 0; i < g[u].size(); i++) {
  38. int v = g[u][i];
  39. if (vis[v]) {
  40. continue;
  41. }
  42. du[v]--;
  43. if (du[v] == 1 || du[v] == 0) {
  44. book[v] = 1;
  45. vis[v] = 1;
  46. q.push(v);
  47. }
  48. }
  49. }
  50. }
  51. void clean () {//记得初始化
  52. cnt = 0;
  53. ans = 0;
  54. memset (val, 0, sizeof(val));
  55. memset (color, 0, sizeof(color));
  56. memset (col, 0, sizeof(col));
  57. memset (sum, 0, sizeof(sum));
  58. memset (du, 0, sizeof(du));
  59. memset (vis, 0, sizeof(vis));
  60. memset (book, 0, sizeof(book));
  61. for (int i = 0; i <= n + 5; i++) {
  62. g[i].clear();
  63. }
  64. while (!q.empty()) {
  65. q.pop();
  66. }
  67. }
  68. int main() {
  69. scanf ("%d", &t);
  70. while (t--) {//毒瘤的多组数据(#→⌒→)
  71. scanf ("%d%d", &n, &m);
  72. for (int i = 1; i <= n; i++) {
  73. scanf ("%d", &val[i]);
  74. }
  75. int a, b;
  76. for (int i = 1; i <= m; i++) {
  77. scanf ("%d%d", &a, &b);
  78. g[a].push_back(b);
  79. g[b].push_back(a);
  80. du[a]++;
  81. du[b]++;
  82. }
  83. topu();
  84. memset (vis, 0, sizeof(vis));
  85. for (int i = 1; i <= n; i++) {
  86. if (!color[i] && !book[i]) {//神似Tarjan
  87. cnt++;
  88. dfs(i);
  89. }
  90. }
  91. for (int i = 1; i <= n; i++) {
  92. col[color[i]]++;
  93. sum[color[i]] += val[i];
  94. }
  95. for (int i = 1; i <= cnt; i++) {
  96. if (col[i] % 2 == 1) {//判断是否是由奇数个点组成的
  97. ans += sum[i];
  98. }
  99. }
  100. printf ("%lld\n", ans);
  101. clean();
  102. }
  103. return 0;
  104. }

5 总结

拓扑只能处理DAG, 把图变成一个线性序列, 利用这一点, 就能解决大多数的拓扑的题了。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注