个人微信公众号:程序员宅急送
之前的几篇文章我讲了分治,回溯,贪心三个思想,今天我将最后一个动态规划。
一、什么是动态规划?
动态规划是一种类似于回溯的思想,他通过空间换时间的方法提高效率,一般用于求最优解问题。
二、动态规划能解决什么类型的问题?
在上面我说道,动态规划其实和回溯很类似。回溯思想在前面的文章我提到过,回溯是一种穷举可能的方法。
动态规划则是通过额外的空间将回溯的递归树中不需要的可能提前剪除达到提高效率的目的。
我们来回忆一下回溯能解决的问题的特征
1、重复子问题(能够分成一个一个重复的阶段)
2、无后效性(后面的选择不会影响前面)
那么动态规划能解决什么问题呢?
1、最优子结构。意思就是说我们的最后的最优解结果是可以通过每一个阶段的最优解一步一步推导出来的。(是不是有点点贪心的味道)回溯为什么没有这个?因为回溯是穷举。
2、无后效性。和回溯一样,我们在处理某一个阶段的问题是,我们只关心当前阶段不需要考虑当前的阶段怎么来的,也不需要考虑后面的阶段会不会改变现在的这个阶段的状态。
3、重复子问题,这个最好理解。因为我们把问题的求解过程分割成了一个一个子问题,这些子问题重复才更容易处理。
三、例题
假设我们有一个4X4的矩阵w。如下图
每一个格子中的数代表相邻格子到达这个格子需要走的距离,我要从左上角走到右下角,每次都只能向右或者向下,求最短路径。
我们先用回溯的思想去解决这个问题,可以画出下面这张简图。
我使用(x坐标,y坐标,当前已走距离)的方法表示状态。
上面这张图我是穷举回溯的每个阶段的状态。(重复子问题)
因为我们的目标是最短路径,也就是说到达相同的位置我只需要走过的路最少的那种方案。
从上面那张图中我们可以看到有些状态的x坐标,y坐标相同但是走过的距离不同,这个时候我们只需要选择里面最优解。(最有子结构)如下图
通过这种剪枝操作,我们能够介绍绝大部分的无效操作,大大提高效率。
四、代码
int num_map[4][4] = { 1,3,5,9,
2,1,3,4,
5,2,6,7,
6,8,4,3,
};
int status[4][4] = { 0 };
int dynamicProcess()
{
int sum = 0;
//初始化第一行
for (int i = 0; i < 4; ++i)
{
sum += num_map[0][i];
status[0][i] = sum;
}
sum = 0;
//初始化第一列
for (int j = 0; j < 4; ++j)
{
sum += num_map[j][0];
status[j][0] = sum;
}
//因为只能向右或者向下,所以上一步只会来自于当前块的上或者左边
//我取二者种的较小值作为上一步。
for (int i = 1; i < 4; ++i)
{
for (int j =1;j<4;++j)
{
int cur_distance = min(status[i-1][j]+ num_map[i][j], status[i][j-1] + num_map[i][j]);
status[i][j] = cur_distance;
if (i == 3 && j == 3)
return status[i][j];
}
}
}
五、回顾总结
1、我首先将最优路径问题分割成了一个一个小的最短路径(重复子问题)
2、在每一个小的选择中,我只使用了较短路径(最优子结构),因为最终的结果一定是多个最短路径构成
3、我的每一次选择都不会影响和改变前面的(无后效性),如果说我在后面选择了某一格子使得路径额外+10(大富翁睡眠10分治=。=)。那么这个问题就不能用动态规划了。因为后面的结果有了额外的影响。
4、其实我觉得,能用回溯解决的问题,绝大多数都能用动态规划解决,只不过比较难想到。所以我建议,凭空想象不如用笔画一画。