【Leetcode】动态规划

本文深入探讨了动态规划在解决打家劫舍、股票交易问题以及背包问题中的应用。通过分析198.打家劫舍、213.打家劫舍II、337.打家劫舍III等题目,展示了动态规划如何计算最大收益。此外,还讨论了买卖股票系列问题,如121.买卖股票的最佳时机,以及01背包和完全背包问题,如416.分割等和子集、518.零钱兑换II。文章揭示了动态规划和组合优化在解决复杂问题时的高效性和灵活性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

打家劫舍问题

198. 打家劫舍

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。

class Solution {
public:
    int rob(vector<int>& nums) {
      int len = nums.size();
        if(len == 0) return 0;
        int pre = 0, cur = 0;
        for(auto &i : nums){
            int temp = cur;
            cur = max(pre + i, cur);
            pre = temp;
        }
        return cur;
    }
};

213. 打家劫舍 II (围成一圈)

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if(n == 1) return nums[0];
        return max(robrange(nums, 0, n-2), robrange(nums, 1, n-1));
    }

    int robrange(vector<int>& nums, int start, int end) {
        int pre = 0, cur = 0;
        for(int i=start; i<=end; ++i){
            int temp = cur;
            cur = max(pre + nums[i], cur);
            pre = temp;
        }
        return cur;
    }
};

337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。
输入: [3,2,3,null,3,null,1]
3
/
2 3
\ \
3 1
输出: 7
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.
要加memo,否则会超时

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode() : val(0), left(nullptr), right(nullptr) {}
 *     TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
 *     TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
 * };
 */
class Solution {
public:
    unordered_map<TreeNode*, int> memo;
    int rob(TreeNode* root) {
        //抢和不抢
        if(root==nullptr) return 0;
        if(memo.find(root)!=memo.end()) return memo[root];
        int do_it = root->val +
                    (root->left==nullptr?0:rob(root->left->left) + rob(root->left->right)) +
                    (root->right==nullptr?0:rob(root->right->left) + rob(root->right->right));
        int not_do = rob(root->left) + rob(root->right);
        int res = max(do_it, not_do);
        memo[root] = res;
        return res;
    }
};

买卖股票问题

121. 买卖股票的最佳时机(买卖一次)

状态为今天持不持股

给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0.

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /*
        dp[i][0]:规定了今天不持股,有以下两种情况:
            昨天不持股,今天什么都不做;
            昨天持股,今天卖出股票(现金数增加),
        dp[i][1]:规定了今天持股,有以下两种情况:
            昨天持股,今天什么都不做(现金数与昨天一样);
            昨天不持股,今天买入股票(注意:只允许交易一次,因此手上的现金数就是当天的股价的相反数)。
        */
        int len = prices.size();
        vector<vector<int> > dp(len, vector<int>(2));
        dp[0][0] = 0; //不持股
        dp[0][1] = -prices[0]; //持股
        for(int i=1; i<len; ++i){  
            dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]);
            dp[i][1] = max(dp[i-1][1], -prices[i]);
        }
        return dp[len-1][0];
    }
};

122. 买卖股票的最佳时机 II(买卖多次)

给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        /*
        dp[i][0]:规定了今天不持股,有以下两种情况:
            昨天不持股,今天什么都不做;
            昨天持股,今天卖出股票(现金数增加),
        dp[i][1]:规定了今天持股,有以下两种情况:
            昨天持股,今天什么都不做(现金数与昨天一样);
            昨天不持股,今天买入股票。
        */
        int len = prices.size();
        vector<vector<int> > dp(len, vector<int>(2));
        dp[0][0] = 0; //不持股
        dp[0][1] = -prices[0]; //持股
        for(int i=1; i<len; ++i){  
            dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return dp[len-1][0];
    }
};

