文章目录
前言:刚开始学或做动态规划的题目的时候,能用dp数组做出来的,就用dp数组,别理那些用几个临时变量的,你dp数组用多了,理解了,自然就会去改进,所以不要一上来就要求那么高,这样容易费时间又打击自己,想想一开始我们都是暴力法慢慢走过来的。
基础
70. 爬楼梯
https://leetcode.cn/problems/climbing-stairs/
dp[n] = dp[n-1] + dp[n-2]
class Solution {
public int climbStairs(int n) {
int n2 = 0;
int n1 = 1;
int n3 = 0;
while(n-- > 0) {
n3 = n1 + n2;
n2 = n1;
n1 = n3;
}
return n3;
}
}
62. 不同路径
思路:动态规划
跟下一题基本一样,由于机器人只能向下或向右,说明到达某个点的路径之和等于上面+左边。
dp[i][j]
代表从起点到该点的路径条数。从而可得动态方程为dp[i][j] = dp[i-1][j] + dp[i][j-1]
。
💡特殊处理,第一行和第一列的每个节点路径都是1。这里直接在for循环里面使用if判断。
class Solution {
public int uniquePaths(int m, int n) {
int[][] sf = new int[m][n];
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (i == 0 || j == 0) {
sf[i][j] = 1;
} else {
sf[i][j] = sf[i - 1][j] + sf[i][j - 1];
}
}
}
return sf[m - 1][n - 1];
}
}
同类题型:63. 不同路径 II
64. 最小路径和
思路:动态规划
由于每个点只能向右或向下,说明到达某个点的最小路径只有两种可能,要么是来自左边,要么来自上面。
用dp[i][j]
代表起点到(i, j)
的最小路径。从而可得出动态方程为dp[i][j] = Max(dp[i-1][j], dp[i][j-1]) + grid[i][j]
,按正常顺序遍历矩阵,就能获得每个点的最小路径。
💡特殊处理,对首行的点,方向只能由左往右,最小路径的计算为dp[0][j] = dp[0][j-1] + grid[0][j]
。首列同理。
🤔这里的dp数组由grid代替,因为矩阵的值并不需要保留,每次用完就没用了。
class Solution {
public int minPathSum(int[][] grid) {
int l1 = grid.length;
int l2 = grid[0].length;
int k = 1;
//第一行
while (k < l2) {
grid[0][k] += grid[0][k - 1];
k++;
}
k = 1;
//第一列
while (k < l1) {
grid[k][0] += grid[k - 1][0];
k++;
}
for (int i = 1; i < l1; i++) {
for (int j = 1; j < l2; j++) {
grid[i][j] += Math.min(grid[i - 1][j], grid[i][j - 1]);
}
}
return grid[l1 - 1][l2 - 1];
}
}
🤔可以使用搜索,例如DFS,但是搜索会出现局部路径重合,也就是走过的路再走一次,这会浪费不必要的时间,而动态规划只需要遍历整个矩阵一次。
*303. 区域和检索 - 数组不可变
题意:求数组某个区间的元素之和。(要求调用次数很多)
思路:动态规划
最简单的思路当然是累计该区间,然后返回。但如果对该方法重复调用,就会m每次都计算一遍,可理解为没有缓存。
所以可通过保存每个区间的和,调用时,再返回即可,用dp[i][j]表示区间为[i,j]的数组元素和
。如果用暴力法的话,会出现重复计算的情况,例如求[0,3]的和,我们并不需要从第一个元素加到第四个元素,而是借助[0,2]的和
,其实就是动态规划,动态规划会帮我们记录中间的值,从而为下一个值的计算省不少事。这里也不难得出动态方程为dp[i][j] = dp[i][j-1] + nums[j]
。
但是这样还是比较慢的,因为需要用到两层for循环,其实我们并不需要计算所有区间,我们只需要计算0-1 0-2 0-3……0-n
这些区间,对于其它区间可通过减法获得,例如[2,3] = [0,3] - [0,1]
, 其中dp[i]代表0-i的区间和,于是可得新的动态方程为dp[i] = dp[i-1] + nums[i]
💡由于这里nums数组元素用过就不用了,所以直接用一个dp数组指向nums数组,而不是通过new。
class NumArray {
private int[] dp; //保存[0-1] [0-2]......[0-n]的数组区间和
public NumArray(int[] nums) {
int len = nums.length;
//指向nums
dp = nums;
for (int i = 1; i < len ;i++) {
dp[i] = dp[i-1] + nums[i];
}
}
public int sumRange(int left, int right) {
//特判left为0的情况
return (left == 0) ? dp[right] : dp[right]-dp[left-1];
}
}
198. 打家劫舍
偷钱,但不能偷相邻的。
思路:动态规划
nums[i]表示第i家存放的金额,dp[i]表示偷第i家时能获得的最高金额
,
其中dp[0]=nums[0],dp[1]=nums[1],dp[2]=dp[0]+arr[2],dp[3]=Max(dp[0],dp[1]) + nums[3],dp[3]之后都是如此。
也就是说当i>2时,偷第i家时能获得的最高金额取决于在i-2家或i-3家中所能偷到的最高金额,即dp[i]=Max(dp[i-2],dp[i-3]) + nums[i]
。
由于计算dp[i]数组时,我们需要用到nums[i]数组,但和那些小于i的nums数据没关系,也就是可以不保留,所以可用nums数组代替dp数组。
代码一开始不理解也正常,都是从简单到复杂,不断优化。可能我这里呈现了很简练的代码,但并不代表这是我一开始就能写出来的。
class Solution {
public int rob(int[] nums) {
int len=nums.length;
int max=0;
for(int i=0;i<len;i++){
if(i==2){
nums[2]+=nums[0];//i=2的情况
}else if(i>2){
nums[i]+= Math.max(nums[i-2],nums[i-3]);
}
if(max<nums[i]) max=nums[i];//记录最大值,包括了i=0和1的情况
}
return max;
}
}
213. 打家劫舍 II
偷钱,且不能偷相邻的,附加条件:第一家和最后一家是相邻的。
思路:动态规划
附加条件,导致动态规划偷最后一家时,不知道前面的是否偷了第一家。我们可以调用两次动态规划避免该问题,即[0, len-2]和[1,len-1],这样就保证第一家和最后一家不会同时被偷。
注意调用两次,如果把nums作为dp数组第一次会修改nums,所以不能,但可以用临时变量代替,观察dp[i]=Max(dp[i-2],dp[i-3]) + nums[i]
,每次计算dp[i],需要dp[i-2] (dp_2),dp[i-3] (dp_3),也需要dp[i-1] (dp_1)==>因为使用临时变量并没有下标,只有保存dp[i-1],才能在下次更新dp_2和dp_3,不然根本没法移动。
⚡️ 动态规划的公式不一定唯一,例如官方解答代码更简洁
class Solution {
public int rob(int[] nums) {
int len =nums.length;
return Math.max(solve(0,len-2,nums),solve(1,len-1,nums));
}
private int solve(int idx, int l, int[] nums){
int max = nums[0]; //防止只有一个 例如[1]
int dp_2=0, dp_3=0, dp_1=0; //补零,特殊值处理(这样每个数都符合dp[i]=Max(dp[i-2],dp[i-3]) + nums[i]),省去很多特殊判断。
for(int i=idx; i<=l; i++) {
int tmp = Math.max(dp_2, dp_3) + nums[i];
dp_3 = dp_2;
dp_2 = dp_1;
dp_1 = tmp;
if(max < tmp) max = tmp;
}
return max;
}
}
120. 三角形最小路径和
https://leetcode.cn/problems/triangle/
从下往上 dp[i][j] = value[i][j] + Min(dp[i][j], dp[i][j+1]) i代表行 j代表列
同理如果该题求最大路径,则只需要将Min改为Max即可
class Solution {
public int minimumTotal(List<List<Integer>> triangle) {
int size = triangle.size() - 1;
for(int i = size - 1; i >= 0; i--) {
//下一行
List<Integer> next = triangle.get(i + 1);
List<Integer> row = triangle.get(i);
int l = row.size();
for(int j = 0; j < l; j++) {
row.set(j, row.get(j) + Math.min(next.get(j), next.get(j+1)));
}
}
//最小值就在顶点
return triangle.get(0).get(0);
}
}
经典
53. 最大子数组和
求最大连续子序列和,要求序列连续
思路:动态规划
连续子序列必定是以某个元素结尾或开头,所以只要求出以每个元素为结尾的最大连续子序列和,然后记录那个最大。
⚡️以某个元素结尾或开头都行,只是数组遍历方向不同而已
用dp[i]表示以nums[i]结尾的最大连续子序列和,其中,该序列必须包含nums[i]。至于最大连续子序列和,要么是其本身,要么就是本身加上前一个的最优解dp[i-1]
。
动态方程:dp[i]=max(nums[i],dp[i-1]+nums[i])
class Solution {
public int maxSubArray(int[] nums) {
int l = nums.length;
int[] dp = new int[l];
dp[0]=nums[0];
int max=dp[0];
//从前往后遍历
for(int i=1;i<l;i++){
dp[i]=Math.max(dp[i-1]+nums[i],nums[i]);
//每个dp[i]表示以nums[i]结尾的最大连续子序列和,也就是说每个dp[i]都可作为结果
if(dp[i]>max){
max=dp[i];
}
}
return max;
}
}
优化:从动态方程中,求解dp[i],只需要依赖到上一个元素的dp值,即dp[i-1],所以可使用一个变量保存上一个元素的dp值,省去创建dp数组,且这里重新换了种思路,也就是以某个元素作为开头的。
class Solution {
public int maxSubArray(int[] nums) {
int l = nums.length - 1;
int dp = nums[l];
int max = dp;
//从后往前遍历
for(int i=l-1;i>=0;i--){
dp=Math.max(dp+nums[i],nums[i]);
if(dp>max){
max=dp;
}
}
return max;
}
}
300. 最长递增子序列
也叫最长不下降子序列(LIS),不要求序列连续,严格递增
思路:动态规划
以某个元素作为结尾,
dp[i]
表示以i为结尾的LIS,如果前面存在比该元素小的,则用该元素的LIS来更新当前的LIS,这样最后的LIS才为最优。由于序列不要求连续,所以当我们想获取前面的LIS时,就不是只比较前一个元素,而是从0到前一个元素都需要比较。也就是比较那些比当前小的元素,从而更新LIS.
动态方程:dp[i]=max{dp[i], dp[j]+1} (j<i && nums[i]>=nums[j])
测试用例:[6,7,4,5] [5,6,3,7]
class Solution {
public int lengthOfLIS(int[] nums) {
int len=nums.length;
int[] dp = new int[len];
int max = 0;
for(int i=0;i<len;i++){
//本身初始化
dp[i]=1;
for(int j=0;j<i;j++){
//在[0-i)中找比i小的元素,更新dp[i]
if (nums[i] > nums[j] && (dp[j]+1) > dp[i]){
dp[i]=dp[j]+1;
}
}
// 记录最大值
if (max < dp[i]) {
max = dp[i];
}
}
return max;
}
}
1143. 最长公共子序列
https://leetcode.cn/problems/longest-common-subsequence/
class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int l1 = text1.length();
int l2 = text2.length();
int[][] dp = new int[l1+1][l2+1];
for(int i=1;i<=l1;i++){
for(int j=1;j<=l2;j++){
if(text1.charAt(i-1)==text2.charAt(j-1)){
dp[i][j]=dp[i-1][j-1]+1;
}else{
dp[i][j]=Math.max(dp[i-1][j],dp[i][j-1]);
}
}
}
return dp[l1][l2];
}
}
类似题目:583. 两个字符串的删除操作
动态规划填表法
下面的题目基本就是利用二维数组,实现动态规划,整个过程就是在填这张二维数组表。其实在最长公共子序列哪里就有体现了。
💡下面的题目在空间复杂度上都能优化,从二维到一维(滚动数组),但暂不更,所以下面都是用二维数组实现。
NC145 01背包
有n个物品,求体积为V的背包能装物品的最大重量。
看完必会,不用看代码,代码有些繁琐
只不过文章例子求的是背包最大价值,其实都一样。
import java.util.*;
public class Solution {
/*
定义一个二维数组 dp 存储最大重量,其中 dp[i][j] 表示前 i 件物品体积为j 的情况下能达到的最大重量。
设第 i 件物品体积为v,重量为w,
根据第 i 件物品是否添加到背包中,可以分两种情况讨论:
--第 i 件物品不可添加到背包,dp[i][j] = dp[i-1][j]
--第 i 件物品可添加到背包中,max(dp[i][j] = dp[i-1][j-v] + w, dp[i-1][j])
*/
public int knapsack (int V, int n, int[][] vw) {
int[][] dp = new int[n+1][V+1];
for(int i = 1; i <= n; i++) {
//某物品体积和重量
int v = vw[i-1][0];
int w = vw[i-1][1];
for (int j = 1; j <= V; j++) {
if(j < v) {
//装不下
dp[i][j] = dp[i-1][j];
} else {
//装的下
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-v] + w);
}
}
}
return dp[n][V];
}
}
⚡️这里有个非常重要的问题需要讨论,因为这关乎到下面的题目,如果你不搞懂,下面基本就是看一道晕一道:看似一样的代码,却不能理解它的含义。
问题:一定要十分明确dp[i][j]的含义和dp边界问题(第一行和第一列)
首先含义:dp[i][j]代表从前i个元素中选择一些元素,使得它们的体积之和为j时所能获得的最大重量,根据动态方程我们知道它的值由上一行的某个数组决定,也就是dp[i-1][?],问号具体多少得看装不装的下,那问题来了,我们二维数组是从i=1,j=1开始遍历的,也就是省去了第一行和第一列,那为什么不用计算第一行和第一列呢?因为它们都是0,而数组默认初始值就是0,所以不用计算
,这个时候就有跳出骂道了,你说个pi,这不很简单的问题嘛,我只想说你先把后面的题给做出来好吧!言归正传,注意0-1背包问题刚好是边界都为0,所以我们根本就不需要考虑,但是下面的题就不一样了,因为下面的初始边界值需要你确定,那如何解决?就是根据dp数组的含义确定初始边界值,从而才能够计算后面的值,像这里第一行dp[0][j]
代表前0个元素的体积之和为j时所能获得的最大重量,哪都0个了,所以肯定都为0,同理对于dp[i][0]
,代表前i个元素的体积之和为0时所能获得的最大重量,由于物品的体积不可能为0(其实也是题目限定的),所以第一列也都为0。后面的题我也会再次强调含义和边界值问题。
416. 分割等和子集
将数组划分为两个子数组,要求两数组的元素和相等。
https://leetcode.cn/problems/partition-equal-subset-sum/
转化:找到一个子集的和为数组总和的一半(sum/2),也就是选择某些元素,使得这些元素的和为sum/2。
首先是数组类型问题,一开始你可能会把dp数组用int类型来使用,也就是
dp[i][j]表示从前i个元素中选择一些元素,使得它们的和为j时所能达到的最大值。其中,当dp[i][j]等于sum(数组)的一半时,就证明存在,这样是可以的,一开始我也这么做,但你会发现很多题解用的是boolean类型,也就是它们的类型是根据题目的求解结果确定的
,但一开始又很难看懂,这个时候就需要用到我在0-1背包强调过的问题。也就是dp数组含义和边界问题
首先dp[i][j]代表从前i个元素中选择一些元素,使得它们的和为j时能否分割为等和子集,它的值由dp[i-1][?]决定,而这个时候就需要考虑边界值问题,如果像0-1背包那样,说明第一行和第一列都为false,那dp数组最总必为false,这显然不对,所以我们需要分析边界值,从而使得后面的值能够计算。首先是第一行dp[0][j],代表前0个元素的和为j时能否分割为等和子集,其中j的值从0到sum/2,其中前0个元素的和只能是0了,这个时候也是能分割的,也就是空数组分割为两个空数组,虽然不符合题意,但边界值就是只能怎么算,对于其它dp[0][j]则为false。接着是第一列dp[i][0],代表从前i个元素中选择一些元素,使得它们的和为0时能否分割为等和子集,由于是前i个中选,说明i肯定包含0,所以dp[i][0]都为true,也就是第一列为true,这个时候就能根据动态方程继续往后计算了
class Solution {
public boolean canPartition(int[] nums) {
//计算数组总和
int sum=0;
for(int s :nums){
sum+=s;
}
//奇数直接返回
if((sum&1)==1){
return false;
}
//数组和的一半
sum/=2;
int l=nums.length;
boolean[][] dp = new boolean[l+1][sum+1];
//边界
dp[0][0] = true;
for(int i=1; i<=l; i++){
//边界
dp[i][0] = true;
//某个数
int num = nums[i-1];
for(int j=1; j<=sum; j++){
if(j >= num){
//可以加入时,由前面的dp决定,只要存在true,则当前dp肯定为true
dp[i][j] = dp[i-1][j-num] || dp[i-1][j];
}else{
//不可以加入,由上一个决定
dp[i][j] = dp[i-1][j];
}
}
}
return dp[l][sum];
}
}
494. 目标和
为数组的所有元素添加正号和负号,使得它们的和为target,求添加正负号有多少种方法
https://leetcode.cn/problems/target-sum/
法一:动态规划:数学推理转为上一题
正号的数组A,负号的数组B,A-B=target
A+A-B = target + A
2*A = target+A+B
A=(target+A+B)/2
也就是找到和为target+A+B一半的集合即可,类似上一题,只不过这道题是求方法数总数,上一道题是判断是否存在。
从这个转换中我们只需找到和为target+A+B一半的集合即可,而不用考虑减的情况。
不过这个规律可能想不到,我觉得没关系,这就跟求完全平方数一样,如果你不知道完全平方数能用加法解决,那你就只会用乘法,所以问题不大,最关键的是dp的求解。
首先明确dp数组的含义:dp[i][j]表示从前i个元素中选择一些元素,使得它们的和为j时的方法数,从而可知dp的类型为int。接着是边界问题:第一行dp[0][i]代表从前0个元素中选择一些元素,使得它们的和为j时的方法数,那就只有dp[0][0]为1了,对于第一列dp[i][0]代表从前i个元素中选择一些元素,使得它们的和为0时的方法数,前i个,说明i肯定包含0,但此时dp[i][0]就不一定是等于1了,因为nums数组中的元素可以为0,这就导致第一列的初始值不一定是0,所以第一列也需要代入动态方程中取计算,也就是j的取值要从0开始取.例如
[0,0,0,0,0,0,0,0,1] 1 很明显dp[i][0]的值肯定>=1,例如dp[1][0]==2,dp[2][0] = 4
class Solution {
public int findTargetSumWays(int[] nums, int target) {
//这里的sum代表A
int sum = target;
for(int num : nums) {
sum+=num;
}
//奇数或者小于0直接返回
if((sum & 1) == 1 || sum < 0) {
return 0;
}
//一半
sum/=2;
int len = nums.length;
int[][] dp = new int[len+1][sum+1];
//边界
dp[0][0] = 1;
for(int i=1; i<=len; i++) {
int num = nums[i-1];
//注意j从0开始
for(int j=0; j<=sum; j++) {
if(j >= num) {
//可以加入时,由前面的dp方法数决定,注意是加法,而不是二选一
dp[i][j] = dp[i-1][j-num] + dp[i-1][j];
} else {
//不可以加入时
dp[i][j] = dp[i-1][j];
}
}
}
return dp[len][sum];
}
}
法二:dfs 贼容易理解,但慢
class Solution {
int count=0;
public int findTargetSumWays(int[] nums, int S) {
dfs(0,0,nums,S);
return count;
}
public void dfs(int sum, int i, int[] arr, int s){
if(i==arr.length){
if(sum==s){
++count;
}
return;
}
dfs(sum+arr[i],i+1,arr,s);
dfs(sum-arr[i],i+1,arr,s);
}
}
474. 一和零
也属于0-1背包的题,只不过这里维度增加。理解为两个背包
https://leetcode.cn/problems/ones-and-zeroes/
法一:动态规划,基于三维数组,但原则不变,dp含义+边界值
含义,dp[i][j][k]
表示从前i个字符串中选择一些字符串,使得包含0和1的个数分别为j和k时,这些串所能构成的最大子集长度。
边界,三维考虑的边界就是xyz三个轴中出现0时的情况
dp[0][j][k]
表示从前0个字符串中选择一些字符串,使得包含0和1的个数分别为j和k
其中包含1和0的个数只能是0,所以dp[0][j][k]均为0
dp[i][0][k]
表示从前i个字符串中选择一些字符串,使得包含0和1的个数分别为0和k
这个是不确定的,因为每个串中可以只包含1,例如[“1”,“11”]这样的子集就不包含0,所以dp值不一定为0,需要代入动态规划中计算。
dp[i][j][0]
同上,需要代入计算,其实就是j和k的取值从0开始
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//创建dp数组
int len = strs.length;
int[][][] dp = new int[len + 1][m + 1][n + 1];
for(int i = 1; i <= len; i++) {
//计算当前串中0和1的个数
int t0 = 0;
for(char c : strs[i-1].toCharArray()) {
if(c == '0') {
t0++;
}
}
int t1 = strs[i-1].length() - t0;
//动态规划
for(int j = 0; j <= m; j++) {
for(int k = 0; k <= n; k++) {
if(j >= t0 && k >=t1) {
//当两者都满足时,该字符串才可能加入,如果假如子集长度+1
dp[i][j][k] = Math.max(dp[i-1][j][k], dp[i-1][j-t0][k-t1] + 1);
} else {
//当两者都不满足时
dp[i][j][k] = dp[i-1][j][k];
}
}
}
}
return dp[len][m][n];
}
}
💡dfs超时
NC309 完全背包
思路:类似0-1背包,只是动态方程稍有不同,因为完全背包同一个物品可以装多次,
dp[i][j]
,很明显边界都为0,这里就不分析了
提升
比较考验数学和逻辑分析能力
152. 乘积最大子数组
暴力
class Solution {
public int maxProduct(int[] nums) {
int max = nums[0];
int l = nums.length;
for(int i = 1; i < l; i++) {
int cur = nums[i];
int dp = cur;
//往前乘,并记下最大值
for(int j = i - 1; j >= 0; j--) {
cur = cur * nums[j];
if(dp < cur) dp = cur;
}
if(max < dp) max = dp;
}
return max;
}
}
动态规划,如果你一开始想不到,可以先尝试暴力,往前乘,然后你会发现这种方式其实就是下面的原始方式
只不过下面有记录前面算过的最大或最小值,而当我们没记录时,当然只能靠暴力
dp[i]
表示以第i个元素作为结尾的最大乘积子数组
优化:dp[i] = Max(dp[i-1]) * nums[i] or Min(dp[i-1]) * nums[i]
因为nums[i]可能为正,也可能为负数,所以不一定等于Max(dp[i-1]) * nums[i]
他应该由前一个作为结尾的最小乘积或者最大乘积决定
所以我们还需要一个数组用于记录以第i个元素作为结尾的最小乘积子数组
class Solution {
public int maxProduct(int[] nums) {
int max = nums[0];
int l = nums.length;
int[] dpMax = new int[l];
int[] dpMin = new int[l];
dpMax[0] = nums[0];
dpMin[0] = nums[0];
for(int i = 1; i < l; i++) {
int a = nums[i] * dpMax[i-1];
int b = nums[i] * dpMin[i-1];
if(a > b) {
dpMax[i] = Math.max(a, nums[i]);
dpMin[i] = Math.min(b, nums[i]);
} else {
dpMax[i] = Math.max(b, nums[i]);
dpMin[i] = Math.min(a, nums[i]);
}
if(dpMax[i] > max) {
max = dpMax[i];
}
}
return max;
}
}
343. 整数拆分
将一个整数拆分为乘积最大的一些数字
思路:动态规划
2=1×1 ——max=1
3=1×2 ——max=2
4=1×3 或 2×2 ——max=4
5=1×4 或 2×3 ——max=6
6=1×5 或 2×4 或 3×3 ——max=9 ==>双指针即可遍历每个数的不同拆分。
当然,这里只是拆分为两个数,所以还需要动态规划来帮我们获取更细的拆分。用dp[i]表示整数 i 所能拆分后所能获得的最大乘积。例如6=1×5,如果dp[5]大于5,则说明5需要拆分,用dp[5]替换5(dp[1]同理)。其中,计算dp[6]时,dp[5]肯定是前面先算好了。且dp[1]=1,dp[2]=1。动态方程为dp[i] =Math.max(l, dp[l]) * Math.max(r, dp[r])
i = l + r(拆分)
class Solution {
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[1] = 1; //初始化
dp[2] = 1;
for (int i = 3; i <= n; i++) {
int l = 1, r = i-l; //双指针
while (l <= r) {
//判断是否需要拆分
int tmp = Math.max(l, dp[l]) * Math.max(r, dp[r]);
//更新最大值
if (tmp > dp[i]) dp[i] = tmp;
l++;
r--;
}
}
return dp[n];
}
}
*279. 完全平方数
用最少数量的完全平方数构造整数n
💡沿用上一题思路,但是很慢
思路:动态规划,(BFS会更好理解)
用dp[i]表示和为i的完全平方数的最少数量,例如5的最少数量是2,拆为4+1,即dp[5] = 2
dp[0] = 0 dp[1] = dp[1-0] + 1
dp[2] = dp[2-1] + 1 数量加一代表减去的那个平方数
dp[3] = dp[3-1] + 1
dp[4] = dp[4-1] + 1 或者 dp[4-4] + 1
dp[5] = dp[5-1] + 1 或者 dp[5-4] + 1
dp[i] = dp[i-pft] + 1 pft是小于等于i的完全平方数,从1开始算 1*1 2*2 3*3
可得动态方程:dp[i] = Math.min(dp[i], dp[i-pft]+1)
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
for (int i = 1; i <= n; i++) {
dp[i] = i; //初始化最坏的情况,全是1
for (int j = 1; j * j <= i; j++) {//1*1 2*2 3*3 ...
dp[i] = Math.min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
}
322. 零钱兑换
https://leetcode.cn/problems/coin-change/
类似完全平方数,也可以用bfs
dp[i]表示金额为i所需硬币的最少数量
需要注意的是初始化,虽然不存在应该赋值为-1,但根据动态方程,我们求的是最少数量,所以应该给他赋值一个正无穷的数,更简单一点就是超过总金额即可(big)。最后再根据dp结果是不是等于big,如果是说明不存在,否则返回
class Solution {
public int coinChange(int[] coins, int amount) {
//dp[i]表示金额为i所需硬币的最少数量
int len = coins.length;
int big = amount + 1;
int[] dp = new int[big];
dp[0] = 0;
for(int i = 1; i <= amount; i++) {
dp[i] = big; //初始化,最坏的情况就是不存在,但不能用-1,应该用一个大于amount的数
for(int j = 0; j < len; j++ ) {
int coin = coins[j];
if(coin <= i) {
dp[i] = Math.min(dp[i], dp[i-coin] + 1);
}
}
}
return dp[amount] == big ? -1 : dp[amount];
}
}
*518. 零钱兑换 II
https://leetcode.cn/problems/coin-change-2/
1.dfs 类似组合总和,只是数据的范围变大,速度下降
组合总和题目限定:对于给定的输入,保证和为 target 的不同组合数少于 150 个。
所以本道题 dfs 超时
500
[3,5,7,8,9,10,11]
改进:先排序 再剪枝,结果还是超时
2.改用动态规划
dp[i]表示金额为i的硬币组合数
如何保证组合不重复:123和321
交换coins和amount的遍历顺序即可,也就是先遍历coins再遍历amount,具体可参看官方题解
class Solution {
public int change(int amount, int[] coins) {
int[] dp = new int[amount+1];
dp[0] = 1;
for(int coin : coins) {
for(int i = 1; i <=amount; i++) {
if(i >= coin) {
dp[i] = dp[i] + dp[i-coin];
}
}
}
return dp[amount];
}
}
* 377. 组合总和 Ⅳ
https://leetcode.cn/problems/combination-sum-iv/
思路:动态规划
dp[i]表示的是和为i的组合数
0 1
1 dp[0] 1
2 dp[0] + dp[1] 2
3 dp[0] + dp[1] + dp[2] 4
4 dp[1] + dp[2] + dp[3] 7
类似上一题零钱兑换II,但题目要求组合没有顺序,也就是1 1 2和2 1 1是不一样的
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1; //初始化
for(int i = 1; i <= target; i++) {
for(int num: nums) {
if(i >= num) {
dp[i]+=dp[i-num];
}
}
}
return dp[target];
}
}
*139. 单词拆分
求解过程类似题目300. 最长递增子序列
https://leetcode.cn/problems/word-break/solution/dan-ci-chai-fen-by-leetcode-solution/
参见官方
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
//dp[i]表示s中前i个字符能否分割为字典中的单词
int l = s.length();
boolean[] dp = new boolean[l + 1];
//初始化
dp[0] = true;
for(int i = 1; i <= l; i++) {
//遍历[0,i)里面的字串,也就是[0,i) [1,i) [2,i) [3,i)...[i-1,i)
for(int j = 0; j < i; j++) {
//如果dp[j]为true,说明前j个字符能分割,此时只需判断字典中是否存在j到i子串
if(dp[j] && wordDict.contains(s.substring(j , i))) {
dp[i] = true;
//找到就可以跳出了
break;
}
}
}
return dp[l];
}
}
650. 只有两个键的键盘
https://leetcode.cn/problems/2-keys-keyboard/
其实你可以大致分析一下,就能找到规律,关键在于n这个数能否找到整除的数(除了本身),如果不行它只能是一个c,然后不断ppppp…结果是n个操作,例如3和7
如果行,则能由整除的数来减少cp次数。
2
c p
3
c p p
4
c p c p dp[2] + dp[2]
5
c p p p p
6
c p p c p dp[3] + dp[2]
7
c p p p p p p
8
c p c p c p dp[4] + dp[2]
9
c p p c p p dp[3] + d[3]
10
c p p p p c p dp[5] + [2]
动态方程
对于偶数,无需判断,有多个方式也无所谓,答案唯一,例如 30 = 15 * 2 或 = 5 * 6 ,操作次数是一样。
==dp[i] = dp[i/2] + dp[2]
对于奇数
==如果[2,i/2]不存在被i整除的数dp[i]=i
,
==否则存在tmp被i整除dp[i] = dp[tmp] + dp[i/tmp]
class Solution {
public int minSteps(int n) {
int[] dp = new int[n+1];
dp[1] = 0;
if(n >= 2) dp[2] = 2;
for(int i = 3; i <= n; i++) {
if((i & 1) == 1) {
// 奇数先赋值最差情况,也就是不能整除[2,i/2],需要操作次数为i,例如3,7
dp[i] = i;
// 然后在判断能否被整除
int j = i / 2;
int tmp = 2;
while(tmp <= j) {
if((i % tmp) == 0) {
dp[i] = dp[tmp] + dp[i/tmp];
break;
}
tmp++;
}
} else {
//偶数
int tmp = i / 2;
dp[i] = dp[tmp] + dp[2];
}
}
return dp[n];
}
}
待整理
376. 摆动序列
求最长子序列的长度,要求子序列为摆动序列,也就是一大一小的序列。不要求子序列连续。
https://leetcode.cn/problems/wiggle-subsequence/description/
class Solution {
public int wiggleMaxLength(int[] nums) {
int len = nums.length - 1;
//记录结果
int ans = 1;
//控制递增递减方向,刚开始为0,表示没有要求
int f = 0;
for (int i = 0; i < len; i++) {
//递增,同时要求上一个为递减
if(nums[i] < nums[i+1] && f != 1 ) {
ans++;
f = 1;
}
//递减,同时要求上一个为递增
if(nums[i] > nums[i+1] && f != -1) {
ans++;
f = -1;
}
}
return ans;
}
}
5. 最长回文子串
思路一:字符串中心扩展(双指针)
思路二:动态规划
外扩的方式实现,判断dp[i][j]是不是时,通过判断dp[i+1][j-1]即可
class Solution {
public String longestPalindrome(String s) {
int l = s.length();
int[][] dp = new int[l][l];
for (int i = 0; i < l; i++) { //长度为1
dp[i][i] = 1;
}
int start = 0, end = 0; //记录最长字串下标——最后一次的记录必定最长(思考)
for (int len = 2; len <= l; len++) { //遍历长度为2、长度为3、长……
int k = l - len;
for (int i = 0; i <= k; i++) {
int j = i + len - 1; //串尾
if (s.charAt(i) == s.charAt(j)) {
if (len == 2 || dp[i + 1][j - 1] == 1) { //长度为2的需要特殊处理
dp[i][j] = 1;
start = i;
end = j;
}
}
}
}
return s.substring(start, end + 1);
}
}
*646. 最长数对链
https://leetcode.cn/problems/maximum-length-of-pair-chain/
思路一:贪心
思路二:参考最长递增子序列
class Solution {
public int findLongestChain(int[][] pairs) {
Arrays.sort(pairs,new Comparator<int[]>(){
@Override
public int compare(int a[], int b[]){
return a[0]-b[0];
}
});
int len = pairs.length;
int dp[] = new int[len];
int ans=-1;
for(int i=0;i<len;i++){
dp[i]=1;
for(int j=0;j<i;j++){
if(pairs[i][0]>pairs[j][1]){
if(dp[j]+1>dp[i]){
dp[i]=dp[j]+1;
}
}
}
if(ans<dp[i]){
ans=dp[i];
}
}
return ans;
}
}