【leetcode】dp---中等(1)***375. 猜数字大小 II_dp最差下最优解_min(max())(2)376. 摆动序列_dp分类(3)*377. 组合总和 Ⅳ_dp背包问题集合

375、我们正在玩一个猜数游戏,游戏规则如下:

我从 1 到 n 之间选择一个数字,你来猜我选了哪个数字。

每次你猜错了,我都会告诉你,我选的数字比你的大了或者小了。

然而,当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。直到你猜到我选的数字,你才算赢得了这个游戏。

示例:

n = 10, 我选择了8.

第一轮: 你猜我选择的数字是5,我会告诉你,我的数字更大一些,然后你需要支付5块。
第二轮: 你猜是7,我告诉你,我的数字更大一些,你支付7块。
第三轮: 你猜是9,我告诉你,我的数字更小一些,你支付9块。

游戏结束。8 就是我选的数字。

你最终要支付 5 + 7 + 9 = 21 块钱。

给定 n ≥ 1,计算你至少需要拥有多少现金才能确保你能赢得这个游戏。

猜数字大小_详细题解

dp[i][j]:以从i到j的数字作为分割点(猜的数),必定赢的游戏所用钱的最小值。

dp[1][3]指只有三个数字1,2,3

  1. 以1作为分割点(猜 1):
    • 答案是1,花费0元
    • 答案是2 / 3,这个时候会进入另一个区间[2,3],花费1+dp[2][3]元
      必定赢得游戏,最多花费max(0,1+dp[2][3])元
  2. 以2作为分割点(猜 2):
    • 答案是1,花费2+dp[1][1]=2+0=2元
    • 答案是2,花费0元
    • 答案是3,花费2+dp[3][3]=2+0=2元
      必定赢得游戏,最多花费max(0,2+dp[1][1],2+dp[3][3])元
  3. 以3作为分割点(猜 3):
    • 答案是1 / 2,花费3+dp[1][2]元
    • 答案是3,花费0元
      必定赢得游戏,最多花费max(0,3+dp[1][2])元

综上,只要进入[1][3]这个区间,我们只要花费min( max(0,1+dp[2][3]) , max(0,2+dp[1][1],2+dp[3][3]) , max(0,3+dp[1][2]) )元必定可以赢的游戏。dp[1][3]也就等于那个min的值。

状态转移方程:

 

  •  dp[i][j]:从区间i~j赢得游戏的最小所需金额
  •  d0 = i + dp[i+1][j]; 选i点
  •  d1 = max(dp[i][k-1], dp[k+1][j]) + k; 选k点,i<k<j
  •  d2 = j + dp[i][j-1]; 选j点
  •  dp[i][j] = min(d0,d1,d2)
// dp[i][j]:从区间i~j赢得游戏的最小所需金额
// d0 = i + dp[i+1][j]; 选i点
// d1 = max(dp[i][k-1], dp[k+1][j]) + k; 选k点,i<k<j
// d2 = j + dp[i][j-1]; 选j点
// dp[i][j] = min(d0,d1,d2)
const int MAX = 1e4;
class Solution {
public:
    int getMoneyAmount(int n) {
        vector<vector<int>> dp(n + 5, vector<int>(n + 5, MAX));
        dp[1][1] = 0;
        for(int j = 2; j <= n; j++){
            dp[j][j] = 0;
            for(int i = j - 1; i >= 1; i--){
                for(int k = i + 1; k < j; k++){
                    dp[i][j] = min(dp[i][j], max(dp[i][k-1], dp[k+1][j]) + k);
                }
                dp[i][j] = min(dp[i][j], min(i + dp[i+1][j], j + dp[i][j-1]));
            }
        }
        return dp[1][n];
    }
};

结果:

执行用时:60 ms, 在所有 C++ 提交中击败了19.88% 的用户

内存消耗:6.7 MB, 在所有 C++ 提交中击败了57.93%

 

376、如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。