123. 买卖股票的最佳时机 III(最多两笔)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        // 需要把已经交易了多少次设置成为一个状态的维度
        int len = prices.size();
        int dp[len][3][2];
        memset(dp, 0, sizeof(dp));
        dp[0][1][1] = -prices[0]; 
        dp[0][2][1] = INT32_MIN;
        for(int i=1; i<len; ++i){ 
            dp[i][1][0] =  max(dp[i-1][1][0], prices[i]+dp[i-1][1][1]);
            dp[i][1][1] =  max(dp[i-1][1][1], -prices[i]);
            dp[i][2][0] =  max(dp[i-1][2][0], prices[i]+dp[i-1][2][1]);
            dp[i][2][1] =  max(dp[i-1][2][1], -prices[i]+dp[i-1][1][0]);
        }
        return max(dp[len-1][2][0], dp[len-1][1][0]);
    }
};

188. 买卖股票的最佳时机 IV(最多k笔)

给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。设计一个算法来计算你所能获取的最大利润。你最多可以完成k 笔交易。注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int len = prices.size();
        int dp[k+1][2];
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i <= k; i++) {
            dp[i][1] = INT32_MIN; //注意初始化
            //dp[0][1]:表示一次交易都没有发生,但是持股,这是不可能的,也不会有后序的决策要用到这个状态值,可以不用管;
            //dp[x][1] 可能是负值,如果我们简单的初始化为0,会干扰我们的结果,必须初始化最小值比较好
        }
        for (int price : prices) {
            for (int j = 1; j <= k; j++) {
                dp[j][1] = max(dp[j][1], dp[j - 1][0] - price);
                dp[j][0] = max(dp[j][0], dp[j][1] + price);
            }
        }
        return dp[k][0];
    }
};

309. 最佳买卖股票时机含冷冻期(买卖多次+冷冻期1一天)

给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​
设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票):
你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int len = prices.size();
        int dp[len][3];
        memset(dp, 0, sizeof(dp));
        dp[0][1] = -prices[0];
        // dp[i][0]: 手上不持有股票,并且今天不是由于卖出股票而不持股,我们拥有的现金数
        // dp[i][1]: 手上持有股票时,我们拥有的现金数
        // dp[i][2]: 手上不持有股票,并且今天是由于卖出股票而不持股,我们拥有的现金数
        for (int i = 1; i < len; i++) {
            dp[i][0] = max(dp[i - 1][0], dp[i - 1][2]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]);
            dp[i][2] = dp[i - 1][1] + prices[i];
        }
        return max(dp[len - 1][0], dp[len - 1][2]);
    }
};

714. 买卖股票的最佳时机含手续费(买卖多次+手续费)

给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;整数 fee 代表了交易股票的手续费用。你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。
输入:prices = [1, 3, 2, 8, 4, 9], fee = 2
输出:8
解释:能够达到的最大利润:
在此处买入 prices[0] = 1
在此处卖出 prices[3] = 8
在此处买入 prices[4] = 4
在此处卖出 prices[5] = 9
总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8

//相比于 买卖多次 在转移方程加fee即可
class Solution {
public:
    int maxProfit(vector<int>& prices, int fee) {
        int len = prices.size();
        vector<vector<int> > dp(len, vector<int>(2));
        dp[0][0] = 0; //不持股
        dp[0][1] = -prices[0]; //持股
        for(int i=1; i<len; ++i){  
            dp[i][0] = max(dp[i-1][0], prices[i]+dp[i-1][1]-fee);
            dp[i][1] = max(dp[i-1][1], dp[i-1][0]-prices[i]);
        }
        return dp[len-1][0];
    }
};

背包问题

通用解法

01 背包问题
如果是 01 背包,即数组中的元素不可重复使用,外循环遍历 arrs,内循环遍历 target,且内循环倒序(因为物品只能用一次,从二维压缩到一维后 不能让之前的结果影响后来的结果所以要倒序):
完全背包问题
(1)如果是完全背包,即数组中的元素可重复使用并且不考虑元素之间顺序,arrs 放在外循环,target在内循环,且内循环正序。(因为可以用多次,所以不用倒序)
(2)如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序。

01背包

416. 分割等和子集

给你一个只包含正整数的非空数组nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
示例 1:
输入:nums = [1,5,11,5]
输出:true
解释:数组可以分割成 [1, 5, 5] 和 [11] 。
示例 2:
输入:nums = [1,2,3,5]
输出:false
解释:数组不能分割成两个元素和相等的子集。
提示:
1 <= nums.length <= 200
1 <= nums[i] <= 100

