DAG上的动态规划是学习动态规划的基础。很多问题都可以转化为DAG上的最长路、最短路或路径计数问题。
也就是通常做题时说的“拓扑排序 + DP”的做法
因为最近做的模拟题中经常会出现这样的题
所以我决定学一下并写一篇博客
其实我原来学过只是没学会
我们先从最基础的讲起
首先,图(Graph)描述的是一些个体之间的关系。
图与线性表和二叉树不同的是:这些个体之间既不是前驱后继的顺序关系,也不是祖先后代的层次关系,而是错综复杂的网状关系
而DAG就是图的一种
DAG(Directed Acyclic Graph)的意思是有向无环图
所谓有向无环图是指任意一条边有方向,且不存在环路的图
通常,我们把顶点表示活动、边表示活动间先后关系的有向图称做顶点活动网(Activity On Vertex network),简称AOV网。
一个AOV网应该是一个DAG,即不应该带有回路,因为若带有回路,则回路上的所有活动都无法进行(对于数据流来说就是死循环)。在AOV网中,若不存在回路,则所有活动可排列成一个线性序列,使得每个活动的所有前驱活动都排在该活动的前面,我们把此序列叫做拓扑序列(Topological order),由AOV网构造拓扑序列的过程叫做拓扑排序(Topological sort)。AOV网的拓扑序列不是唯一的,满足上述定义的任一线性序列都称作它的拓扑序列。
以上就是一些最基础的定义
拓扑排序的实现步骤是
1.先把入度为零的点输出并在图中删除(因为在刚开始的一个DAG中入度为零的点可能不止一个,所以拓扑序列并不一定是唯一的)
2.删除与刚刚输出的点有关的边(即使那个点的出度为零)
3.重复上述两步,直至所有点都被输出,或者不再有入度为零的点,后者说明这个图是有环的,所以也可以通过拓扑排序来判断这个图是不是有环图
拓扑排序的代码实现
有两种方法来实现拓扑排序
1.Kahn算法
2.基于DFS实现
——Kahn算法——
Kahn算法的思路就是先使用一个栈用来保存入度为零的点,然后输出栈顶元素并将与栈顶元素有关的边删除,减少与栈顶元素有关的顶点的入度数量并且将入度减少到零的顶点也入栈。
具体的代码实现如下(指针版):
1 bool Graph_DG::topological_sort(){ 2 cout << "图的拓扑序列为:" << endl; 3 //栈s用于保存栈为空的顶点下标 4 stack<int> s; 5 int i; 6 ArcNode *temp; 7 //计算每个顶点的入度,保存在indgree数组中 8 for (int i = 0; i != this -> vexnum; i++){ 9 temp = this -> arc[i].firstarc; 10 while (temp){ 11 ++this -> indegree[temp -> adjvex]; 12 temp = temp -> next; 13 } 14 } 15 //把入度为0的顶点入栈 16 for (int i = 0; i != this -> vexnum; i++){ 17 if (!indegree[i]){ 18 s.push(i); 19 } 20 } 21 //count用于计算输出的顶点个数 22 int count = 0; 23 while (!s.empty()){ 24 //如果栈为空,则结束循环 25 i = s.top(); 26 s.pop(); 27 //保存栈顶元素,并且栈顶元素出栈 28 cout << this -> arc[i].data << " "; 29 temp = this -> arc[i].firstarc; 30 while (temp){ 31 if (!(--this -> indegree[temp -> adjvex])){ 32 //如果入度减少到为0,则入栈 33 s.push(temp -> adjvex); 34 } 35 temp = temp -> next; 36 } 37 ++count; 38 } 39 if (count == this -> vexnum){ 40 couot << endl; 41 return true; 42 } 43 cout << "此图有环,无拓扑序列" << endl; 44 return false;//说明这个图有环 45 }
复杂度为O(V + E)
——基于DFS实现——
推荐学习这个实现方式
因为这个比较好写比较常用
同样地,这个的复杂度也为O(V + E)
借助DFS来完成拓扑排序:
在访问完一个结点之后把它加到当前拓扑序的首部
之所以不是尾部
是因为每次访问完之后,当前的点都是最后的结点
比如1 -> 2, 2 -> 3
因为我们在访问的时候都是从前往后访问的
所以最后访问的数就是最后的数
具体的代码实现如下(数组版):
1 int c[maxn]; 2 int topo[maxn], t; 3 bool dfs(int u){ 4 c[u] = -1;//表示正在访问 5 for (int v = 0; v < n; v++) 6 if (G[u][v]){ 7 if (c[v] < 0) return false;//存在有向环,失败退出 8 else if (!c[v] && !dfs(v)) return false; 9 } 10 c[u] = 1; 11 topo[--t] = u; 12 return true; 13 } 14 bool toposort(){ 15 t = n; 16 memset(c, 0, sizeof(c)); 17 for (int u = 0; u < n; u++) 18 if (!c[u]) 19 if (!dis(u)) return false; 20 return true; 21 }
这里用到了一个c数组,c[u] = 0表示从来没有访问过(从来没有调用过dfs(u)),c[u] = 1表示已经访问过,并且还递归访问过它的所有子孙(即dfs(u)曾被调用过,并已返回),c[u] = -1表示正在访问(即递归调用dfs(u)正在栈帧中,尚未返回
Tip:可以用DFS求出有向无环图(DAG)的拓扑排序。如果排序失败,说明该有向图存在有向环,不是DAG
有一道拓扑排序的模板题,超级裸
关于DP,因为这篇博客属于DP专题中的一个小专题,所以在这里不详细讲述DP了,只给出一些基础的定义
DP(Dynamic Programming),中文名称叫做动态规划
动态规划是一种用途很广的问题求解方法,它本身并不是一个特定的算法,而是一种思想,一种手段
动态规划的理论性和实践性都比较强,一方面需要理解“状态”、“状态转移”、“最优子结构”、“重叠子问题”等概念
另一方面又需要根据题目的条件灵活设计算法
所以说DP非常难玄啊
拓扑排序、DP和DAG上的动态规划(拓扑排序 + DP)的关系
就类似于1 + 1 = 2的关系
所以在做完了准备工作后
我怎么感觉这是拓扑排序专题
开始进入正题
DAG上的动态规划分为两个小题型
1.最长路及其字典序
2.固定终点的最长路和最短路
——最长路及其字典序——
模型:嵌套矩阵问题
有n个矩形,每个矩形可以用两个整数a、b描述,表示它的长和宽。矩形X(a,b)可以嵌套在矩形Y(c,d)中,当且仅当a < c,b < d,或者b < c,,a < d(相当于把矩形X旋转90°)。例如,(1,5)可以嵌套在(6,2)内,但不能嵌套在(3,4)内。你的任务是选出尽量多的矩形排成一行,使得除了最后一个之外,每一个矩形都可以嵌套在下一个矩形内。如果有多解,矩形编号的字典序应该尽量小。
分析:
此题是一个很显然的DAG上的动态规划
矩阵之间的“可嵌套”是显然的二元关系,二元关系可以用图来建模。如果矩阵X可嵌套在矩阵Y里面,那么就从X向Y连一条有向边。这个有向图是无环的,因为一个矩形无法直接或者间接的嵌套在自己内部。
所以,这是一个DAG。这样,我们所求的就是DAG上的最长路径
但我们如何求DAG上不固定起点的最长路径呢
仿照数字三角形的做法,我们设d[i]为从i点出发的最长路径,因为第一步只能走到它的相邻点,所以转移方程很显然的就能写出
d(i) = max{d(j) + 1 | (i, j) ∈ E}
其中,E为边集,最终答案是所有d(i)中的最大值。
代码如下:
1 int dp(int i){ 2 int &ans = d[i]; 3 if (ans > 0) return ans; 4 ans = 1; 5 for (int j = 1; j <= n; j++) 6 if (G[i][j]) ans = max(ans, dp(j) + 1); 7 return ans; 8 }
原题中还有一个要求,就是要求字典序最小,我们只需要