php 网页最右下角,算法题:动态规划

动态规划是一种优化策略,用于解决最优化问题。它通过保存之前计算的结果避免重复计算,提高效率。本文以斐波那契数列为案例,详细解释了动态规划的组成部分:初始条件、边界情况和状态转移方程,并展示了如何优化代码。接着,通过硬币组合和机器人路径问题,阐述了解决动态规划问题的步骤,包括确定最后一步、子问题和状态转移方程。最后,给出了动态规划在数组、最小硬币组合和最短路径问题上的应用示例。
摘要由CSDN通过智能技术生成

动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程。

动态规划的核心思想:下一个状态可以由上一个状态推导而来,因此可以利用数组保存之前计算的结果,避免重复计算。

我们来看一个经典案例:斐波那契数列function fibonacci(n) {

if(n == 1 || n == 2) return 1;

return fibonacci(n - 1) + fibonacci(n - 2)

}

上面的代码看似简单,但其实存在一个严重问题。通过下图我们可以看出,在递归过程中,很多节点被重复计算了,这些无疑导致了性能损失,而且随着问题规模的变大,重复计算的节点呈几何级数增长。

bVcQzDJ

那么怎么样优化这个流程呢?由斐波那契数列的特性可以看出,第 n 个值可以由第 n-1 和第 n-2 个值推导出来,例如我们要求第 10 个值,可以确定第 10 个值必然也是一个数,而且这个数可以由第 9 和第 8 个值相加得出,这样我们就确定了 最后一步 和 子问题 :最后一步:f(10) 一定也是一个数

子问题:f(10) = f(9) + f(8)

由于初始状态已知,即 f(1) = 1 以及 f(2) = 1 ,因此我们可以从最初的状态不断向后递推,即 f(n) = f(n-1) + f(n-1) ,然后用一个数组保存之前的状态,避免重复计算。function fibonacci(n) {

if (n == 1 || n == 2) return 1; // 边界值处理

let dp = new Array(n);

dp[0] = 1; // 初始条件

dp[1] = 1;

for (let i=2; i

dp[i] = dp[i-1] + dp[i-2]; // 状态转移方程

}

return dp[n-1]

}

上面的代码中,我们用了一个长度为 n 数组保存之前的状态,时间复杂度降低到了 O(n) ,极大提高了效率。从上面的代码中,我们可以总结出动态规划的两个核心组成部分:初始条件和边界情况 以及 状态转移方程。所谓状态转移方程,也就是由上一状态推导到下一状态的递推公式。在上面的例子中,状态转移方程可以抽象为:f(i) = f(i-1) + f(i-2)

一般来说,解动态规划的题目都需要开一个数组,求第 n 个元素,就需要开一个长度为 n 的数组,空间复杂度为 O(n) 。但数组不是必须的,有些情况下是可以简化的。例如在斐波那契数列的例子中,我们只关心当前状态的前两个状态就行,不需要把之前所有的状态都给保存下来。因此,上面的代码还可以再优化,只创建一个长度为 2 的数组就满足需要了,空间复杂度降为常数阶。function fibonacci(n) {

if (n == 1 || n == 2) return 1;

let dp = [1, 1];

for (let i=2; i

let res = dp[0] + dp[1];

dp[0] = dp[1];

dp[1] = res % 1000000007;

}

return dp[1];

}上面的代码中使用了长度为 2 的数组,其实还可以再简单点,不用数组,用两个变量就行

总结一下,解动态规划的题目有以下几个步骤:先要确定状态(最后一步 + 子问题)

然后确定状态转移方程

确定初始条件和边界情况

下面一起分析几道经典例题进一步理解动态规划思想。你有三种硬币,2元、5元、7元,每种硬币足够多,买一本书需要27元,用最少的硬币组合

看到这个题,最直观的想法就是暴力枚举,想要硬币最少,那就用面值最大的去凑:

7+7+7+7 > 27 (排除)

7+5+5+5+5=27, 5枚硬币

7 +7 +7+2+2+2 6枚

很明显暴力枚举太慢了,而且会涉及到很多重复的计算。既然计算机可以通过内存保存之前的内容,又快,很明显,我们开一个数组来保存之前的状态。

确定状态

最后一步

对于这道题,虽然我们不知道最优策略是什么,但是最优策略肯定是K枚硬币,a1, a2, ....ak 面值加起来是27。所以一定有一枚最后的硬币: ak ,除掉这么硬币,前面硬币的面值加起来是 27-ak 。我们不关心前面的 k-1 枚硬币是怎么拼出 27-ak 的(可能有一种拼法,也可能有 100 种拼法),但是我们确定前面的硬币拼出了 27-ak 。

因为是最优策略, 所以拼出 27-ak 的硬币数一定要最少,否则这就不是最优策略了。

