动态规划专题

分享丨【题单】动态规划(入门/背包/状态机/划分/区间/状压/数位/树形/数据结构优化) - 力扣(LeetCode)

X.线性DP

Leetcode 3176/3177.求出最长好子序列 I/求出最长好子序列 II

题型分析: 

 (1)记忆化搜索(能过小数据)时间复杂度为O(n²k)。母题来源于LIS

class Solution {
public:
    // 状态数是n·k,状态专题的时间复杂度是n,所以总的时间复杂度是O(n²·k)
    int maximumLength(vector<int>& nums, int k) {
        int n = nums.size();
        int f[n][k + 1];
        memset(f, -1, sizeof f);
        // dp(i, x)表示以nums[i]结尾的,有至多x对相邻元素不同 的 最长子序列的长度
        function<int(int, int)> dp = [&](int i, int x)->int{
            if(x < 0)   return 0;
            if(i == 0)  return 1;
            if(f[i][x] != -1)   return f[i][x];
            int res = INT_MIN;
            for(int j = 0; j < i; j ++){
                if(nums[j] == nums[i])
                    res = max(res, dp(j, x) + 1);
                else
                    res = max(res, dp(j, x - 1) + 1);
            }
            return f[i][x] = res;
        };
        int ans = 0;
        for(int i = 0; i < n; i ++)
            ans = max(ans, dp(i, k));
        return ans;
    }
};

 (2)优化:从值域角度考虑:时间复杂度为O(n·k)

class Solution {
public:
    int maximumLength(vector<int>& nums, int k) {
        // 按照出现的值进行优化, 而非下标(状态转移必然要O(n)),由于值域1e9,所以用hash
        unordered_map<int, vector<int>> fs; 
        vector<int> mx(k + 1, 0);  
        // 为了实现O(1)的状态转移,需要用O(1)的时间知道f[y][j - 1] for y in set的最大值,用mx维护
        for(int x: nums){
            auto& f = fs[x];
            f.resize(k + 1);
            for(int j = k; j >= 0; j --){
                // 这边j要倒序(思想类似于背包),防止污染上一轮的mx[j - 1]
                f[j] += 1;  // 必定会+1(把x放到x结尾的子序列的末尾)sit1:
                if(j > 0)   f[j] = max(f[j], mx[j - 1] + 1);    // sit2: 加到y结尾的子序列末尾(y!=x)
                // mx[j - 1]就表示了max(f[y][j] for y in set)这里y是可以等于x的,如果也是mx[j -1]的子序列就是与x相等的
                //那么mx[j-1]就变成了f[x][j - 1],相较于f[x][j]甚至条件更严格,所以只会比上面的f[x][j]小,不影响答案。
                mx[j] = max(mx[j], f[j]);   // 及时比较更新
            }
        }
        return mx[k];
    }
};

Leetcode 198. 打家劫舍

法一:记忆化搜索(自顶向下)

时间与空间均为O(n)

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n, -1);
        function<int(int)> dp = [&](int i)-> int{
            if(i < 0)   return 0;
            if(f[i] != -1)  return f[i];
            int res = 0;
            res = max(dp(i - 1), dp(i - 2) + nums[i]);
            f[i] = res;
            return res;
        };
        return dp(n - 1);
    }
};

法二:动态规划&&空间优化

dp: 时间与空间均为O(n)

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

空间优化(滚动数组)--空间复杂度优化到O(1)

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        int f1 = 0, f2 = 0, f3 = 0;
        for(int i = 0; i < n; i ++){
            f3 = max(f2, nums[i] + f1);
            int t = f2;
            f2 = f3;
            f1 = t;
        }
        return f2;
    }
};

Leetcode 213. 打家劫舍 II

Leetcode 337. 打家劫舍 III

Leetcode 2560. 打家劫舍 IV

Leetcode 121. 买卖股票的最佳时机

Leetcode 122. 买卖股票的最佳时机 II

Leetcode 123. 买卖股票的最佳时机 III

Leetcode 188. 买卖股票的最佳时机 IV

Leetcode 53. 最大子数组和

方法一:DP

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> f(n + 1, -2e9);
        if(nums[0] > -2e9) f[0] = nums[0];
        for(int i = 1; i < n; i ++){
            f[i] = max(nums[i], f[i - 1] + nums[i]);
        }
        int res = -2e9;
        for(int i = 0; i < n; i ++) res = max(res, f[i]);
        return res;
    }
};

方法二:前缀和

 转换为Leetcode 121. 买卖股票的最佳时机

class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int n = nums.size(), res = -2e9;
        int min_pre_sum = 0;
        int pre_sum = 0;
        for(int i = 0; i < n; i ++){
            pre_sum += nums[i];     // 维护出前缀和
            res = max(res, pre_sum - min_pre_sum);      // 减去前缀和的最小值
            min_pre_sum = min(min_pre_sum, pre_sum);    // 维护前缀和的最小值
        }
        return res;
    }
};

