动态规划
DP用法:
1.建立动态方程
2.确定初始状态
Easy
- 例题1
数组的每个索引作为一个阶梯,第 i个阶梯对应着一个非负数的体力花费值 cost[i](索引从0开始)。
每当你爬上一个阶梯你都要花费对应的体力花费值,然后你可以选择继续爬一个阶梯或者爬两个阶梯。
您需要找到达到楼层顶部的最低花费。在开始时,你可以选择从索引为 0 或 1 的元素作为初始阶梯。
输入: cost = [10, 15, 20]
输出: 15
解释: 最低花费是从cost[1]开始,然后走两步即可到阶梯顶,一共花费15。
输入: cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出: 6
解释: 最低花费方式是从cost[0]开始,逐个经过那些1,跳过cost[3],一共花费6。
为什么要使用动态规划:
答:要到达楼层顶部即dp[n]得值最小,建立在使得dp[n-1],dp[n-2]最小的基础上,各自试探1步或者两步,且要保证这两层的代价也是最小,由此符合dp记录所有状态的规则。
由此开始:—>
1.确定动态方程dp[i]=min{dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]}
2.初始状态:dp[0]=0,dp[1]=0 注意这个初始状态的设定很有趣,初始要登上的阶梯没有踩着其他的阶梯
此题可以优化空间复杂度
代码如下:
public class Solution {
/*
要统计到楼顶的状态,也即最后一个状态的最小花费,可以到最后一个,或两个的最小花费+当前花费
由此想到动态规划
动态方程dp[i]=min{dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]}
初始状态:dp[0]=0,dp[1]=0 注意这个初始状态的设定很有趣,初始要登上的阶梯没有踩着其他的阶梯
此题可以优化空间复杂度
*/
public int minCostClimblingstaits(int[] cost){
int[] dp=new int[cost.length];
dp[0]=0;
dp[1]=0;
for (int i=2;i<cost.length;i++){
dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
}
return Math.min(dp[cost.length-2]+cost[cost.length-2],dp[cost.length-1]+cost[cost.length-1]);
}
public static void main(String[] args) {
Solution solution = new Solution();
int[] cost = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1};
int result = solution.minCostClimblingstaits(cost);
System.out.println(result);
}
}
MID
- 例题
最长回文字符串
问题分析:是否是动规划题?是,当前字串存在中状态回文/非回文,且依赖于字串是回文串/非回文串
动态方程设立:
回文态:dp[i][j]=dp[i+1][j-1]&&s[i]==s[j]
非回文态dp[i][j]=dp[i+1][j-1] &&s[i]!=s[j];
初始状态:
单字符:dp[i][i]为回文态
双字符:s[i]=s[j] j-i<3
package 回文字符串;
//给你一个字符串s,找到s中最长的回文子串
//此题寻找最大回文串,回文串最小为2,最大为length,从此处确认左右边界,每次左边从头开始,记录最长的左和长度。
public class Solution {
public String longestPalindrome(String s){
//1.建立动态方程
// dp[i][j]=True:dp[i+1][j-1] && s[i]==s[j]
//dp[i][j]=false:others
//2.确定初始状态
//dp[i][i]=True
// aba为回文串 aa为回文串,ab不为回文串 也就是 s[i]!=s[j] dp[i][j]=false
//当前字串一共有两个状态1.是回文串,2.不是回文串
//初始状态
int length = s.length();
boolean[][] dp = new boolean[length+1][length+1];
for (int i=0;i<length+1;i++){
dp[i][i]=true;
}
int maxLen=1;
int begin=0;
char[] chars = s.toCharArray();
//递推
//枚举字串长度
for (int L=2;L<=length;L++){
//枚举左边界
for (int i =0;i<length;i++){
//由L和i可以确定右边界,即j=L+i-1
int j = L+i-1;
//如果有边界越界,就可以退出当前循环
if (j>=length){
break;
}
if (chars[i]!=chars[j]){
dp[i][j]=false;
}else{
if (j-i<3){
dp[i][j]=true;
}else{
dp[i][j]=dp[i+1][j-1];
}
}
if (dp[i][j]&&(j-i+1)>maxLen){
begin=i;
maxLen=j-i+1;
}
}
}
return s.substring(begin,begin+maxLen);
}
public static void main(String[] args) {
String s="cbbd";
Solution solution = new Solution();
String s1 = solution.longestPalindrome(s);
System.out.println(s1);
}
}
Hard
- 例题2
给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
输入:k = 2, prices = [2,4,1]
输出:2
解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2 。
输入:k = 2, prices = [3,2,6,5,0,3]
输出:7
解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4 。
随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。
第一眼自然能看出是dp,毕竟和上题差不多
1.首先确定两个状态:当天手里是否持有股票,其中dp[i][0],第i天不持有股票,dp[i][1],第i天持有股票
2.确定状态转移方程
dp[i][0]=max{dp[i-1][1]+prices[i],dp[i-1][0]}当天没有股票的状态来自昨天有今天卖/昨天没有今天也没买
dp[i][1]=max{dp[i-1][1],dp[i-1][0]-prince[1]}当天有股票的状态来自昨天没有今天买/昨天有今天没买
3.确定初始状态:dp[0][0]=0;dp[0][1]=-price[0]
上述是错误的,因为没有看清题干:1.有交易笔数要求2.要求再次购买前需要出售掉之前的股票
什么叫做1笔交易,卖出是完成一笔交易,买入时增加一笔交易
1.第i天一共有两种状态:手里持有一只股票/手里不持有股票
2.确定状态转移方程
手里持有一支股票,此时为第j笔交易:(1).来自当天购买此时进行第j笔交易,利润为前天不持有股票第j-1笔交易的利润,(2).来自前天遗留,前天的交易为第j笔
state1[i][j]=Math.max(state2[j-1]-price[i],state1[i-1][j])
手里不持有股票,此时为第j笔交易:(1).当前不购买股票,上一笔交易完成j-1,(2).当天售卖股票获得price[i]利润,售卖的为手里有一只股票且还处于第j笔交易的状态
state2[i][j]=Math.max(state2[i][j],state1[i][j]+price[i])
3.确定初始状态
主要是对第0天和第0笔交易的设置
自己构思一下,下面代码注释有
public class Soulution {
public int maxProfit1(int k,int[] prices){
int length=prices.length;
int[][] dp=new int[length][2];
dp[0][0]=0;
dp[0][1]-=prices[0];
for(int i=1;i<length;i++){
dp[i][0]=Math.max(dp[i-1][1]+prices[i],dp[i-1][0]);
dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-prices[1]);
}
return dp[length-1][0];
}
public int maxProfit(int k,int[] prices){
int length=prices.length;
int[][] state1=new int[length][k+1]; //第i天进行第j笔交易时,手里有1支股票
int[][] state2=new int[length][k+1]; //第i天进行第j笔交易,手里没有股票
if(length<=0||k==0){//笔数是0,或者是没有价格是无法交易的,也就没有利润
return 0;
}
/*
第0天,进行1笔交易手里有股票应该为-price[0]
>=2笔交易交易手里有股票的情况不存在
进行>=1笔交易的情况时不存在的
*/
for (int i=0;i<=k;i++){
state1[0][i]=state2[0][i]=Integer.MIN_VALUE/2;
}
/*
初始状态,第0天,进行0笔交易,手里有股票的情况??都没有进行交易手里则咋会有股票呢,这种情况应该也是不存在的
第0天,进行0笔交易,手里无股票的最大利润为0
*/
state1[0][1]=-prices[0];
state2[0][0]=0;
for (int i=1;i<length;i++){
state1[i][0]=Math.max(state1[i-1][0],state2[i-1][0]-prices[i]);//以此来保证所有天的,当天有股票为当天包括之前没有进行过交易,当天有股票的情况是
for (int j=1;j<=k;j++){
//当前进行j笔交易
state1[i][j]=Math.max(state1[i-1][j],state2[i-1][j-1]-prices[i]);//以买入作为一笔交易,当天在进行第j笔交易,且手里有一支股票,可能原因为前天剩余的股票此时还是第j笔交易,前天没有剩余的j-1笔交易,今天买入
state2[i][j]=Math.max(state1[i-1][j]+prices[i],state2[i-1][j]);//当天在进行第j笔交易,且手里没有股票,可能原因为前天不剩余股票此时为第j笔交易,前天剩余的第j笔交易,今天卖出
}
}
//最终最大利润肯定为第n-1天,手里不含有股票,进行x笔交易的最大值 函数式编程
return Arrays.stream(state2[length-1]).max().getAsInt();
}
public static void main(String[] args) {
int k = 2;
int[] prices={2,4,1};
Soulution soulution=new Soulution();
System.out.println(soulution.maxProfit(k,prices));;
}
}
贪心
HARD
- 例题1
给定一个已排序的正整数数组 nums,和一个正整数 n 。从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums 中某几个数字的和来表示。请输出满足上述要求的最少需要补充的数字个数。
输入: nums = [1,3], n = 6
输出: 1
解释:
根据 nums 里现有的组合 [1], [3], [1,3],可以得出 1, 3, 4。
现在如果我们将 2 添加到 nums 中, 组合变为: [1], [2], [3], [1,3], [2,3], [1,2,3]。
其和可以表示数字 1, 2, 3, 4, 5, 6,能够覆盖 [1, 6] 区间里所有的数。
所以我们最少需要添加一个数字。
输入: nums = [1,5,10], n = 20
输出: 2
解释: 我们需要添加 [2, 4]。
输入: nums = [1,2,2], n = 5
输出: 0
首先读题,感觉有点递归的思想,优先补“集和”缺少的最小的数字,直到满足条件
1.首先要根据数组获取所有可能组合,并计算所有组合的值
2.根据n判断组合缺少的值,可能这也需要一个记录
确定一个最小的缺失的值,通过添加它可以直接将区间从[1,x-1]扩展到[1,2x-1]
问题是怎么确定这个最小的缺失值
无法思考,看题解学习
题解:由此可以提出一个贪心的方案,每次找到未被数组nums覆盖的最小的整数,x在数组中补充x,然后寻找下一个未被覆盖的最小的整数,重复上述步骤直到[1,n]中的所有数字都被覆盖
具体实现方面,任何时候都应该满足[1,x-1]内的所有数组都被覆盖。令x的初始值为1,数组下标index的初始值为0,则初始状态下区间[1,x-1]为空区间,满足区间内的所有数组都被覆盖,进行如下操作
- 如果index在数组nums的下标范围内,且nums[index]<=x,则将nums[index]的值加给x,并将index的值加1(此处是,如果该数组里的值包含在区间内,得到新的区间起始点,并将index的值+1)
被覆盖的区间从[1,x-1]扩展到[1,x+num[index]-1],对x的值更新以后,被覆盖的区间为【1,x-1】.
- 否则,x没有被覆盖,因此需要在数组中补充x,然后将x的值乘以2.
在数组中补充x,被覆盖的区间从[1,x-1]扩展到[1,2x-1],对x更新后,被覆盖的区间为[1,x-1]
重复上述操作,直到x的值大于n
由于任何时候都满足区间[1,x-1]内的所有数字都被覆盖,因此上述操作可以保证区间[1,n]内的所有数字都被覆盖。
又由于上述操作,只在x不被覆盖时才在数组中补充x,如果不补充x,则x无法被覆盖,因此可以保证补充的数字字数最少。如果减少补充的数字个数,则无法覆盖区间[1,n]内的所有数字
大致意思为:从x=1开始判断,若第i个数不在当前[以实现1,x-1]的区间内,那我们需要补的最小的数为x,此时将x补入,区间转化为以实现[1,2x-1],若数组第i个数在区间内,即nums[i]<=2x(注意是小于等于,等于表示已经在区间内,不需要补入这个数),则我们实现的区间范围转换为[1,x+num[i]-1],然后我们去找数组内的第2个数。
终止条件为最后一个需要补入的数是<=n的,那么我们不需要补入它,然后x范围*2,超出终止。
最终实现:
public class Soulution {
public int minPatches(int[] nums ,int n){
int length=nums.length;
int index=0;
int patches=0;
int x=1;
while(x<=n){
if (index<length&&nums[index]<=x){
x+=nums[index];
index++;
}else{
patches++;
x*=2;
}
}
return patches;
}
public static void main(String[] args) {
Soulution soulution=new Soulution();
int[] nums = {};
int n = 8;
System.out.println(soulution.minPatches(nums,n));
}
}
注意x必须为long,否则有个示例是无法通过的