class Solution {
public:
    bool canPartition(vector<int>& nums) {
        if(nums.size() < 2) return false;
        //计算总和
        int sum = accumulate(nums.begin(), nums.end(), 0.0);
        int maxNum = *max_element(nums.begin(), nums.end());
        if(sum % 2) return false;
        int target = sum / 2;
        if (maxNum > target) return false; //[99,1]
        
        vector<vector<bool> > dp(nums.size(), vector<bool>(target+1));
        for (int i = 0; i < nums.size(); i++) {
            dp[i][0] = true; //如果不选取任何正整数,则被选取的正整数等于 0
        }
        dp[0][nums[0]] = true; //只有一个正整数 nums[0] 可以被选取
        for(int i=1; i<nums.size(); ++i){
            for(int j=1; j<=target; ++j){
                if(j-nums[i]<0){ //和不足,不能取第i个物品
                    dp[i][j] = dp[i-1][j];
                }
                else{
                    dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]; //取不取第i个物品
                }
            }
        }
        return dp[nums.size()-1][target];
        /*
        优化:
        // vector<int> dp(target + 1, 0);
        dp[0] = true;
        for (int i = 0; i < n; i++) {
            int num = nums[i];
            for (int j = target; j >= num; --j) {
                dp[j] |= dp[j - num];
            }
        }
        return dp[target];
        */
    }
};

494. 目标和

输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3

/*
加正负符号得到最终的目标和,相当于我们把加+的集合在一起就是正数和x,把加-的集合在一起就是负数和y.而正数和x就是我们背包问题的目标和。
x-y = target 而x+y = nums的和sum 因此x=(target+sum)/2.
也就是从nums里选出几个数,使其和为x,这就转换成了0-1背包问题
*/
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for (int& num : nums) {
            sum += num;
        }
        int diff = sum - target;
        if (diff < 0 || diff % 2 != 0) {
            return 0;
        }
        int neg = diff / 2;
        vector<int> dp(neg + 1);
        dp[0] = 1;
        for (int& num : nums) {
            for (int j = neg; j >= num; j--) {
                dp[j] += dp[j - num];
            }
        }
        return dp[neg];
    }
};

1049 最后一块石头的重量 II

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

/*
和494类似,加正负符号。记负号石头之和是neg,正号之和就是sum-neg
(sum−neg)−neg=sum−2⋅neg
要使最后一块石头的重量尽可能地小,neg 需要在不超过 ⌊sum/2⌋ 的前提下尽可能地大
因此本问题可以看作是背包容量为⌊sum/2⌋,物品重量和价值均为stones_i
*/
class Solution {
public:
    int lastStoneWeightII(vector<int> &stones) {
        int sum = accumulate(stones.begin(), stones.end(), 0); //#include <numeric>
        int n = stones.size(), m = sum / 2;
        vector<vector<int> > dp(n + 1, vector<int>(m + 1));
        dp[0][0] = true; //其中 dp[i+1][j] 表示前i个石头能否凑出重量 j。
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j <= m; ++j) {
                if (j < stones[i]) {
                    dp[i + 1][j] = dp[i][j];
                } else {
                    dp[i + 1][j] = dp[i][j] || dp[i][j - stones[i]];
                }
            }
        }
        for (int j = m;; --j) {
            if (dp[n][j]) {
                return sum - 2 * j;
            }
        }
    }
};

完全背包-组合

518. 零钱兑换 II

给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1

//如果组合问题需考虑元素之间的顺序,需将 target 放在外循环,将 arrs 放在内循环,且内循环正序
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        vector<int> dp(amount + 1); //dp[i] 目标金额为i时的组合数
        dp[0] = 1;
        for (int& coin : coins) {
            for (int i = coin; i <= amount; i++) {
                dp[i] += dp[i - coin];
            }
        }
        return dp[amount];
    }
};

完全背包-排列

