DAG上的动态规划是学习动态规划的基础。不少问题均可以转化为DAG上的最长路、最短路或路径计数问题。算法
也就是一般作题时说的“拓扑排序 + DP”的作法数组
由于最近作的模拟题中常常会出现这样的题学习
因此我决定学一下并写一篇博客this
其实我原来学过只是没学会spa
咱们先从最基础的讲起.net
首先,图(Graph)描述的是一些个体之间的关系。设计
图与线性表和二叉树不一样的是:这些个体之间既不是前驱后继的顺序关系,也不是祖前后代的层次关系,而是错综复杂的网状关系指针
而DAG就是图的一种code
DAG(Directed Acyclic Graph)的意思是有向无环图blog
所谓有向无环图是指任意一条边有方向,且不存在环路的图
一般,咱们把顶点表示活动、边表示活动间前后关系的有向图称作顶点活动网(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 }
原题中还有一个要求,就是要求字典序最小,咱们只须要