写点什么

算法题目解析:从一道题目看动态规划

发布于: 2021 年 04 月 22 日
算法题目解析:从一道题目看动态规划

一 简介

动态规划(Dynamic Programming,简称 DP)问题,是算法中比较让人头疼的问题之一(算法大神请绕路)。原理并不复杂,相信大家也都能说出大概以及关键步骤:确定状态转移方程。但实际做题时,还是很难确保得心应手。本篇将尝试进行解析,并结合示例看如何能够有效理解并深入,再后续遇到 DP 问题时可以快速解答。

动态规划的应用极其广泛,包括工程技术、经济、工业生产、军事以及自动化控制等领域,并在背包问题、生产经营问题、资金管理问题、资源分配问题、最短路径问题和复杂系统可靠性问题等中取得了显著的效果。(本段来自百度百科)

二 概念

2.1 什么是动态规划

动态规划算法通常基于一个递推公式(状态转移方程)和一个或多个初始状态。当前子问题的解将由上一次子问题的解推导而出。使用动态规划解决问题需要多项式时间的复杂度,因此比回溯、暴力等方法要快很多。

为什么叫动态规划?首先,动态规划解决的问题,过程可以分为若干个相互关联的“阶段”,每个阶段都要作出“决策”,使整个过程达到最好的效果。因此各阶段的决策依赖于当前面临的“状态”,又影响以后的“发展”。各阶段决策确定后,就组成一个决策序列。所以这类问题业往往可以成为多阶段决策问题。这类问题中,各阶段的决策,既依赖于当前状态,又会引起状态转移,所以会成为“动态”。

2.2 基本思想

动态规划算法通常用于求解具有某种最优性质的问题。这类问题可能会有许多可行解。每一个解都对应于一个值,希望找到的是具有最优值的解。动态规划算法与分治法类似,基本思想也是将待求解问题分解成若干个子问题,先求解子问题,然后从这些子问题的解得到原问题的解。与分治法的差别在于,适合用动态规划求解的问题,经分解得到子问题往往不是互相独立的。若用分治法来解这类问题,则分解得到的子问题数目太多,有些子问题被重复计算了很多次。通过保存已解决的子问题的答案,在需要时再找出已求得的答案的方式,避免大量的重复计算,节省时间。

我们可以用一个表来记录所有已解的子问题的答案。不管该子问题以后是否被用到,只要它被计算过,就将其结果填入表中。这就是动态规划的基本思路。具体的动态规划算法多种多样,但它们具有相同的填表格式。

2.3 局限

动态规划对于解决多阶段决策问题的效果是明显的,但是动态规划也有一定的局限性。首先,它没有统一的处理方法,必须根据问题的各种性质并结合一定的技巧来处理;另外当变量的维数增大时,总的计算量及存贮量急剧增大。因而,受计算机的存贮量及计算速度的限制,当今的计算机仍不能用动态规划方法来解决较大规模的问题,这就是“维数障碍”。

三 问题实例

3.1 问题描述

选择 leetcode 的第 62 题:62. 不同路径,问题描述:

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。问总共有多少条不同的路径?

示例:


如上图所示,m=3,n=3,从左上角到右下角,共有 3 中可能的路径,用数组位置表示:

路径 1:arr[0,0]->arr[0,1]->arr[0,2]->arr[1,2]

路径 2:arr[0,0]->arr[0,1]->arr[1,1]->arr[1,2]

路径 3:arr[0,0]->arr[1,0]->arr[1,1]->arr[1,2]

3.2 问题解析

通过上面的示例,可以了解问题的含义和限制条件。用 i 表示横坐标,j 表示纵坐标,f(i,j) 表示从左上角走到 (i, j)的路径数量,其中 i 和 j 的范围分别是 [0, m) 和 [0, n)。

由于我们每一步只能从向下或者向右移动一步,因此要想走到 (i, j),如果向下走一步,那么会从 (i-1, j)走过来;如果向右走一步,那么会从 (i, j-1)走过来。因此我们可以写出动态规划转移方程:

f(i,j)=f(i−1,j)+f(i,j−1)

需要注意的是,如果 i=0,那么 f(i-1,j)并不是一个满足要求的状态,需要忽略这一项;同理,如果 j=0,那么 f(i,j-1)并不是一个满足要求的状态,需要忽略这一项。

初始条件为 f(0,0)=1,即从左上角走到左上角有一种方法。

最终的答案即为 f(m-1,n-1)

3.3 代码

public int uniquePaths(int m, int n) {    int[][] f = new int[m][n];    for (int i = 0; i < m; ++i) {        f[i][0] = 1;    }    for (int j = 0; j < n; ++j) {        f[0][j] = 1;    }    for (int i = 1; i < m; ++i) {        for (int j = 1; j < n; ++j) {            f[i][j] = f[i - 1][j] + f[i][j - 1];        }    }    return f[m - 1][n - 1];}
复制代码

3.4 复杂度分析

时间复杂度:O(mn)。

空间复杂度:O(mn),即为存储所有状态需要的空间。注意到 f(i, j)仅与第 i 行和第 i-1 行的状态有关,因此我们可以用滚动数组代替代码中的二维数组,使空间复杂度降低为 O(n)。此外,由于交换行列的值并不会对答案产生影响,因此我们总可以通过交换 m 和 n 使得 m<=n,这样空间复杂度降低至 O(min(m, n))。

四 总结

再回顾一下解题过程:

1)创建数组,存储阶段(子问题)的解(决策);

2)初始化,如果 1)中创建的是二维数组,那么就是初始化 arr[0][0]~arr[0][n] 和 arr[0][0]~arr[m][0];

3)这样后面在计算时就可以在循环中使用前面子问题的解了(状态转移方程的执行)

4)输出计算结果

发布于: 2021 年 04 月 22 日阅读数: 19
用户头像

磨炼中成长,痛苦中前行 2017.10.22 加入

微信公众号【程序员架构进阶】。多年项目实践,架构设计经验。曲折中向前,分享经验和教训

评论

发布
暂无评论
算法题目解析:从一道题目看动态规划