前言
动态规划也是重点啊,所以多找点题目写写,基本都是啥卡的
有的太简单的弱智题目就不放这了哈
63. 不同路径 II - 力扣(LeetCode)
有障碍了
区别就是在递推公式要加上判断条件,以及如果障碍在第一行或者第一列的时候,初始化的问题
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
//如果在起点或终点出现了障碍,直接返回0
if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) {
return 0;
}
for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) {
dp[i][0] = 1;
}
for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) {
dp[0][j] = 1;
}
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = (obstacleGrid[i][j] == 0) ? dp[i - 1][j] + dp[i][j - 1] : 0;
}
}
return dp[m - 1][n - 1];
}
}
96. 不同的二叉搜索树 - 力扣(LeetCode)
一开始尝试找规律没找到
思路
背包问题
01背包问题dp[i][j]
是什么:0-i的物品放大哦容量为j的背包的最大价值
二维数组:**dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);**
换成一维就是: dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
一维01背包必须先遍历物品
然后遍历背包时是倒序便利
套路:
//先遍历物品,再倒序遍历背包
for(int i=0;i<weight.length;i++)
{
for(int j=sumweight;j>=weight[i];j--)
{
dp[j]=Math.max(dp[j], dp[j-weight[i]]+value[i]);
}
}
有时候weight和value是同一个数组
而完全背包问题,如果是纯完全背包问题,无所谓遍历顺序
如果是装满背包有多少种方法,要分是排列数还是组合数:组合数:先物品,排列数:先背包
完全背包问题套路:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量,因为是无限取,所以可以正序遍历
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//先背包,再物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 0; i < weight.size(); i++) { // 遍历物品
if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
416. 分割等和子集 - 力扣(LeetCode)
就是给你物品列表,价值就是具体的数,然后容量也没确定,反正就是看你能不能使价值等于所有数加起来的/2
0-1背包问题,因为每个数只能用一次
卡尔的思路是重量就等于价值,这样的好处是什么,就是背包的容量是可以被确定的
class Solution {
public boolean canPartition(int[] nums) {
int values=0;
for(int i=0;i<nums.length;i++)
{
values+=nums[i];
}
if(values%2!=0)return false;
values/=2;//目标价值
int []dp=new int[values+1];//背包最大就是和价值相同
for(int i=0;i<nums.length;i++)
{
for(int j=values;j>=nums[i];j--)
{
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[values]==values;
}
}
1049. 最后一块石头的重量 II - 力扣(LeetCode)
这道题目等价于什么:把数组分为两部分,让两部分的差尽可能相等或者小,那么不就和上一题一样么
public int lastStoneWeightII(int[] stones) {
int sum=0;
for(int i:stones)
{
sum+=i;
}
int target=sum/2;
int[]dp=new int[1501];
for(int i=0;i<stones.length;i++)
{
for(int j=target;j>=stones[i];j--)
{
dp[j]=Math.max(dp[j], dp[j-stones[i]]+stones[i]);
}
}
return sum-2*dp[target];
}
494. 目标和 - 力扣(LeetCode)
思路:转化为背包问题,分为+集合和-集合,最后结果就是sum=+集合加上-集合
target=+集合减去-集合
然后得出,+集合要等于(tartget+sum)/2
这种装满背包有多少种方法的递推公式:dp[j]+=dp[j-numbers[i]],dp[j]为装满容量为j的背包有多少种方法
public int findTargetSumWays(int[] nums, int target) {
int sum=0;
for(int num:nums)
{
sum+=num;
}
if((sum+target)%2!=0)return 0;
if(target<0&&sum<-target)return 0;
int left=(sum+target)/2;
int[]dp=new int[left+1];
dp[0]=1;//根据题目推导的
for(int i=0;i<nums.length;i++)
{
for(int j=left;j>=nums[i];j--)
{
dp[j]+=dp[j-nums[i]];
}
}
return dp[left];
}
518. 零钱兑换 II - 力扣(LeetCode)
这就到了完全背包问题
有了目标和的基础,做这个不难
//这里面coins就是物品,然后价值和重量也是相等的,就是coins的数值,然后总容量就是amount
//dp[j]:容量为j有多少种凑法
//地推:dp[j]+=dp[j-weight[i]]
public int change(int amount, int[] coins) {
int []dp=new int[amount+1];
dp[0]=1;//当容量为0只有一种,那就是什么也不装
for(int i=0;i<coins.length;i++)
{
for(int j=coins[i];j<=amount;j++)
{
dp[j]+=dp[j-coins[i]];
}
}
return dp[amount];
}
322. 零钱兑换 - 力扣(LeetCode)
零钱兑换2是问有多少种凑法,这个是问题最少硬币个数
硬币是无线的,那么也是一个完全背包问题
看我一开始写的:
//dp[j]为容量为j的背包,装满他的最少硬币个数
//这题目也是一样,重量等于价值,就是coins【i】
//递推公式:dp[j],dp[j-coins[i]]推出,但是还得保证dp[j-conis[i]]的可以凑成的
//初始化:dp[0]=0;
public int coinChange(int[] coins, int amount) {
int[]dp=new int[amount+1];
for(int i=0;i<coins.length;i++)
{
for(int j=coins[i];j<=amount;j++)
{
if(dp[j-coins[i]]!=0)
{
dp[j]=dp[j-coins[i]]+1;
}
}
}
return dp[amount];
}
我漏了哪些思考:应该取最小的dp[j],因为双循环你的dp[j]肯定会比较不止一次啊
所以修改:
//dp[j]为容量为j的背包,装满他的最少硬币个数
//这题目也是一样,重量等于价值,就是coins【i】
//递推公式:dp[j],dp[j-coins[i]]推出,但是还得保证dp[j-conis[i]]的可以凑成的
//初始化:dp[0]=0;
public int coinChange(int[] coins, int amount) {
int[]dp=new int[amount+1];
for(int i=0;i<dp.length;i++)
{
dp[i]=Integer.MAX_VALUE;//初始化成最大值,防止被覆盖
}
dp[0]=0;
for(int i=0;i<coins.length;i++)
{
for(int j=coins[i];j<=amount;j++)
{
if(dp[j-coins[i]]!=Integer.MAX_VALUE)
{
dp[j]=Math.min(dp[j-coins[i]]+1,dp[j]);
}
}
}
return dp[amount]==Integer.MAX_VALUE?-1:dp[amount];//还等于最大值说明凑不出来
}
279. 完全平方数 - 力扣(LeetCode)
这个一眼完全背包问题啊
public int numSquares(int n) {
int max=Integer.MAX_VALUE;
int[]dp=new int[n+1];
Arrays.fill(dp, max);
dp[0]=0;
for(int i=1;i*i<=n;i++)
{
for(int j=i*i;j<=n;j++)
{
dp[j]=Math.min(dp[j], dp[j-i*i]+1);//一定能凑成,所以不需要判断了
}
}
return dp[n];
}
139. 单词拆分 - 力扣(LeetCode)
可以重复使用,一眼完全背包啊吗,因为这里其实是排列数,就是你的字符得有顺序,所以先遍历背包
//也是背包问题背包就就是给的String s,
//物品列表就是wordDICT
//然后可以重复使用,说明是完全背包问题
//但是单词有顺序,是排列数,所以是先遍历背包
public boolean wordBreak(String s, List<String> wordDict) {
boolean []dp=new boolean[s.length()+1];//dp[i]:长度为i能否背凑成
dp[0]=true;
for(int i=0;i<=s.length();i++)
{
for(int j =0;j<i&&!dp[i];j++)//遍历物品,注意,这里是遍历字符串并且判断他是不是物品
{
if(dp[j]&&wordDict.contains(s.substring(j,i)))
{
dp[i]=true;
}
}
}
return dp[s.length()];
}
打家劫舍和股票问题
198. 打家劫舍 - 力扣(LeetCode)
//每个房间都有两个选择:偷和不偷
//dp[i]:走到这个房间能偷到的最高金额
//偷:dp[i]=dp[i-2]+nums[i]
//不偷:dp[i]=dp[i-1]
public int rob(int[] nums) {
int []dp=new int[nums.length];
dp[0]=nums[0];
if(nums.length==1)return dp[0];
dp[1]=Math.max(nums[0], nums[1]);
for(int i=2;i<nums.length;i++)
{
dp[i]=Math.max(dp[i-1],dp[i-2]+nums[i]);
}
int res=Integer.MIN_VALUE;
return dp[nums.length-1];
}
213. 打家劫舍 II - 力扣(LeetCode)
比上一题多了一个条件,首尾相邻
其实就是把打家劫舍抽象了
class Solution {
//区别就是和上一题相比,这一题最后一个和第一个是相邻的,那么我们直接分两种情况,第一件偷和第一间不偷
public int rob(int[] nums) {
if(nums.length==0)return 0;
if(nums.length==1)return nums[0];
int v1=solve(nums, 0, nums.length-2);//偷第一件,那最后一件不能投
int v2=solve(nums, 1, nums.length-1);//不偷异地检
return v1>v2?v1:v2;
}
public int solve(int[] nums,int l,int r) {
if(l==r)return nums[l];
int []dp=new int[nums.length];
dp[l]=nums[l];
dp[l+1]=Math.max(nums[l], nums[l+1]);
for(int i=l+2;i<=r;i++)
{
dp[i]=Math.max(dp[i-2]+nums[i],dp[i-1]);
}
return dp[r];
}
}
121. 买卖股票的最佳时机 - 力扣(LeetCode)
有一说一这题是算在贪心算法里
//维护当前的最小值,和当前的最大利润,计算本次利润和最大利润比较即可
public int maxProfit(int[] prices) {
int []res=new int[prices.length];
int minvalue=Integer.MAX_VALUE;
int maxprofit=Integer.MIN_VALUE;
for(int i=0;i<prices.length;i++)
{
res[i]=prices[i]-minvalue>0?prices[i]-minvalue:0;
minvalue=Math.min(prices[i], minvalue);
maxprofit=Math.max(res[i], maxprofit);
}
return maxprofit;
}
但是为了后面的不能用贪心算法解的题目呢,还是得会动态规划怎么写
那么这就涉及到二维dp了dp[i][0]表示第i天持有股票,dp[i][1]表示第i天不持有股票
public int maxProfit(int[] prices) {
int[][]dp=new int[prices.length][2];
dp[0][0]=-prices[0];//第一天持有,那么直接减去数值
dp[0][1]=0;
for(int i=1;i<prices.length;i++)
{
dp[i][0]=Math.max(dp[i-1][0], -prices[i]);//持有,那么要么是今天买,要么是之前买的
dp[i][1]=Math.max(dp[i-1][0]+prices[i], dp[i-1][1]);//不持有,要么是卖了,要么是没买
}
return dp[prices.length-1][1];//你最后肯定得卖掉
}
122. 买卖股票的最佳时机 II - 力扣(LeetCode)
比上一题多了的部分就是可以多次购买出售了
但是这题用动态规划理解太复杂了,我们直接上贪心算法:只要今天股价比昨天高,就交易
public int maxProfit(int[] prices) {
int len=prices.length;
if(len<2)
return 0;
int res=0;
for(int i=1;i<len;i++)
{
int diff=prices[i]-prices[i-1];
if(diff>0)
{
res+=diff;
}
}
return res;
}
因为你不限制交易次数啊,所以我们可以把所有的利润空间全部拿下
123. 买卖股票的最佳时机 III - 力扣(LeetCode)
这题你就不能用贪心了,只能老老实实动态规划了,草!!!!
多维动态规划的关键在于:拆分状态
这里就能拆成四个状态:
第一次持有,第一次不持有,第二次持有,第二次不持有
分别对应dp[i][1][2][3][4]
那么递推公式呢
dp[i][1]第一次持有,所以要么是今天买了,-prices[i],要么是之前买过了dp[i-1][1]
dp[i][2]第一次不持有,要么是今天卖了,dp[i-1][1]+price[i],要么是啥也没干,dp[i-1][2]
dp[i][3]第二次持有,要么是今天买了dp[i-1][2]-prices[i],要么啥也没干dp[i-1][3]
dp[i][4]第二次不持有,要么是今天卖了dp[i-1][3]+prives[i],要么啥也没干,dp[i-1][4]
总结之后易得:
public int maxProfit(int[] prices) {
int len=prices.length;
int[][]dp=new int[len][5];
dp[0][1]=-prices[0];
dp[0][3]=-prices[0];//初始化
for(int i=1;i<prices.length;i++)
{
dp[i][1]=Math.max(dp[i-1][1],-prices[i]);
dp[i][2]=Math.max(dp[i-1][1]+prices[i],dp[i-1][2]);
dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-prices[i]);
dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+prices[i]);
}
return dp[len-1][4];
}
188. 买卖股票的最佳时机 IV - 力扣(LeetCode)
至多k次交易,其实根据上一题找规律就行了
买卖两次我们定义是4个状态,5的数组,那么k次就是2k个状态,2k+1大的数组呗
public int maxProfit(int k, int[] prices) {
if(prices.length==0)return 0;
int len=prices.length;
int[][]dp=new int[len][2*k+1];
for(int i=1;i<k*2;i+=2)
{
dp[0][i]=-prices[0];//初始化,奇数(第i次持有)全部初始化成-prices[0]
}
for(int i=1;i<len;i++)//找规律一个一个填吧
{
for(int j=0;j<k*2-1;j+=2)//对每一天,去计算k个状态,j始终为偶数
{
dp[i][j+1]=Math.max(dp[i-1][j+1],dp[i-1][j]-prices[i]);//j+1为奇数,那就是持有
dp[i][j+2]=Math.max(dp[i-1][j+2],dp[i-1][j+1]+prices[i]);//不持有
}
}
return dp[len-1][k*2];
}
序列问题
300. 最长递增子序列 - 力扣(LeetCode)
class Solution {
//重点是什么,你要和你前面的所有子序列比
public int lengthOfLIS(int[] nums) {
int []dp=new int[nums.length];
Arrays.fill(dp, 1);
for(int i=1;i<nums.length;i++)
{
for(int j=0;j<i;j++)
{
if(nums[i]>nums[j])
{
dp[i]=Math.max(dp[j]+1,dp[i]);
}
}
}
int res=0;
for(int i=0;i<nums.length;i++)
{
res=Math.max(res, dp[i]);
}
return res;
}
}
674. 最长连续递增序列 - 力扣(LeetCode)
//要连续了,那就是只用和前面一个比就行了
public int findLengthOfLCIS(int[] nums) {
int[]dp=new int[nums.length];
Arrays.fill(dp, 1);
for(int i=1;i<nums.length;i++)
{
if(nums[i]>nums[i-1])
{
dp[i]=dp[i-1]+1;
}else
{
dp[i]=1;
}
}
int res=0;
for(int i=0;i<dp.length;i++)
{
res=Math.max(res, dp[i]);
}
return res;
}
718. 最长重复子数组 - 力扣(LeetCode)
//dp[i][j]表示i-1,j-1为结尾的,两个的最长重复子数组
//地推公式:如果相等,那么等于dp[i-1][j-1]+1,因为是子数组,所以无需其他情况
public int findLength(int[] nums1, int[] nums2) {
int [][]dp=new int[nums1.length+1][nums2.length+1];
int res=0;
for(int i=1;i<=nums1.length;i++)
{
for(int j=1;j<=nums2.length;j++)
{
if(nums1[i-1]==nums2[j-1])
{
dp[i][j]=dp[i-1][j-1]+1;
}
res=Math.max(res,dp[i][j]);
}
}
return res;
}
1143. 最长公共子序列 - 力扣(LeetCode)
你看是不是就是上一题把子数组换成子序列
这种问题统统表示为i-1.j-1为结尾的,这样就不用初始化了
public int longestCommonSubsequence(String text1, String text2) {
int len1=text1.length();
int len2=text2.length();
int [][]dp=new int[len1+1][len2+1];
for(int i=1;i<=len1;i++)
{
for(int j=1;j<=len2;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[len1][len2];
}
53. 最大子数组和 - 力扣(LeetCode)
//子数组,那就是连续的序列
//
public int maxSubArray(int[] nums) {
int[]dp=new int[nums.length];
dp[0]=nums[0];
if(nums.length==1)
{
return dp[0];
}
int res=dp[0];
for(int i=1;i<nums.length;i++)
{
dp[i]=Math.max(dp[i-1]+nums[i], nums[i]);
res=Math.max(res, dp[i]);
}
return res;
}
72. 编辑距离 - 力扣(LeetCode)
动态规划核心:分割状态
我们的思路是只对一个进行删除修改操作,不做添加,因为对一个加相当于对另一个减
//dp[i][j]为i-1,j-1转换的最少操作次数
//那我们就是删除修改
//删除:可以对1也可以对2 ,修改对谁都是一样的
//所以 是三种状态
//删除1:那么就变成了dp[i-1][j]+1,删除2:dp[i][j-1]+1,修改:dp[i-1][j-1]+1
//那就行了,一股闹比较谁最小就行了
public int minDistance(String word1, String word2) {
int m=word1.length();
int n=word2.length();
int[] []dp=new int[m+1][n+1];
for(int i=1;i<=m;i++)
{
dp[i][0]=i;
}
for(int j=1;j<=n;j++)
{
dp[0][j]=j;
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
if(word1.charAt(i-1)==word2.charAt(j-1))
{
dp[i][j]=dp[i-1][j-1];//相等就和之前一样啊
}else
{
dp[i][j]=Math.min(dp[i][j-1],Math.min(dp[i-1][j], dp[i-1][j-1]))+1;
}
}
}
return dp[m][n];
}
有一说一现在写已经很熟了,进步肉眼可见