1 题目场景
1 计数
- 有多少种方式走到右下角;
- 有多少种方法选出k个数使得和为sum;
2 求最值
- 从左上角走到右下角路径的最大数字和;
- 最长不含重复字符的子字符串;
3 求存在性
- 取石子游戏,先手是否必胜;
- 能不能选出k个数使得和是sum;
2 解题步骤
- 确定状态(两个核心:1、最后一步;2、化成子问题得状态方程);
- 建立状态转移方程;
- 确定开始和边界条件;
- 计算顺序;
3 案例分析
3.1 *最少的硬币组合
- 有三种硬币,分别是2元、5元和7元;
- 买一本书需要27元;
- 如何用最少的硬币组合正好付清,不需要对方找钱;
3.1.1 确定状态
3.1.1.1 最后一步
- 最优策略是k枚硬币,最后一个硬币的面值是ak,则前面的k-1枚硬币总值为27-ak;
- 我们不关心前面的k-1枚硬币是怎么拼出27-ak的,甚至不知道ak和k,但可以确定前面的硬币拼出了27-ak;
- 因为是最优策略,所以拼出27-ak的硬币数一定要最少;
3.1.1.2 化成子问题得状态方程
- 原问题是用最少多少枚硬币拼出27,现将其转化成子问题,而且规模更小:用最少多少枚硬币拼出27 - ak;
- 状态方程:f(x) = 最少多少枚硬币拼出x;
3.1.2 建立状态转移方程
- 最后那枚硬币可能是2、5或7,因此f(27) = min{f(27-2), f(27-5), f(27-7)} + 1;
- 可得状态转移方程:f(x) = min{f(x-2), f(x-5), f(x-7)} + 1;
3.1.3 确定开始和边界条件
3.1.3.1 确定开始
f(0) = 0,即拼出0元需要0枚硬币。
3.1.3.2 边界条件
- 如果x小于0,意味着不能拼出x,于是令f(x) = 无穷大;
- 因此f(1) = min{f(-1), f(-4), f(-6)} + 1 = 无穷大,表示拼不出1;
3.1.4 计算顺序
- 从f(0)开始,计算f(1)、f(2)、f(3)、…、f(27);
- 也就说是,当计算到f(x)时,f(x-2)、f(x-5)、f(x-7)已经得到结果了;
- f(x)有两种情况:1、= 最少用多少枚硬币拼出x;2、= 无穷大表示无法用硬币拼出x;
代码(Java)
class Solution {
//coin代表硬币的种类[2,5,7],x代表要拼凑的值
public int coinChange(int[] coin, int x) {
int[] f = new int[x+1];
f[0] = 0;
for (int i=1; i<=x; i++) {
f[i] = (int)Double.POSITIVE_INFINITY; // 开始默认无穷大,即不能拼凑
for (int j=0; j<coin.length; j++) { // 三种面值的硬币各循环一次
if ((i - coin[j]) >= 0 && f[i - coin[j]] != (int)Double.POSITIVE_INFINITY) {
f[i] = Math.min(f[i], f[i - coin[j]] + 1);
}
}
}
if (f[x] == (int)Double.POSITIVE_INFINITY) {
return -1;
}
return f[x];
}
public static void main(String[] args) {
Solution s = new Solution();
int[] coin = new int[] {2, 5, 7};
System.out.println(s.coinChange(coin, 27));
}
}
5
3.2 *多少种方式走到右下角
给定m*n的网格,机器人从左上角(0,0)出发,每一步可以向下或向右走一步,有多少种不同的方式走到右下角?
3.2.1 确定状态
3.2.1.1 最后一步
右下角坐标(m-1,n-1),前一步机器人向右或向下到达右下角,前一步机器人的坐标(m-2,n-1)或(m-1,n-2)。
3.2.1.2 化成子问题
- 设机器人有x种方式从左上角走到(m-2,n-1),有y种方式从左上角走到(m-1,n-2),因此共有x+y种方式走到(m-1,n-1);
- 子问题:机器人分别有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2);
- 状态方程:设f(x)(y) = 机器人有多少种方式从左上角走到(i,j);
3.2.2 建立状态转移方程
- 前一步可能在最后一步的左边或上边;
- 可得状态转移方程:f(x)(y) = f(x-1)(y) + f(x)(y-1);
3.2.3 确定开始和边界条件
3.2.3.1 确定开始
f(0)(0) = 1,机器人只有一种方式到左上角。
3.2.3.2 边界条件
x = 0或y = 0,前一步唯一,因此f(x)(y) = 1。
3.2.4 计算顺序
- 从f(0)(0)开始,利用双层for循环,依次计算每一行的值;
- 答案即f(m-1)(n-1);
代码(Java)
class Solution {
public int moveCounts(int m, int n) {
int[][] matrix = new int[m][n];
for (int i=0; i<m; i++) {
for (int j=0; j<n; j++) {
if (i==0 || j==0) matrix[i][j] = 1;
else matrix[i][j] = matrix[i-1][j] + matrix[i][j-1];
}
}
return matrix[m-1][n-1];
}
public static void main(String[] args) {
Solution test = new Solution();
System.out.println(test.moveCounts(3,2));
}
}
3
3.3 *青蛙能否跳到第n个石头上
- 有n块石头分别置于x轴的0,1,…,n-1位置;
- 一只青蛙在石头0,想跳到石头n-1;
- 如果青蛙在第i块石头上,它最多可以向右跳a[i];
- 问青蛙能否跳到石头n-1;
例1:
输入:a=[2,3,1,1,4]
输出:True
例2:
输入:a=[3,2,1,0,4]
输出:False
3.3.1 确定状态
3.3.1.1 最后一步
- 如果青蛙能跳到最后一块石头n-1,假设最后一步是从石头i跳过来的,i < n-1;
- 需要满足两个条件:1、青蛙可以跳到石头i;2、从石头i可以跳到石头n-1,即n-1-i <= a[i];
3.3.1.2 化成子问题得状态方程
- 原问题求青蛙能不能跳到石头n-1,现将其转化成子问题:求青蛙能不能跳到石头i(0 <= i < n-1);
- 状态方程:f(x):青蛙能不能跳到石头x;
3.3.2 建立状态转移方程
得状态转移方程f(x) = OR0<=i<x(f(i) AND i + a(i) >= x)。
- 其中OR0<=i<x表示枚举上一次跳到的石头i
3.3.3 确定开始和边界条件
3.3.3.1 确定开始
f(0) = True,因为青蛙一开始就在石头0
3.3.3.2 边界条件
无
3.3.4 计算顺序
- 初始化f(0) = True;
- 计算f(1)、f(2)、…、f(n-1);
- 答案是f(n-1);
代码(Java)
class Solution {
public boolean canJump(int[] a) {
int n = a.length;
boolean[] dp = new boolean[n];
dp[0] = true;
for(int i=1; i<n; i++) {
dp[i] = false;
for (int j=i-1; j>=0; j--) {
if(dp[j] && j + a[j] >= i) {
dp[i] = true;
break;
}
}
}
return dp[n-1];
}
public static void main(String[] args) {
Solution test = new Solution();
int[] a = {2,3,1,1,4};
int[] b = {3,2,1,0,4};
System.out.println(test.canJump(a));
System.out.println(test.canJump(b));
}
}
也可通过维护可到达的最远距离降低时间复杂度和空间复杂度,参见 https://blog.csdn.net/sc179/article/details/115413793