322. 零钱兑换II

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT16_MAX); //dp[i] 目标金额为i时的最小硬币数
        dp[0] = 0;
        //遍历所有状态的取值
        for (int i = 1; i <= amount; i++) {
            for(int &coin : coins){
                // 子问题无解,跳过
                if(i-coin<0){
                    continue;
                }
                dp[i] = min(dp[i],dp[i-coin]+1);
            }
        }
        return dp[amount] == INT16_MAX? -1 : dp[amount];
    }
};

139. 单词拆分

给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        //使用worddict能否构成s, 可用多次, 完全背包 + 排列
        int num = wordDict.size();
        int len_s = s.size();
        vector<bool> dp(len_s + 1);  //dp[i] 为从0-i的单词是否能由wordDict中的单词拼接而来
        dp[0] = true; //不使用任何构成字符串
        for (int i = 1; i <= len_s; i++) {
            for(int &word : wordDict){
                int wl = word.size();
                if(i-wl >= 0 and word == s.substr(i-wl, wl)){
                    dp[i] = dp[i] or dp[i-wl];
                }
            }
        }
        return dp[len_s];
    }
};

983. 最低票价

在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days 的数组给出。每一项是一个从 1 到 365 的整数。
火车票有三种不同的销售方式:
一张为期一天的通行证售价为 costs[0] 美元;
一张为期七天的通行证售价为 costs[1] 美元;
一张为期三十天的通行证售价为 costs[2] 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2 天获得一张为期 7 天的通行证,那么我们可以连着旅行 7 天:第 2 天、第 3 天、第 4 天、第 5 天、第 6 天、第 7 天和第 8 天。
返回你想要完成在给定的列表 days 中列出的每一天的旅行所需要的最低消费。
示例 1:
输入:days = [1,4,6,7,8,20], costs = [2,7,15]
输出:11
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 1 天生效。
在第 3 天,你花了 costs[1] = $7 买了一张为期 7 天的通行证,它将在第 3, 4, …, 9 天生效。
在第 20 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 20 天生效。
你总共花了 $11,并完成了你计划的每一天旅行。
示例 2:
输入:days = [1,2,3,4,5,6,7,8,9,10,30,31], costs = [2,7,15]
输出:17
解释:
例如,这里有一种购买通行证的方法,可以让你完成你的旅行计划:
在第 1 天,你花了 costs[2] = $15 买了一张为期 30 天的通行证,它将在第 1, 2, …, 30 天生效。
在第 31 天,你花了 costs[0] = $2 买了一张为期 1 天的通行证,它将在第 31 天生效。
你总共花了 $17,并完成了你计划的每一天旅行。

class Solution {
public:
    int mincostTickets(vector<int>& days, vector<int>& costs) {
        int last = days[days.size()-1];
        vector<int> dp(last+1, 0); //当前某一天需要花费的最少费用
        int idx = 0;
        for (int i = 1; i <= last; i++) {
            if (i == days[idx]) {
                int cost = INT_MAX;
                int oneDayAgo = i-1;
                int sevenDaysAgo = i-7>0?i-7:0;
                int thirtyDaysAgo = i-30>0?i-30:0;
                cost = min({dp[oneDayAgo] + costs[0], dp[sevenDaysAgo] + costs[1], dp[thirtyDaysAgo] + costs[2], cost});
                dp[i] = cost;
                idx++;
            } else {
                dp[i] = dp[i-1]; //贪心:当前不是待处理天数
            }
        }
        return dp[last];
    }
};

279. 完全平方数

给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, …)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。
给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4

class Solution {
public:
    int numSquares(int n) {
        vector<int> f(n + 1, INT_MAX); //和为n时的个数
        f[0] = 0;
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j * j <= i; j++) {
                f[i] = min(f[i], f[i - j * j]+1);
            }
        }
        return f[n];
    }
};

377. 组合总和 Ⅳ

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {
        vector<unsigned int> dp(target+1); //和为i时的组合数
        //16位系统中一个int能存储的数据的范围为-32768~32767,而unsigned int能存储的数据范围则是0~65535
        dp[0] = 1;
        for(int i=1; i<=target; ++i){
            for(auto &num : nums){
                if(i >= num){
                    dp[i] = dp[i] + dp[i-num];
                }
            }
        }
        return dp[target];
    }
};

