LeetCode|动态规划入门三题
一、爬楼梯
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例 1:
输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
示例 2:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
解题思路
本题是动态规划里面最简单的题目了,印象中第一次见到这个题目还是在蓝桥杯的练习题中。
虽然本篇是动态规划的入门文章,但是我并不会说任何书面上写的动态规划算法的定义。这是因为动态规划的概念还是很复杂的,看完动态规划算法的思路和特性估计你就被动态规划吓到了。
关于动态规划的定义,我只说一句个人看法——动态规划求的是全局最优解。全局意味着动态规划是要全盘考虑问题的。话不多说,我们先从题目解答中开始体会。
本题中,我们看到,爬1阶梯只有1种方法,爬2阶梯有2种方法。那我们就能想到,爬3阶梯有3种,这是因为爬3阶等于爬2阶的基础上再爬1阶。同理爬4阶等于在3阶的基础上爬1阶和2阶的基础上爬2阶;
这种“站在巨人的肩膀上”的思路就是动态规划的思想。
实现代码
首先我们能想到的是使用递归的方法。递归方法跟我们上面的思路正好反过来。递归的思路是爬n阶楼梯的方式等于爬n-1阶楼梯的次数加上n-2阶楼梯的次数。但是递归方法不会保存计算结果,导致有很多重复的计算,因此超时也就不可避免了。
1. 递归
class Solution {
public int climbStairs(int n) {
if(n==1) return 1;
if(n==2) return 2;
return climbStairs(n-1)+climbStairs(n-2);
}
}
运行结果:超时
2. 动态规划
动态规划的整体思路跟递归很像,但是会通过一个dp数组保存计算结果。这样计算速度就会快很多。
class Solution {
public int climbStairs(int n) {
int[] steps=new int[n];
if(n==1){
return 1;
}
steps[0]=1;
steps[1]=2;
for(int i=2;i<n;i++){
steps[i]=steps[i-1]+steps[i-2];
}
return steps[n-1];
}
}
二、连续子数组的最大和
给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例 1:
输入:nums = [-2,1,-3,4,-1,2,1,-5,4]
输出:6
解释:连续子数组 [4,-1,2,1] 的和最大,为 6 。
示例 2:
输入:nums = [1]
输出:1
示例 3:
输入:nums = [0]
输出:0
示例 4:
输入:nums = [-1]
输出:-1
示例 5:
输入:nums = [-100000]
输出:-100000
提示:
1 <= nums.length <= 3 * 104
-105 <= nums[i] <= 105
进阶: 如果你已经实现复杂度为 O(n) 的解法,尝试使用更为精妙的 分治法 求解。
解题思路
动态规划是本题的最优解。动态规划的思路也很清晰。假设有一个数组[-2,1]
,那么我们可以看到[-2,1]
两数的和比数组第二个元素1要小。因此连续子数组的最大和为1。当数组变为[-2,1,-3]
的时候,我们已经知道[-2,1]
这部分数组的连续子数组的最大和为1。那么1跟[-3]
比较是1比较大,因此这个数组的连续子数组的最大和依然为1。
简而言之,每次数组新增一个元素,都需要跟增加前的数组的连续子数组的最大和相比较,结果较大的那个就是新增后数组的连续子数组的最大和。代码实现如下:
实现代码
class Solution {
public int maxSubArray(int[] nums) {
int dp[]=new int[nums.length];
dp[0]=nums[0];
int max=dp[0];
for(int i=1;i<dp.length;i++){
//compare the num last status plus current num and current num
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
//compare max and dp[i],it will update max
max=Math.max(max,dp[i]);
}
return max;
}
}
三、游戏币组合
硬币。给定数量不限的硬币,币值为25分、10分、5分和1分,编写代码计算n分有几种表示法。(结果可能会很大,你需要将结果模上1000000007)
示例1:
输入: n = 5
输出:2
解释: 有两种方式可以凑成总金额:
5=5
5=1+1+1+1+1
示例2:
输入: n = 10
输出:4
解释: 有四种方式可以凑成总金额:
10=10
10=5+5
10=5+1+1+1+1+1
10=1+1+1+1+1+1+1+1+1+1
说明:
注意:
你可以假设:
0 <= n (总金额) <= 1000000
解题思路
这道题的状态转移方程似乎不那么容易看出来,不过我们可以列一个表格来观察一下,看能不能发现一点什么规律。
硬币数\总面值 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
1 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 1 |
2 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
3 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
4 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 |
5 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 | 0 |
6 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 1 |
7 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
9 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
10 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
f(n) | 1 | 1 | 1 | 1 | 2 | 2 | 2 | 2 | 2 | 4 |
首先我们定义一个dp数组和所有的硬币数组
int[] dp = new int[n + 1];
int[] coins = {1,5,10,25};
并且设置dp[0]=1,为边界的条件,作为完美能被一个硬币表示的情况为 1。
dp[i]+=dp[i-coin]
当i-coin为0时结果为1,表示一个硬币能表示的情况。
我们可以简单的得出状态转移方程
// 题目中需求对结果取模1000000007
dp[i] = dp[i] + dp[i - coin]
代码实现
class Solution {
public int waysToChange(int n) {
int dp[]=new int[n+1];
int coins[]={1,5,10,25};
dp[0]=1;
for(int coin : coins) {
for(int i = coin; i <= n; i++) {
dp[i] = (dp[i] + dp[i - coin]) % 1000000007;
}
}
return dp[n];
}
}