DP经典模型:有向无环图(DAG)上的动态规划
有向无环图(DAG)模型是动态规划中最重要的模型之一。在众多千奇百怪的动态规划问题背后,其本质都是DAG模型,这些问题就可以转化为DAG上的经典问题求解。因此掌握好有向无环图经典问题的动态规划思路,是深入学习动态规划必不可少的一步。
一、有向无环图的经典问题求解
如下图是一张有向无环图,每条边上的边权值表示两个节点之前的距离,现在要求从节点A到节点F的最短距离:

从节点A出发,到达节点F共有以下路径:
A > B > D > F , 距离为1+5+4=10
A > B > E > F , 距离为1+8+2=11
A > C > E > F , 距离为4+3+2=9
因此,最短距离为9
我们可以用动态规划思想解决此问题,定义状态dp[i]表示从起点出发以节点 i 结尾的最短距离,对于状态dp[i],其上一阶段可能位于它的任意前驱 j(即所有能够到达i的节点)上,那么对于节点i来说,其能做出的决策只有:从节点 j 到达节点 i,d[j]则表示从起点到达节点j的最短距离,dis[j][i]表示从节点 j 到达节点 i 的距离,那么从起点经过节点 j 到达节点 i 的最短距离就等于d[j]+dis[j][i],由于我们要求从起点到达节点 i 的最短距离,其可能经过所有的节点 j,所以最短路径就是经过这 j 个节点的路径中最短的那条,状态转移方程如下:
dp[i] = min{dp[j] + dis[j][i] | j是i的前驱节点}
当然,我们也可以换一种状态的表示方法,我们定义dp[i]表示从节点i出发到达终点的最短距离,此时状态转移方程就变为:
dp[i] = min {dp[j] + dis[i][j] | j是i的后继节点}
最终,dp[F]就是我们要求得的从节点A到节点F的最小距离。
如果问题要求A到F的最大距离,则只要将状态及状态转移方程中的最小值换成取最大值。
另外地,我们知道动态规划问题除了最优化问题外还有一种方案数问题,即问题变为求A到F的所有路径有多少?此时也只需要将状态改为方案数,将状态转移方程改为累加求和即可。
例题1:城市交通路网
本题我们采用dp[i]表示从节点i出发的最短距离的状态来编写代码,编码思路有很多,比如采用传统的递推形式,在该状态表示下,我们需要逆序递推,才能保证i的后继是已经被计算过的。本文给出一种递归+记忆化搜索的编码形式:
#include <iostream>
#include <cstring>
using namespace std;
const int MAX_N = 100;
int n;
int dis[MAX_N + 1][MAX_N + 1];
int dp[MAX_N + 1];
int next_node[MAX_N + 1];
int solve(int i) {
// 记忆化搜索
if (dp[i] != 0x3f3f3f3f) {
return dp[i];
}
for (int j = 1; j <= n; j++) {
if (dis[i][j] > 0) {
int candidate = solve(j) + dis[i][j];
if (dp[i] > candidate) {
dp[i] = candidate;
next_node[i] = j;
}
}
}
return dp[i];
}
int main() {
cin >> n;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
cin >> dis[i][j];
}
}
memset(dp, 0x3f, sizeof(dp));
dp[n] = 0;
memset(next_node, 0, sizeof(next_node));
cout << "minlong=" << solve(1) << endl;
cout << 1;
int current = next_node[1];
while (current != 0) {
cout << " " << current;
current = next_node[current];
}
cout << endl;
return 0;
}
例题2:挖地雷
本题样例输入可以描述为如下的有向无环图:

可以看到该问题与我们上述举例的有向无环图问题存在一定差异:
1. 我们举例的DAG是边带权,而该图是节点带权;
2. 我们举例的DAG问题是确定了起点或者终点的,而本题并不确定!
怎么办呢,我们可以对该图进行一点小小的变形,添加一个虚拟节点0,其能够连接所有实际的节点,然后将节点的权值转移到边上,就可以得到下面的DAG:

此时,我们再定义状态dp[i]表示,从起点0出发,到达节点i的最长距离,是不是就跟举例的DAG问题一样了?!当然,由于终点仍然没有固定,所以最终的答案要在所有的dp[i]中取最大值。
二、有向无环图模型的拓展
例题3:最长上升子序列
大意:给定一个整数序列array,找到它的所有严格递增子序列中最长的序列,输出其长度。
例:6,3,1,5,2,3,7
它的最长上升子序列有:1, 2, 3, 7,长度为4,因此结果为4。
下面我们思考如何将其与DAG模型联系起来:
我们将每个数字都视为一个节点,对于第 i 个数字,根据题目要求,如果它后面的数字 j 满足 j>i,就满足上升子序列,那么数字 j 节点就可以作为数字 i 节点的后继,按此规则找出每个数字的后继节点,然后我们就可以据此画出如下的DAG:

题目要求最长的上升子序列,只要将该图中的边权全部视为1,是不是就变成了在该DAG中找到一条最长的路径?而这与例题2是完全相同的。于是一道全新的题型就这样被我们转化为了DAG的经典问题求解。
有了例题三的经验,相信读者对于该问题应该已经能很轻松的将其转化为DAG模型了吧。
将每个矩形都看作一个节点,对于矩形 i,如果有另外一个矩形 j 满足,i的宽小于 j 的宽且 i 的长小于 j 的长或者 i 的长小于 j 的宽且 i 的宽小于 j 的长,就满足嵌套的定义,那么就可以将矩形 j 作为矩形 i 的后继节点,据此也能画出一张DAG,然后将边权视为1,问题就等价于在该DAG中找出一条最长路径。轻松搞定!
例题5:跳台阶_牛客网
楼梯共有n(100 > n > 0)阶台阶,上楼时可以一步上1阶,也可以一步上2阶,试求出走到楼梯顶一共有多少种走法?
我们在动态规划入门(一):从爬楼梯开始中对爬楼梯问题做过讲解,现在我们以一种新的视角来重新理解一下爬楼梯问题:
将每个台阶看作一个节点,在第i级台阶,可以走到第 i+1 级台阶或第 i+2 级台阶,也就是第 i+1 级台阶与第 i+2 级台阶可以作为第i级台阶的后继节点,可以构造出如下图所示的DAG:

本题则转换为一个DAG上求路径总数的问题,求方案总数可以不关心边权,根据上文讲解的求解方法,对于任意节点 i,定义状态dp[i]表示从起点到达节点 i 的路径总数,则dp[i]就等于其所有前驱 j 的方案总数dp[j]之和,在本题中,i 的前驱就是i - 1与 i - 2,因此状态转移方程就为dp[i] = dp[i-1] + dp[i-2]。
更多推荐



所有评论(0)