Leetcode 3129. 找出所有稳定的二进制数组 I/II

方法一:记忆化搜索

class Solution {
public: 
    int f[1010][1010][2];
    int numberOfStableArrays(int zero, int one, int limit) {
        int MOD = 1e9 + 7;
        memset(f, -1, sizeof f);
        // dp(i,j,k)表示用i个0个和j个1构造合法方案数,其中第i+j的位置填k
        function<int(int, int, int)> dp = [&](int i, int j, int k) -> int{
            if(i < 0 || j < 0)  return 0;
            if(i == 0)  return k == 1 && j <= limit;
            if(j == 0)  return k == 0 && i <= limit;
            if(f[i][j][k] != -1)    return f[i][j][k];
            int res = 0;    // 方案数
            if(k == 0)      // +MOD保证答案非负
                res = ((long long)dp(i - 1, j, 0) + dp(i - 1, j, 1) - dp(i - limit - 1, j, 1) + MOD) % MOD;
            else    
                res = ((long long)dp(i, j - 1, 0) + dp(i, j - 1, 1) - dp(i, j - limit - 1, 0) + MOD) % MOD;
            f[i][j][k] = res;
            return res;
        };
        return (dp(zero, one, 1) + dp(zero, one, 0)) % MOD;
    }
};

8/7日每日一题,错因:当j>limit的时候显然是错误情况需要返回0。

在下面的dp方法中,体现在

方法二:DP

由记忆化搜索1:1转化来,主要注意:

        ①初始条件:分别进行初始化,zero和limit取min(不一定够用)

        ②i - limit - 1可能会越界,越界的部分都是0,这里用问号冒号表达式处理了

        ③内部计算可能会超出int范围,所有用longlong, +MOD )%MOD

class Solution {
public:
    int MOD = 1e9 + 7;
    int f[1010][1010][2] = {0};
    int numberOfStableArrays(int zero, int one, int limit) {
        // 初始化
        for(int i = 1; i <= min(zero, limit); i ++)
            f[i][0][0] = 1;
        for(int i = 1; i <= min(one, limit); i ++)
            f[0][i][1] = 1;
        // 递推
        for(int i = 1; i <= zero; i ++)
            for(int j = 1; j <= one; j ++){
                f[i][j][0] = ((long long)f[i - 1][j][0] + f[i - 1][j][1] - (i >= limit + 1? f[i - limit - 1][j][1]: 0) + MOD) % MOD;
                f[i][j][1] = ((long long)f[i][j - 1][0] + f[i][j - 1][1] - (j >= limit + 1? f[i][j - limit - 1][0]: 0) + MOD) % MOD;
            }
        return ((long long)f[zero][one][0] + f[zero][one][1]) % MOD;
    }
};

Leetcode 1035. 不相交的线

思考过程&题解:

class Solution {
public:
    int f[510][510] = {0};
    int maxUncrossedLines(vector<int>& nums1, vector<int>& nums2) {
        int n = nums1.size(), m = nums2.size();
        memset(f, -1, sizeof f);
        function<int(int, int)> dp = [&](int i, int j) -> int{
            if(i < 0 || j < 0)    return 0;   // 有一端没有数字就连接不了
            if(f[i][j] != -1)   return f[i][j];
            int res = 0;
            if(nums1[i] == nums2[j]) res = dp(i - 1, j - 1) + 1;
            else    res = max(dp(i - 1, j), dp(i, j - 1));
            f[i][j] = res;
            return res;
        };
        return dp(n - 1, m - 1);
    }
};

Leetcode 552. 学生出勤记录 II

思路:

代码:

class Solution {
public:
    const static int MOD = 1e9 + 7;
    int checkRecord(int n) {
        int f[n + 2][3][5];
        memset(f, -1, sizeof f);
        auto dp = [&](auto&& dp, int i, int j, int k) -> long long{
            if(j > 1 || k >= 3)    return 0;    // 注意顺序, 有可能是i == 0且j和k非法,那应该算是非法状态返回0,如果与下面这行互换,则会返回1
            if(i == 0)  return 1;               // 其他解决办法: 特判当j == 0的时候才加dp(i - 1, j + 1, 0),k同理
            if(f[i][j][k] != -1)    return f[i][j][k];
            f[i][j][k] = ((long long)dp(dp, i - 1, j, 0) + dp(dp, i - 1, j + 1, 0) + dp(dp, i - 1, j, k + 1)) % MOD;
            return f[i][j][k];
        };
        return dp(dp, n, 0, 0) % MOD;
    }
};

Leetcode 3154. 到达第 K 级台阶的方案数

dp思路(核心在于寻找限制/子问题)