例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3) 是正负交替出现的。相反, [1,4,7,2,5] 和 [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。

示例 1:

输入: [1,17,5,10,13,15,10,5,16,8]
输出: 7
解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。

特殊情况:[0,0] 输出:1

dp[i][j]:以i为结尾的最长序列,j:i属于上扬或者下垂。j=0,i属于下垂;j=1,i属于上扬

状态转移:找i前大于i,dp[i][0] = max(dp[j][1]); 找i前小于id,dp[i][1] = max(dp[j][0]);

// dp[i][j]:以i为结尾的最长序列,j:i属于上扬或者下垂。j=0,i属于下垂;j=1,i属于上扬
// 状态转移:找i前大于i,dp[i][0] = max(dp[j][1]); 找i前小于id,dp[i][1] = max(dp[j][0]);
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if(n <= 1) return n;
        vector<vector<int>> dp(n, vector<int>(2, 0));
        dp[0][0] = 1;
        dp[0][1] = 1;
        bool flag = true;
        for(int i = 1; i < n; i++){
            if(nums[i]) flag = false;
            for(int j = 0; j < i; j++){
                if(nums[j] < nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1); 
                if(nums[j] > nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1);
            }
        }
        if(flag) return 1;
        return max(dp[n-1][0], dp[n-1][1]);
    }
};

结果:

执行用时:24 ms, 在所有 C++ 提交中击败了7.56% 的用户

内存消耗:7.9 MB, 在所有 C++ 提交中击败了5.20% 的用户

 

修改:优化减少时间复杂度

 dp[i][j]:前i个数字的最长序列,j=0,序列结尾为下垂;j=1,序列结尾为上扬
 状态转移:若nums[i]>nums[i-1],dp[i][1] = dp[i-1][0]+1;dp[i][0] = dp[i-1][0] 
          若nums[i]<nums[i-1],dp[i][0] = dp[i-1][1]+1;dp[i][1] = dp[i-1][1] 
          若nums[i]=nums[i-1], dp[i][0] = dp[i-1][0], dp[i][1] = dp[i-1][1] 

// dp[i][j]:前i个数字的最长序列,j=0,序列结尾为下垂;j=1,序列结尾为上扬
// 状态转移:若nums[i]>nums[i-1],dp[i][1] = dp[i-1][0]+1;dp[i][0] = dp[i-1][0] 
//          若nums[i]<nums[i-1],dp[i][0] = dp[i-1][1]+1;dp[i][1] = dp[i-1][1] 
//          若nums[i]=nums[i-1], dp[i][0] = dp[i-1][0], dp[i][1] = dp[i-1][1] 
class Solution {
public:
    int wiggleMaxLength(vector<int>& nums) {
        int n = nums.size();
        if(n <= 1) return n;
        vector<vector<int>> dp(n, vector<int>(2, 0));
        dp[0][0] = 1;
        dp[0][1] = 1;
        bool flag = true;
        for(int i = 1; i < n; i++){
            if(nums[i]) flag = false;
            if(nums[i] > nums[i-1]) dp[i][1] = dp[i-1][0] + 1, dp[i][0] = dp[i-1][0]; 
            else if(nums[i] < nums[i-1]) dp[i][0] = dp[i-1][1] + 1, dp[i][1] = dp[i-1][1];
            else dp[i][0] = dp[i-1][0], dp[i][1] = dp[i-1][1];
        }
        if(flag) return 1;
        return max(dp[n-1][0], dp[n-1][1]);
    }
};

结果:

执行用时:4 ms, 在所有 C++ 提交中击败了57.96% 的用户

内存消耗:7.9 MB, 在所有 C++ 提交中击败了5.20% 的用户

 

377、给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。

示例:

nums = [1, 2, 3]
target = 4

所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)

请注意,顺序不同的序列被视作不同的组合。

因此输出为 7。

 状态转移:步长为nums[i]中元素,
 dp[i] = sum(dp[i - nums[j]]) 

由于数据偏大,需要设置合理的MOD和数据类型long long 

// dp[i]:和为i的组合数
// 状态转移:步长为nums[i]中元素,
// dp[i] = sum(dp[i - nums[j]])
class Solution {
public:
    long long MOD = 1e12+7;
    int combinationSum4(vector<int>& nums, int target) {
        int n = nums.size();
        vector<long long> dp(target + 1, 0);
        dp[0] = 1;
        for(int i = 1; i <= target; i++){
            for(int j = 0; j < n; j++){
                if(i - nums[j] >= 0) dp[i] += dp[i-nums[j]] % MOD;
            }
        }
        return dp[target] % MOD;
    }
};

结果:

执行用时:4 ms, 在所有 C++ 提交中击败了65.21% 的用户

内存消耗:6.9 MB, 在所有 C++ 提交中击败了77.92% 的用户

 

常见背包问题集合 

©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页