有趣的动态规划

一、理论基础和模板

动态规划(Dynamic Programming)的理论基础是运筹学;动态规划中有一些概念,比如重叠子问题(Overlapping Subproblems)、和最优子结构(Optimal Substructure)、状态转移方程等。

  • 重叠子问题(Overlapping Subproblems)

实际要解决的问题,可以通过解决这个问题的子问题而解决。比如斐波那契数列,F(n)可以通过F(n-1)与F(n-2)的和得到;

  • 最优子结构特性(Optimal Substructure Property)

如果问题的最优解,可以通过使用子问题的最优解获取,则说明这个问题具有最优子结构特性。

  • 状态(state)与转移(transition)方程
1.1 解决一个动态规划问题的4步曲
  • step 1.根据「重叠子问题」和「最优子结构特性」来判断目标问题是否可以通过动态规划解决
  • step 2.用最少的参数定义dp数组(状态),并确定dp数据以及对应下标的含义
  • step 3.确定dp数组的状态转移方程以及初始化;
  • step 4.使用「表格」(Tabulation)或者「备忘录」(Memoization)来保存dp,降低时间复杂度或空间复杂度(定义dp)

最难的就是步骤2和步骤3,如何根据问题,分析重叠子问题和最优子结构,进而定义好dp的含义以及dp的状态转移方程;在寻找递推关系时,可以分析dp[i]与dp[i-1]之前的关系,也有可能是dp[i]和dp[0]~dp[i-1]都有关系。二位的dp数组也类似,可能dp[i][j]与dp[i-1][j-1]有关系,也有可能和0->i-1,0->j-1都有关系,而且是取最值的关系。

1.2 代码模板
# 初始化 base case
dp[0][0][...] = base case
# 进行状态转移
for 状态1 in 状态1的所有取值:
    for 状态2 in 状态2的所有取值:
        for ...
            dp[状态1][状态2][...] = 求最值(选择1,选择2...)

二、递推问题引入

  • 斐波那契数列

斐波那契数列并非严格意义的动态规划,但是它和动态规划类似,有重叠子问题,并且状态转移方程比较明确:

dp[i]=dp[i-1]+dp[i-2]

如下:

	public int fib(int n) {
   
        if (n <= 1) {
   
            return n;
        }
        int[] dp = new int[n + 1];
        dp[0] = 0;
        dp[1] = 1;
        for (int i = 2; i < n + 1; i++) {
   
            dp[i] = dp[i - 1] + dp[i - 2];
        }
        return dp[n];
    }

以上代码只用于演示,实际则有int越界的问题,需要通过

题目如下,详见题目不同路径

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

其实从”回溯“算法的角度,这个题目很好写,按照回溯算法的套路,很快就可以枚举所有的路径,如下:


	public int uniquePaths(int m, int n) {
   
        bt(0, 0, m, n);
        return sum;
    }

    private int sum = 0;

    public void backtrack(int stepX, int stepY, int m, int n) {
   
        //搜集结果
        if (stepX == m - 1 && stepY == n - 1) {
   
            sum++;
            return;
        }

        //终止条件
        if (stepX > m - 1 || stepY > n - 1) {
   
            return;
        }

        //做选择,或者向下,或者向右

        //先向右做选择:+回溯
        if (stepY < n - 1) {
   
            stepY++;
            backtrack(stepX, stepY, m, n);
            stepY--;
        }

        //再向下做选择:+回溯
        if (stepX < m - 1) {
   
            stepX++;
            backtrack(stepX, stepY, m, n);
            stepX--;//由于后续不再消费stepX,此代码可以省略
        }
    }

甚至根据回溯算法,也可以把「路径」保存并打印出来。但回溯算法最大的问题是时间复杂度太高,如果以上述代码提交,会出现超时的情况。此时我们就需要考虑动态规划了。

我们可以通过观察,查找对应的递推关系:当我们要走到{i,j}这个位置时,根据题目表述,我们可以通过向下或者向右得到,所以我们定义dp[i][j]为走到i*j网格需要的路径总数,那么由于走到{i,j}的坐标,可以有坐标需要{i-1,j}和{i,j-1}分别向右和向下走到,因此dp[i][j] = dp[i][j-1]+dp[i-1][j];而问题的解,正好是dp[m-1][n-1];代码如下:


	public int uniquePaths(int m, int n) {
   
        int[][] dp = new int[m][n];

        //初始化
        for (int i = 0; i < dp[0].length; i
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值