class Solution {
public:
    int waysToReachStair(int k) {
        unordered_map<long long, int> memo;
        auto dfs = [&](auto&& dfs, int i, int j, bool pre_down) -> int {
            if (i > k + 1) { // 无法到达终点 k
                return 0;
            }
            // 把状态 (i, j, pre_down) 压缩成一个 long long
            long long mask = (long long) i << 32 | j << 1 | pre_down;
            if (memo.contains(mask)) { // 之前算过了
                return memo[mask];
            }
            int res = i == k;
            res += dfs(dfs, i + (1 << j), j + 1, false); // 操作二
            if (!pre_down) {
                res += dfs(dfs, i - 1, j, true); // 操作一
            }
            return memo[mask] = res; // 记忆化
        };
        return dfs(dfs, 1, 0, false);
    }
};

组合数学方法见数学专题。

Leetcode 2708. 一个小组的最大实力值

class Solution {
public:
    // maxx = max({maxx, nums[i], maxx * nums[i], minn * nums[i]});
    // minn = min({minn, nums[i], minn * nums[i], maxx * nums[i]});     两个式子要同时计算
    long long maxStrength(vector<int>& nums) {
        long long maxx = nums[0], minn = nums[0];
        for(int i = 1; i < nums.size(); i ++){
            long long tmp = maxx;
            long long x = nums[i];
            maxx = max({maxx, x, maxx * x, minn * x});
            minn = min({minn, x, minn * x, tmp * x});
        }
        return maxx;
    }
};

如果将子序列修改为子数组,那么变为下面152题。

Leetcode 152. 乘积最大子数组

 思路分析:

 关注这里面在维护当前最大值和当前最小值的时候,nums[i]是一定会在的(而非选or不选)。

class Solution {
public:
    int maxProduct(vector<int>& nums) {
        int res = INT_MIN, curmax = 1, curmin = 1;
        for(int i = 0; i < nums.size(); i ++){
            if(nums[i] < 0) swap(curmax, curmin);
            curmax = max(nums[i], curmax * nums[i]);
            curmin = min(nums[i], curmin * nums[i]);
            res = max(res, curmax);
        }
        return res;
    }
};

X.数位DP

Leetcode 2376. 统计特殊整数(模板) 

class Solution {
public:
    int countSpecialNumbers(int n) {
        string s = to_string(n);
        int m = s.length();
        int f[m][1 << 10];          // 备忘录
        memset(f, -1, sizeof f);    // 初始化为-1,-1即表示没有搜索
        function<int(int, int, bool, bool)> dp = [&] (int i, int mask, bool is_lim, bool is_num)->int{
            if(i == m)  return is_num;      // is_num为true则代表搜索到了合法的数字
            if(!is_lim && is_num && f[i][mask] != -1)    return f[i][mask]; // 一定注意!is_lim这个限制

            int res = 0;
            // sit 1: 前面都没有填数字,那么这一位也可以不填数字
            if(!is_num) 
                res = dp(i + 1, mask, false, false);
            // sit 2: 枚举每一种可能填写的位数
            int upbound = is_lim ? s[i] - '0' : 9;
            for(int j = 1 - is_num; j <= upbound; j ++)
                if ((mask >> j & 1) == 0)   // 要填的这个数字不在mask(集合)中
                    res += dp(i + 1, mask | (1 << j), is_lim && j == upbound, true);
            // 将答案记忆化
            if(!is_lim && is_num)   // 最大值和最小值只会计算一次,无需记忆化
                f[i][mask] = res;
            return res;
        };
        return dp(0, 0, true, false);
    }
};

Leetcode 902. 最大为 N 的数字组合

 直接修改一下板子就可以。

class Solution {
public:
    int atMostNGivenDigitSet(vector<string>& digits, int n) {
        string s = to_string(n);
        int m = s.length(), f[m];
        memset(f, -1, sizeof f);

        function<int(int, bool, bool)>  dp = [&](int i, bool is_lim, bool is_num)->int{
            if (i == m) return is_num;
            if (!is_lim && is_num && f[i] != -1)  return f[i];

            int res = 0;
            // sit 1:
            if (!is_num)    res = dp(i + 1, false, false);    
            // sit 2:
            char up = is_lim ? s[i] : '9';
            for (int j = 0; j < digits.size() && digits[j][0] <= up; j ++)
                res += dp(i + 1, digits[j][0] == s[i] && is_lim, true);
            if (!is_lim && is_num)  f[i] = res;
            return res;
        };
        return dp(0, true, false);  // true表示得按照限制去执行
    }
};

Leetcode 1012. 至少有 1 位重复的数字

“至少”这种问题-->正难则反,转换成求无重复数字的个数。答案等于 n 减去无重复数字的个数。(leetcode 2376)

