1.题目描述
1.1笔者分析
写动态规划简单题的时候,你会发现套路都一模一样,一个传递函数,到了中等题,就多加了几个传递函数。
class Solution {
public int maxProfit(int[] prices) {
int n=prices.length;
if(n==0)return 0;
int[] sell=new int[n+1];
int[] buy=new int[n+1];
int[] cool=new int[n+1];
sell[0]=0;
buy[0]=-prices[0];
cool[0]=0;
for(int i=1;i<n;i++){
buy[i]=Math.max(cool[i-1]-prices[i],buy[i-1]);
sell[i]=Math.max(buy[i-1]+prices[i],sell[i-1]);
cool[i]=Math.max(Math.max(sell[i-1],buy[i-1]),cool[i-1]);
}
return sell[n-1];
}
}
2.题目描述
2.1笔者分析
记忆化回溯(也称为递归+备忘录),自顶向下。采用记忆化后的时间复杂度为O(2^n),如果不进行记忆的话,时间复杂度将是O(n!),可以理解为已压缩成了只有一个分支了。我们发现,例如[2,3]和[3,2]之后的玩家选择状态都是一样的,都是可以除了2,3之外的数字进行选择,那么就可以对选择2和3后第一个玩家能不能赢进行记忆存储。这里采用state[]数组存储每个数字是否都被选过,选过则记录为1,然后我们将state.toString()使得[2,3]和[3,2]它们的结果都是一样的"0011",作为key,存储在HashMap中,value是选了2和3之后第一个玩家是否稳赢。
public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
if (maxChoosableInteger >= desiredTotal) return true;
if ((1 + maxChoosableInteger) * maxChoosableInteger / 2 < desiredTotal) return false; //1,..maxChoosable数列总和都比目标和小
int[] state = new int[maxChoosableInteger + 1]; //state[1]=1表示1被选了
return backtraceWitMemo(state, desiredTotal, new HashMap<String, Boolean>());
}
private boolean backtraceWitMemo(int[] state, int desiredTotal, HashMap<String, Boolean> map) {
String key = Arrays.toString(state); //这里比较关键,如何表示这个唯一的状态,例如[2,3]和[3,2]都是"0011",状态一样
if (map.containsKey(key)) return map.get(key); //如果已经记忆了这样下去的输赢结果,记忆是为了防止如[2,3],[3,2]之后的[1,4,5,..]这个选择区间被重复计算
for (int i = 1; i < state.length; i++){
if (state[i] == 0){ //如果这个数字i还没有被选中
state[i] = 1;
//如果当前选了i已经赢了或者选了i还没赢但是后面对方选择输了
if (desiredTotal - i <= 0 || !backtraceWitMemo(state, desiredTotal - i, map)) {
map.put(key, true);
state[i] = 0; //在返回之前回溯
return true;
}
//如果不能赢也要记得回溯
state[i] = 0;
}
}
//如果都赢不了
map.put(key, false);
return false;
}
3.题目描述
3.1笔者分析
超时了。。。。但看到评论区大佬们的解法我感觉我又可以了。
这道题类似于完全背包问题,每个物品都可以无限使用,但是要求背包必须装满,而且要求背包中的物品数目最少,归纳为数学问题就是
- v[i]:代表每种硬币的价值
- x[i]:代表每种硬币拿的个数,0<=x[i]<=amount/v[i]
- 所求问题可以归纳为:
- 在满足:amount=v1x1+v2x2+v3x3+…vnxn的条件下
- 求:target=min{x1+x2+x3+…xn}
- 最简单的一种思路就是把所有{xi}的组合全部拿出来然后让target最小即可,利用递归就可以解决问题,但是时间复杂度会很高,但是如果有好的剪枝策略也可以使用。
- 另外一种方法就是常规的动态规划,利用一个amount+1长度的dp数组,记录每一个状态的最优解。
其实写了几道动态规划的题之后发现难点在于状态转移方程(这样叫只是感觉好理解)和状态对象的选取,哪些数据的存储可以减少一些不必要的计算,那么它就是状态对象,
class Solution {
public int coinChange(int[] coins, int amount) {
if(coins.length==0)return -1;
int[] dp=new int[amount+1];
Arrays.fill(dp,1,dp.length,Integer.MAX_VALUE);
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],dp[j-coins[i]]+1);
}
}
}
if(dp[amount]!=Integer.MAX_VALUE)
return dp[amount];
return -1;
}
}
4.题目描述
4.1笔者分析
本来是打算和上题一样,存入每个分数的方法数,但后来发现有重复了,比如15=10+5,15=5+10,在我的算法里面就属于不同类型,但其实是重复的。但有些方法是真巧妙啊,彻底规避了这个问题。就本题而言,把硬币种类循环放外面就行了,真是牛逼啊。其实我感觉今天要写一题就做一题分析了,太舒适了。
class Solution {
public int waysToChange(int n) {
int[] dp=new int[n+1];
int mod=1000000007;
if(n<=0)return 0;
int[] coins=new int[]{25,10,5,1};
dp[0]=1;
for(int coin:coins){
for(int i=coin;i<=n;i++){
dp[i]=(dp[i]+dp[i-coin])%mod;
}
}
return dp[n];
}
}
5.题目描述
5.1笔者分析
我承认有点飘了,有点简单。
class Solution {
public int combinationSum4(int[] nums, int target) {
int[] dp=new int[target+1];
Arrays.sort(nums);
dp[0]=1;
int temp=0;
int i,j;
for(i=1;i<=target;i++){
temp=0;
for(j=0;j<nums.length;j++){
if(i>=nums[j])temp+=dp[i-nums[j]];
else break;
}
dp[i]=temp;
}
return dp[target];
}
}
总结
动态规划牛逼,多状态对象,双层循环内外交换出题,每日十题三十四天,以下图为证