子问题

在确定了最后一步之后,我们得到了子问题:最少用多少枚硬币可以拼出 27-ak 。很明显,最后的那枚硬币只可能是 2 ,5 或者 7 中的某一个。因此我们可以列出下面几种情况:如果 ak=2 , f(27) = f(27-2)+1 (1代表最后一枚硬币2)

如果 ak=5 , f(27) = f(27-5)+1 (1代表最后一枚硬币5)

如果 ak=7 , f(27) = f(27-7)+1 (1代表最后一枚硬币7)

所以使用最少的硬币数 f(27) = min{f(27-2)+1, f(27-5)+1, f(27-7)+1}

状态转移方程

设状态 f(x) = 最少用多少枚硬币拼出 x

对于任意的 x : f(X) = min{f(X-2)+1, f(X-5)+1, f(X-7)+1}

初始条件和边界情况提出问题:

x-2, x-5, x-7 小于0怎么办?

什么时候停下来?

如果不能拼出Y, 就定义f[Y] = 正无穷。例如:拼不出f[1]=min{f(-1)+1, f(-3)+1, f(-6)+1}

初始条件:f[0] = 0个人觉得前两步其实不难,倒是这一步有点麻烦且易错

参考代码class Solution {

public int coinChange(int[] coins, int amount) {

int[] dp = new int[amount+1];

dp[0] = 0; // 初始值

for (int i=1; i<=amount; i++) {

dp[i] = Integer.MAX_VALUE;

for (int j=0; j

if (i >= coins[j] && dp[i - coins[j]] != Integer.MAX_VALUE) {

dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);

}

}

}

if (dp[amount] == Integer.MAX_VALUE) {

dp[amount] = -1;

}

return dp[amount];

}

}

零钱兑换其实有点难的,不是很好理解。这道比较简单,很容易就能理解。一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角。

问总共有多少条不同的路径?

bVcQA6q

确定状态

最后一步

无论机器人用何种方式到达右下角,总有最后挪动的一步:向右或者向下。

如果设右下角的坐标为 (m-1, n-1) ,那么最后一步的上一步机器人的位置只可能在 (m-2, n-1) 或者 (m-1, n-2) 。

子问题

如果机器人从左上角走到 (m-2, n-1) 有 X 种方式,从左上角走到 (m-1, n-2) 有 Y 种方式,那么机器人从左上角走到 (m-1, n-1) 就有 X+Y 种方式。

问题转化为,机器人有多少种 方式从左上角走到 (m-2, n-1) 或者 (m-1, m-2) 。

如下图所示,如果是走到 (m-2, n-1) ,我们可以直接把最后一列去掉;同理如果走到 (m-1, n-2) ,可以直接把最后一行去掉。

bVcQA9k

状态转移方程

对于任意一个格子都有:f[i][j] = f[i-1][j] + f[i][j-1]f[i][j] 代表机器人有多少种方式走到 [i][j]

f[i-1][j] 代表机器人有多少种方式走到 [i-1][j]

f[i][j-1] 代表机器人有多少种方式走到 [i][j-1]

初始条件和边界情况

初始条件:f[0][0]=1 ,因为机器人只有一个方式到左上角

边界情况:在第 0 行或者第 0 列,即 i=0 或 j=0 的情况,都只有一种走法。在其他位置都满足状态转移方程。

参考代码class Solution {

public int uniquePaths(int m, int n) {

int[][] dp = new int[m][n]; // 使用二维数组来记录某一位置有几种走法

for (int i=0; i

for (int j=0; j

if (i == 0 || j == 0) {

dp[i][j] = 1; // 边界情况

} else {

dp[i][j] = dp[i-1][j] + dp[i][j-1]; // 其他情况都满足状态方程

}

}

}

return dp[m-1][n-1];

}

}class Solution {

public int maxSubArray(int[] nums) {

int len = nums.length;

int[] dp = new int[len];

dp[0] = nums[0];

int max = nums[0];

for (int i=1; i

dp[i] = Math.max((dp[i-1] + nums[i]), nums[i]);

max = Math.max(dp[i], max);

}

return max;

}

}class Solution {

public int minPathSum(int[][] grid) {

int rows = grid.length, columns = grid[0].length;

int[][] dp = new int[rows][columns];

dp[0][0] = grid[0][0];

for (int i=1; i

dp[i][0] = dp[i-1][0] + grid[i][0];

}

for (int j=1; j

dp[0][j] = dp[0][j-1] + grid[0][j];

}

for (int i=1; i

for (int j=1; j

dp[i][j] = Math.min(dp[i-1][j], dp[i][j-1]) + grid[i][j];

}

}

return dp[rows-1][columns-1];

}

}

参考:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值