class Solution {
public:
    int numDupDigitsAtMostN(int n) {
        string s = to_string(n);
        int m = s.length(), f[m][1 << 10];
        memset(f, -1, sizeof(f));
        function<int(int, int, bool, bool)> dp = [&](int i, int mask, bool is_lim, bool is_num)->int{
            if (i == m) return is_num;
            if (!is_lim && is_num && f[i][mask] != -1)  return f[i][mask];

            int res = 0;
            if (!is_num)    res = dp(i + 1, mask, false, false); // 本位不选
            int up = is_lim ? s[i] - '0' : 9;
            for(int j = 1 - is_num; j <= up; j ++){
                if (((mask >> j) & 1) == 0)
                    res += dp(i + 1, mask | (1 << j), j == up && is_lim, true);
            }
            if (!is_lim && is_num)  f[i][mask] = res;
            return res;
        };
        return n - dp(0, 0, true, false);   // is_lim设置为true时表示第一个位需要受到s[0]的限制
    }
};

Leetcode 600. 不含连续1的非负整数

        前几题是直接在位置上填十进制数,这边实际上就是改成填写二进制数即可。

class Solution {
public:
    // 多少个整数的二进制表示中不存在连续的1?
    int findIntegers(int n) {
        int m = __lg(n);        // 二进制表示最高位的位数在哪(从0开始)
        int f[m + 1][2];        // 第二维的状态只有2个, 第一维代表二进制空位上的下标(m有可能为0,所以要+1),第二维是该下标对应的数字是多少
        memset(f, -1, sizeof f);

        // 为什么不用 bool is_num:因为本题是连续的1,所以有前导0也无所谓
        function<int(int, int, bool)> dp = [&](int i, int pre, bool is_lim)->int{
            if (i < 0)     return 1;   // 能运行到这个条件的,一定是正确情况
            if (!is_lim && f[i][pre] != -1)  return f[i][pre];

            int res = 0;
            int up = is_lim ? n >> i & 1 : 1;
            // sit 1: 本位填0无所谓,一定不会涉及到连续1的条件
            res += dp(i - 1, 0, up == 0 && is_lim);
            // sit 2: 如果本位要填写1,一定要检查
            if(pre != 1 && up == 1)    res += dp(i - 1, 1, is_lim);
            if(!is_lim)     f[i][pre] = res;
            return res;
        };
        return dp(m, 0, true);
    }
};

Leetcode 233. 数字 1 的个数

class Solution {
public:
    int countDigitOne(int n) {
        string s = to_string(n);
        int m = s.length();
        int f[m][m];
        memset(f, -1, sizeof f);
        function<int(int, int, bool, bool)> dp = [&](int i, int k, bool is_lim, bool is_num)->int{
            if(i == m)  return k;
            if(!is_lim && is_num && f[i][k] != -1) return f[i][k];
            int res = 0;
            if(!is_num) res = dp(i + 1, 0, false, false);
            int up = is_lim ? s[i] - '0' : 9;
            for(int j = 1 - is_num; j <= up; j ++){
                res += dp(i + 1, k + (j == 1), is_lim && j == up, true);
            }
            if(!is_lim && is_num)   f[i][k] = res;
            return res;
        };
        return dp(0, 0, true, false);
    }
};

六.划分型DP

6.1 判定能否划分

6.2 计算划分最优值

6.3 约束划分个数

(固定右,枚举左)

Leetcode 3117. 划分数组得到最小的值之和

寻找子问题

class Solution {
public:
    
    int minimumValueSum(vector<int>& nums, vector<int>& andValues) {
        const int N = INT_MAX / 2;      // 防止下面+nums[i]溢出(func:让非法状态作废)
        unordered_map<long long, int> f;
        int n = nums.size(), m = andValues.size();
        // 维度i:访问数组的第i个元素 维度j:andValues数组的第j个元素 维度and_当前组前面已经&的结果(单调不升)
        auto dp = [&](auto&& dp, int i, int j, int and_) -> int {
            if(n - i < m - j)   return N;   // 不够划分的
            if(j == m)  return i == n ? 0: N;
            
            int ad = and_ & nums[i];    // 先算新的and值,才有state!!!
            long long st = (long long) i << 36 | (long long) j << 32 | ad;
            if(f.count(st)) return f[st];
            if(ad < andValues[j])   return N;           // 剪枝, 往后再运算不可能得到andValues[j]了
            int res = dp(dp, i + 1, j, ad);             // 不选(即继续往当前子数组添加数,而非重新开一个子数组)
            if(ad == andValues[j])                      // and值重置为-1为下一各子数组的情况做准备
                res = min(res, dp(dp, i + 1, j + 1, -1) + nums[i]);    // 选择nums[i]作为子数组的最后一个元素
            return f[st] = res;                         // 记忆化
        };
        int ans = dp(dp, 0, 0, -1);
        return ans < N ? ans: -1;
    }
};

6.4 不相交区间

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_Ocean__

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值