最长子序列

300. 最长递增子序列

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。
例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
示例 1:
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
示例 2:
输入:nums = [0,1,0,3,2,3]
输出:4
示例 3:
输入:nums = [7,7,7,7,7,7,7]
输出:1

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int len = nums.size();
        vector<int> dp(len); // dp[i] 为 到i的最长序列长度
        int res = INT32_MIN;
        for(int i=0; i<len; ++i){
            dp[i] = 1;
            for(int j=0; j<i; ++j){
                if(nums[j] < nums[i]){
                    dp[i] = max(dp[i], dp[j]+1);
                }
            }
            res = max(res, dp[i]);
        }
        return res;
    }
};

673. 最长递增子序列的个数

给定一个未排序的整数数组,找到最长递增子序列的个数。
示例 1:
输入: [1,3,5,4,7]
输出: 2
解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。
示例 2:
输入: [2,2,2,2,2]
输出: 5
解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。

class Solution {
public:
    int findNumberOfLIS(vector<int>& nums) {
        int len = nums.size();
        vector<int> dp(len, 1); // dp[i] 为 到i的最长序列长度
        vector<int> count(len, 1); // count[i] 为 到i的最长序列长度个数
        int max_length = 0;
        for(int i=0; i<len; ++i){
            for(int j=0; j<i; ++j){
                if(nums[j] < nums[i]){
                    if(dp[i] < dp[j]+1){
                        dp[i] = dp[j] + 1;
                        count[i] = count[j];
                    }
                    else if(dp[i] == dp[j]+1){
                        count[i] += count[j];
                    }
                }
            }
            max_length = max(max_length, dp[i]);
        }

        int res = 0;
        for(int i=0; i<len; ++i){
            if(dp[i] == max_length){
                res += count[i];
            }
        }
        return res;
    }
};

674. 最长连续递增序列

给定一个未经排序的整数数组,找到最长且连续递增的子序列,并返回该序列的长度。
示例 1:
输入:nums = [1,3,5,4,7]
输出:3
解释:最长连续递增序列是 [1,3,5], 长度为3。
尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。
示例 2:
输入:nums = [2,2,2,2,2]
输出:1
解释:最长连续递增序列是 [2], 长度为1。

class Solution {
public:
    int findLengthOfLCIS(vector<int>& nums) {
        int len = nums.size();
        vector<int> dp(len, 1); // dp[i] 为 到i的最长序列长度
        int res = 0;
        for(int i=1; i<len; ++i){
            if(nums[i-1] < nums[i]){
                dp[i] = dp[i-1]+1;
            }
            res = max(res, dp[i]);
        }
        return res;
    }
};

72.编辑距离

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
插入一个字符
删除一个字符
替换一个字符
示例 1:
输入:word1 = “horse”, word2 = “ros”
输出:3
解释:
horse -> rorse (将 ‘h’ 替换为 ‘r’)
rorse -> rose (删除 ‘r’)
rose -> ros (删除 ‘e’)

状态:dp[i+1][j+1] 为s1[0,…,i]和s2[0,…,j]之间的编辑距离
s1[i-1]!=s2[j-1]时,有三种操作:

  • 增:在 s1[i-1] 插入一个和 s2[j-1] 一样的字符 dp[i][j-1]
  • 删:s[i-1] 这个字符删掉 dp[i-1][j]
  • 替:直接把 s1[i-1] 替换成 s2[j-1] dp[i-1][j-1]
class Solution {
public:
    int minDistance(string word1, string word2) {
         int lenA = word1.size();
        int lenB = word2.size();
        if(lenA * lenB == 0) return lenA+lenB;
        int dp[lenA+1][lenB+1];
        for(int i=0; i<lenA+1; ++i){
            dp[i][0] = i;
        }
        for(int j=0; j<lenB+1; ++j){
            dp[0][j] = j;
        }
        for(int i=1; i<lenA+1; ++i){
             for(int j=1; j<lenB+1; ++j){
                 if(word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1];
                 else{
                     dp[i][j] = min({dp[i-1][j], dp[i][j-1], dp[i-1][j-1]}) + 1;
                 }
             }
         }
         return dp[lenA][lenB];
    }
};

