个人理解
:::info
1.可以根据上一个或几个状态递推出来
2.1维和2维的区别是有几个未知参数【不一定】
3.形式如不用个例子列举,只得出一个结果优先考虑动态规划,不行就递归
:::
Case 1 滚动数组式动态规划
大富翁游戏,玩家根据骰子的点数决定走的步数,即骰子点数为1时可以走一步,点数为2时可以走两步,点数为n时可以走n步。求玩家走到第n步(n<=骰子最大点数且是方法的唯一入参)时,总共有多少种投骰子的方法。
分析 1 骰子🎲一个有六种可能,其实可优化为1维数组
| 含义 | 可能的骰子 | 状态1【用于累加】 | n = 1 | n = 2 | n = 3 | n = 4 |
|---|---|---|---|---|---|---|
| 每次都是1 | 1 | 1 | 1 | 1 | 1 | 1 |
| 包含12 | 2 | 1 | 1 | 2 | 3 | 5 |
| 包含123 | 3 | 1 | 1 | 2 | 4 | 7 |
| 包含1234 | 4 | 1 | 1 | 2 | 4 | 8 |
| 包含12345 | 5 | 1 | 1 | 2 | 4 | 8 |
| 包含123456 | 6 | 1 | 1 | 2 | 4 | 8 |
状态转移方程
i-j >= 0 dp[i] = dp[i-j] j是1-6的累加和 【滚动数组源于不用使用前一个状态的结果】
例如 当骰子🎲只会出现1,2面时 n=4 可以有 n = 3 状态 和 n = 2 的状态 累加
当骰子🎲只会出现1,2,3面时 n=4 可以有 n = 3 状态 和 n = 2 的状态 和 n = 1的状态累加
public class Main {public static void main(String[] args) {System.out.println(new Main().kind(3));}int kind(int n){int[] dp = new int[n+1];dp[0] = 1;for (int i = 1; i <= n; i++) {for (int j = 1; j <= 6; j++) {if (i-j >= 0)dp[i] = dp[i] + dp[i-j];}}return dp[n];}}
Case 2 笔试场景动态规划
有n个积木,每个积木长读为2,要在长度为k的空地上将这些个积木摆放下来。每一个积木必须摆放在前一个积木之上(也就是摆放在它前一格或后一格),问一共有多少种摆法,结果对998244353取模。
分析 1 第i个木块可能有两个选择,⬅️上角和➡️上角,此外需要考虑k的边界限制
矩阵宽度 代表长度为2的积木放在k的场地上有几种放法
矩阵长度 代表有几个积木
d[i][j]代表第i个积木放在j位置有几种 需要二维数组,因为需要i-1和j-1
表格【假设k = 4,n = 4】
初始化
| 含义 | 1个积木 |
|---|---|
| 当上个积木位置是[0,1]此位置有几种放法 | 1 |
| 当上个积木位置是[1,2]此位置有几种放法 | 1 |
| 当上个积木位置是[2,3]此位置有几种放法 | 1 |
状态转移递推
| 2个积木 | 3个积木 | 4个积木 | 5个积木 |
|---|---|---|---|
| 1 | 2 | 2 | 4 |
| 2 | 2 | 4 | 4 |
| 1 | 2 | 2 | 4 |
状态转移方程
当 j+1 < k的时候,即可以放在➡️上角时候,dp[i][j] += dp[i-1][j+1]
当 j > 0 的时候,即可以放在⬅️上角时候,dp[i][j] += dp[i-1][j-1]
当 0 < j < k-1 时 ⬅️上角 和 ➡️上角都可以放置 dp[i][j] += (dp[i-1][j+1] + dp[i-1][j-1])
当5个积木时,放在[0,1]位置有4种放法,放在[1,2]位置有4种放法,放在[2,3]位置有4种放法。
static int MOD = 998244353;public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int n = scanner.nextInt();int k = scanner.nextInt();// dp[i][j]表示将第i个积木放在起始位置为j的位置有多少种摆法// 状态转移也很好想,因为当前积木要么放在前一个积木的前上方// 要么放在前一个积木的后上方,注意边界情况long[][] dp = new long[n+1][k-1];long ans = 0;Arrays.fill(dp[1],1);System.out.println("初始化dp = "+ Arrays.deepToString(dp));for(int i = 2;i <= n;i++){for(int j = 0;j < k - 1;j++){if(j > 0){dp[i][j] = dp[i-1][j-1];dp[i][j] %= MOD;}if(j + 1 < k - 1){dp[i][j] = dp[i-1][j+1];dp[i][j] %= MOD;}if (j > 0 && (j + 1 < k - 1)){dp[i][j] = (dp[i-1][j+1] + dp[i-1][j-1]);dp[i][j] %= MOD;}}}System.out.println("dp = "+ Arrays.deepToString(dp));for(int i = 0;i < k-1;i++){ans = (ans + dp[n][i])%MOD;}System.out.println(ans);}
Case 3
给定一个正整数 n ,将其拆分为 k 个 正整数 的和( k >= 2 ),并使这些整数的乘积最大化。 返回 你可以获得的最大乘积 。
确定递推公式
可以想 dp[i]最大乘积是怎么得到的呢?
其实可以从1遍历j,然后有两种渠道得到dp[i].
- 一个是j * (i - j) 直接相乘。
- 一个是j * dp[i - j],相当于是拆分(i - j)
public int integerBreak(int n) {int[] dp = new int[n+1];dp[1] = 1;for(int i = 2;i <= n;i++){for(int j = 1;j < i;j ++){dp[i] = Math.max(dp[i],j*(i-j));dp[i] = Math.max(dp[i],dp[j]*(i-j));}}return dp[n];}
Case 3 对角线式动态规划
给你一个字符串 s ,请你统计并返回这个字符串中 回文子串 的数目。回文字符串 是正着读和倒过来读一样的字符串。子字符串 是字符串中的由连续字符组成的一个序列。具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 示例 1: 输入:s = “abc” 输出:3 解释:三个回文子串: “a”, “b”, “c” 示例 2: 输入:s = “aaa” 输出:6 解释:6个回文子串: “a”, “a”, “a”, “aa”, “aa”, “aaa”
递推分析
举例 “aba”
转移方程
初始化 a,b,c都为回文
为了兼容”aa”和”aba”这种情况,需设置i-j >= 2的限制,返回为true
dp数组呈现对角线以上是边界范围,1行3列代表从’a’到’c’即”abc”的子串是否符合,此时两个条件’a’ == ‘c’ 相等并且 前一个状态2行2列的 “b” 需要是回文子串。
| a 1 | b 2 | c 3 | |
|---|---|---|---|
| a 1 | true | false | true |
| b 2 | true | false | |
| c 3 | true |
public int countSubstrings(String s) {int n = s.length();int max = 0;boolean[][] dp = new boolean[n+1][n+1];System.out.println("初始化dp ");for (int i = 1; i <= n; i++) {dp[i][i] = true;}System.out.println(Arrays.deepToString(dp));System.out.println("最终dp ");for (int i = n; i >= 1; i--) {for (int j = i+1; j <= n; j++) {if (s.charAt(i-1) == s.charAt(j-1)){if (dp[i+1][j-1] || j-i <= 2){dp[i][j] = true;}}}}System.out.println(Arrays.deepToString(dp));for (int i = 1; i <= n; i++) {for (int j = i; j <= n; j++) {if (dp[i][j]){max += 1;}}}return max;}