相似题目:
583. 两个字符串的删除操作
和上题相比只有删除操作,只需要改不相等时,把s1删除掉或者把s2删除掉。
712. 两个字符串的最小ASCII删除和
比较的时候加上ASCII码值即可。
1035. 不相交的线
每个数字都可以选择连或者不连,当两值相等的时候,必然是dp[i-1][j-1]+1;不等的时候,选择连的话,可以是dp[i][j-1],选择不连的话,可以是dp[i-1][j-1]和dp[i-1][j]。

1143. 最长公共子序列

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(),n = text2.size();
        int dp[m+1][n+1];
        memset(dp, 0, sizeof(dp));
        for (int i = 1; i <= m; i++) {
            for (int j = 1; j <= n; j++) {
                if(text1[i-1]==text2[j-1]){
                    dp[i][j] = dp[i-1][j-1]+1;
                } else{
                    dp[i][j] = max(dp[i-1][j],dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
};

516. 最长回文子序列

给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。
示例 1:
输入:
“bbbab”
输出:
4
一个可能的最长回文子序列为 “bbbb”。
示例 2:
输入:
“cbbd”
输出:
2
一个可能的最长回文子序列为 “bb”。
在这里插入图片描述
在这里插入图片描述

//斜着便遍历或者反着遍历
class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n = s.size();
        int dp[n][n];
        memset(dp, 0, sizeof(dp));
        for (int i = 0; i < n; i++) dp[i][i] = 1;
        for (int i = n - 1; i >= 0; i--) {
        	for (int j = i + 1; j < n; j++) {
	            if (s[i] == s[j])
	                dp[i][j] = dp[i + 1][j - 1] + 2;
	            else
	                dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]);
	            }
        	}
        return dp[0][n - 1];
    }
};

5.最长回文子串

class Solution {
public:
    string longestPalindrome(string s) {
        int n = s.size();
        if (n < 2) {
            return s;
        }

        int maxLen = 1;
        int begin = 0;
        // dp[i][j] 表示 s[i..j] 是否是回文串
        vector<vector<int>> dp(n, vector<int>(n));
        // 初始化:所有长度为 1 的子串都是回文串
        for (int i = 0; i < n; i++) {
            dp[i][i] = true;
        }
        // 递推开始
        // 先枚举子串长度
        for (int L = 2; L <= n; L++) {
            // 枚举左边界,左边界的上限设置可以宽松一些
            for (int i = 0; i < n; i++) {
                // 由 L 和 i 可以确定右边界,即 j - i + 1 = L 得
                int j = L + i - 1;
                // 如果右边界越界,就可以退出当前循环
                if (j >= n) {
                    break;
                }

                if (s[i] != s[j]) {
                    dp[i][j] = false;
                } else {
                    if (j - i < 3) {
                        dp[i][j] = true;
                    } else {
                        dp[i][j] = dp[i + 1][j - 1];
                    }
                }

                // 只要 dp[i][L] == true 成立,就表示子串 s[i..L] 是回文,此时记录回文长度和起始位置
                if (dp[i][j] && j - i + 1 > maxLen) {
                    maxLen = j - i + 1;
                    begin = i;
                }
            }
        }
        return s.substr(begin, maxLen);
    }
};

1312. 让字符串成为回文串的最少插入次数

数位DP

模版

typedef long long ll;
int a[20];
ll dp[20][state];//dp[pos][pre]不同题目状态不同,比如当前枚举到pos位,前面一位枚举的是pre的个数
ll dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/)//不是每个题都要判断前导零
{
    //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
    if(pos==-1) return 1;
    //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
    if(!limit && !lead && dp[pos][state]!=-1) return dp[pos][state];
    int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
    ll ans=0;
    //开始计数
    for(int i=0;i<=up;i++)//枚举
    {
        if() ...
        else if()...
        ans+=dfs(pos-1,/*状态转移*/,lead && i==0,limit && i==a[pos])  //保证合法
        /*比如题目要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
        前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
    }
    //计算完,记录状态
    if(!limit && !lead) dp[pos][state]=ans;
    return ans;
}
ll solve(ll x)
{
    int pos=0;
    while(x)//把数位都分解出来
    {
        a[pos++]=x%10;//个人老是喜欢编号为[0,pos)
        x/=10;
    }
    return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);
}
int main()
{
    ll le,ri;
    while(~scanf("%lld%lld",&le,&ri))
    {
        memset(dp,-1,sizeof dp);
        printf("%lld\n",solve(ri)-solve(le-1)); //分别求区间[0,ri]和[0,le-1]就是[ri,le-1]
    }
}

233. 数字 1 的个数

给定一个整数 n,计算所有小于等于 n 的非负整数中数字 1 出现的个数。

class Solution {
public:
    int digit[11]; //2^31 是10位 倒序存储数字 个十百012
    int dp[11][11]; 
    int countDigitOne(int n) {
        //dp[pos][state]:当前位是pos位,已经出现了count个1
        if(n<=0) return 0;
        memset(dp,-1,sizeof(dp));
        int pos = 0;
        while(n){
            digit[pos++] = n%10;
            n /= 10;
        }
        return dfs(pos-1, 0, true);
    }

    int dfs(int pos, int count, bool limit){
        //边界
        if(pos==-1) return count;
        //记忆化 !limit使dp状态唯一
        if(!limit && dp[pos][count]!= -1) return dp[pos][count]; //没有达到limit
        //计数
        int res=0;
        int max = (limit?digit[pos]:9);
        for(int i=0; i<=max; ++i){
            res += dfs(pos-1, count+(i==1), limit&&(i==max));
        }
        //记录状态
        if(!limit) dp[pos][count] = res;
        return res;
    }
};

题目一:数位上不能有4也不能有连续的62

typedef long long ll;
int a[20];
int dp[20][2]; //dp[pos][sta]表示当前第pos位,前一位是否是6的状态
int dfs(int pos,int pre,int sta,bool limit)
{
    if(pos==-1) return 1;
    if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
    int up=limit ? a[pos] : 9;
    int tmp=0;
    for(int i=0;i<=up;i++)
    {
        if(pre==6 && i==2)continue;
        if(i==4) continue;//都是保证枚举合法性
        tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);
    }
    if(!limit) dp[pos][sta]=tmp;
    return tmp;
}
int solve(int x)
{
    int pos=0;
    while(x)
    {
        a[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1,-1,0,true);
}
int main()
{
    int le,ri;
    //memset(dp,-1,sizeof dp);可优化
    while(~scanf("%d%d",&le,&ri) && le+ri)
    {
        memset(dp,-1,sizeof dp);
        printf("%d\n",solve(ri)-solve(le-1));
    }
    return 0;
}

题目二:求F(x)

F(x) = An * 2n-1 + An-1 * 2n-2 + … + A2 * 2 + A1 * 1,Ai是十进制数位,然后给出a,b求区间[0,b]内满足f(i)<=f(a)的i的个数。减法

const int N=1e4+5;
int dp[12][N];
int f(int x)
{
    if(x==0) return 0;
    int ans=f(x/10);
    return ans*2+(x%10);
}
int all;
int a[12];
int dfs(int pos,int sum,bool limit)
{
    if(pos==-1) {return sum<=all;}
    if(sum>all) return 0;
    if(!limit && dp[pos][all-sum]!=-1) return dp[pos][all-sum];
    int up=limit ? a[pos] : 9;
    int ans=0;
    for(int i=0;i<=up;i++)
    {
        ans+=dfs(pos-1,sum+i*(1<<pos),limit && i==a[pos]);
    }
    if(!limit) dp[pos][all-sum]=ans;
    return ans;
}
int solve(int x)
{
    int pos=0;
    while(x)
    {
        a[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1,0,true);
}
int main()
{
    int a,ri;
    int T_T;
    int kase=1;
    scanf("%d",&T_T);
    memset(dp,-1,sizeof dp);
    while(T_T--)
    {
        scanf("%d%d",&a,&ri);
        all=f(a);
        printf("Case #%d: %d\n",kase++,solve(ri));
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值