【LeetCode周赛】2022上半年题目精选集——动态规划

news2024/11/25 7:03:56

文章目录

  • 2140. 解决智力问题
    • 解法1——倒序DP(填表法)
    • 解法2——正序DP(刷表法)⭐⭐⭐
  • 2167. 移除所有载有违禁货物车厢所需的最少时间⭐⭐⭐
    • 解法1——前缀和⭐⭐⭐⭐⭐
    • 解法2——前后缀分解 + 动态规划
      • 代码1——看了思路之后自己写的
      • 代码2——代码1的优化(一次遍历)⭐
  • 2172. 数组的最大与和(状态压缩DP)⭐⭐⭐⭐⭐
    • 思路
    • 代码
    • 补充:相似题目——1879. 两个数组最小的异或值之和⭐⭐⭐
  • 2188. 完成比赛的最少时间⭐⭐⭐⭐⭐
    • 思路——结合性质巧妙线性DP (预处理每种圈数的最短时间 + 动态规划)
    • 代码
  • 2209. 用地毯覆盖后的最少白色砖块⭐⭐⭐⭐⭐
    • 思路——考虑是否使用第i条地毯且其末尾覆盖第j块板砖
    • 代码
  • 2218. 从栈中取出 K 个硬币的最大面值和(分组背包)🐂🐂🐂
    • 分组DP模板
  • 2246. 相邻字符不同的最长路径(树形DP)
    • 思路——树形DP
    • 代码
  • 2262. 字符串的总引力⭐⭐⭐⭐⭐
    • 思路——记录各个字母上次出现的位置,考虑增加的引力值
    • 代码
  • 2266. 统计打字方案数
    • 分组 + 线性DP + 乘法原理
  • 2272. 最大波动的子字符串⭐⭐⭐⭐⭐
    • 思路——枚举最多和最少的字符+最大子数组和
    • 代码
  • 2305. 公平分发饼干(子集状态压缩DP)
    • 解法1——dfs回溯+剪枝
    • 解法2——子集状压DP⭐⭐⭐⭐⭐(很**重要**!值得一学)
      • 代码技巧——如何枚举一个集合的所有子集⭐🐂
      • 代码的空间优化
    • 补充:相似题目练习——1723. 完成所有工作的最短时间
  • 2312. 卖木头块⭐⭐⭐
    • 解法1——记忆化搜索
    • 解法2——线性DP
  • 2318. 不同骰子序列的数目
      • 解法1——三维DP
      • 解法2——二维DP⭐
  • 2320. 统计放置房子的方式数
    • 代码1
    • 代码2——变量代替dp数组
    • 代码3——static代码块预处理
  • 2321. 拼接数组的最大分数
    • 转换成最大子数组和
  • LCP 53. 守护太空城(子集状压DP)⭐⭐⭐⭐⭐🚹🚹🚹🚹🚹

https://leetcode.cn/circle/discuss/G0n5iY/

2140. 解决智力问题

2140. 解决智力问题
在这里插入图片描述

提示:

1 <= questions.length <= 10^5
questions[i].length == 2
1 <= pointsi, brainpoweri <= 10^5

解法1——倒序DP(填表法)

填表法适用于大多数 DP:通过当前状态所依赖的状态,来计算当前状态。

由于选择 i 时需要跳过后面的一部分,因此我们想要知道后面被选择的情况,所以倒序遍历会更加方便。

定义 f[i] 表示解决区间 [i,n−1] 内的问题可以获得的最高分数

class Solution {
    public long mostPoints(int[][] questions) {
        int n = questions.length;
        long[] dp = new long[n];
        dp[n - 1] = questions[n - 1][0];    // dp[i]表示从i~n-1任意选择时的最大值
        for (int i = n - 2; i >= 0; --i) {
            // 计算选i的情况
            dp[i] = questions[i][0];
            if (i + questions[i][1] + 1 < n) dp[i] += dp[i + questions[i][1] + 1];
            // 与不选i的情况取最大值
            dp[i] = Math.max(dp[i], dp[i + 1]);
        }
        return dp[0];
    }
}

解法2——正序DP(刷表法)⭐⭐⭐

定义 f[i] 表示解决区间 [0,i) 内的问题可以获得的最高分数
在这里插入图片描述

class Solution {
    public long mostPoints(int[][] questions) {
        int n = questions.length;
        long[] dp = new long[n + 1];
        for (int i = 0; i < n; ++i) {
            // 不选i
            dp[i + 1] = Math.max(dp[i + 1], dp[i]);
            // 选i
            int j = Math.min(n, i + questions[i][1] + 1);
            dp[j] = Math.max(dp[j], dp[i] + questions[i][0]);
        }
        return dp[n];
    }
}

居然还可以这样 dp?
枚举到 i 的时候可以不是更新 dp[i],而是更新和它相关的另外一些位置

2167. 移除所有载有违禁货物车厢所需的最少时间⭐⭐⭐

2167. 移除所有载有违禁货物车厢所需的最少时间
在这里插入图片描述

解法1——前缀和⭐⭐⭐⭐⭐

https://leetcode.cn/problems/minimum-time-to-remove-all-cars-containing-illegal-goods/solutions/1249244/yi-chu-suo-you-zai-you-wei-jin-huo-wu-ch-qinx/

在这里插入图片描述

将求最后一个公式最小值的过程翻译成代码如下:

class Solution {
    public int minimumTime(String s) {
        int n = s.length(), preBest = 0, preSum = 0, ans = Integer.MAX_VALUE;
        for (int j = 0; j < n; ++j) {
            preBest = Math.min(preBest, j - 2 * preSum);
            preSum += (s.charAt(j) - '0');
            ans = Math.min(ans, preBest + 2 * preSum - j);
        }
        return ans + n - 1;
    }
}

解法2——前后缀分解 + 动态规划

在这里插入图片描述

代码1——看了思路之后自己写的

class Solution {
    public int minimumTime(String s) {
        int n = s.length();
        int[] dp1 = new int[n], dp2 = new int[n];
        dp1[0] = s.charAt(0) == '1'? 1: 0;
        dp2[n - 1] = s.charAt(n - 1) == '1'? 1: 0;
        for (int i = 1; i < n; ++i) {
            if (s.charAt(i) == '0') dp1[i] = dp1[i - 1];
            else dp1[i] = Math.min(dp1[i - 1] + 2, i + 1);
        }
        int ans = dp1[n - 1];
        for (int i = n - 2; i >= 0; --i) {
            if (s.charAt(i) == '0') dp2[i] = dp2[i + 1];
            else dp2[i] = Math.min(dp2[i + 1] + 2, n - i);
            ans = Math.min(ans, (i - 1 >= 0? dp1[i - 1]: 0) + dp2[i]);
        }
        return ans;
    }
}

代码2——代码1的优化(一次遍历)⭐

class Solution {
    public int minimumTime(String s) {
        int n = s.length();
        int ans = n, pre = 0;
        for (int i = 0; i < n; ++i) {
            if (s.charAt(i) == '1') pre = Math.min(pre + 2, i + 1);
            ans = Math.min(ans, pre + n - 1 - i);
        }
        return ans;
    }
}

2172. 数组的最大与和(状态压缩DP)⭐⭐⭐⭐⭐

2172. 数组的最大与和
在这里插入图片描述

思路

在这里插入图片描述
注意这里 空蓝子的位置 j,对应的编号是 j / 2 + 1。
即位置 0, 1, 2, 3, 4, 5, 6, 7 会被映射成 1, 1, 2, 2, 3, 4, 4, 4。

代码

class Solution {
    public int maximumANDSum(int[] nums, int numSlots) {
        int n = nums.length, ans = 0;
        int[] dp = new int[1 << (numSlots * 2)];
        for (int i = 0; i < dp.length; ++i) {
            int c = Integer.bitCount(i);    // 1的个数,即已经放进篮子的数量
            if (c >= n) continue;
            for (int j = 0; j < numSlots * 2; ++j) {    // 枚举每个篮子(尝试是空蓝子的话放入nums[c])
                if ((i & (1 << j)) == 0) {              // 如果是空蓝子的话
                    int s = i | (1 << j);               // 在i的基础上放入j篮子
                    dp[s] = Math.max(dp[s], dp[i] + ((j / 2 + 1) & nums[c]));
                    ans = Math.max(ans, dp[s]);
                }
            }
        }   
        return ans;
    }
}

在循环 j 的过程中会尝试在每一个空蓝子中放入 nums[c]。

补充:相似题目——1879. 两个数组最小的异或值之和⭐⭐⭐

https://leetcode.cn/problems/minimum-xor-sum-of-two-arrays/
在这里插入图片描述

class Solution {
    public int minimumXORSum(int[] nums1, int[] nums2) {
        int n = nums1.length;
        int[] dp = new int[1 << n];
        Arrays.fill(dp, (int)2e9);
        dp[0] = 0;
        for (int mask = 1; mask < dp.length; ++mask) {
            int c = Integer.bitCount(mask);
            for (int j = 0; j < n; ++j) {
                if ((mask >> j & 1) == 1) {     // 检查这一位是否已经被设置了
                    // 如果已经被设置了,那就从没有被设置的状态转移过来
                    dp[mask] = Math.min(dp[mask], dp[mask ^ (1 << j)] + (nums1[c - 1] ^ nums2[j]));
                }
            }
        }
        return dp[dp.length - 1];
    }
}

代码中通过 mask ^ (1 << j) 将第 j 位的 1 去掉。

2188. 完成比赛的最少时间⭐⭐⭐⭐⭐

2188. 完成比赛的最少时间

在这里插入图片描述

提示:

1 <= tires.length <= 10^5
tires[i].length == 2
1 <= fi, changeTime <= 10^5
2 <= ri <= 10^5
1 <= numLaps <= 1000

思路——结合性质巧妙线性DP (预处理每种圈数的最短时间 + 动态规划)

https://leetcode.cn/problems/minimum-time-to-finish-the-race/solutions/1295939/jie-he-xing-zhi-qiao-miao-dp-by-endlessc-b963/
在这里插入图片描述

代码

class Solution {
    public int minimumFinishTime(int[][] tires, int changeTime, int numLaps) {
        // minSec[i]表示连续使用同一个轮胎跑x圈的最小耗时
        int[] minSec = new int[18];     // 考虑题目数据范围,最多17圈就要换轮胎
        Arrays.fill(minSec, Integer.MAX_VALUE / 2);
        for (int[] tire: tires) {
            long f = tire[0], r = tire[1];
            for (int x = 1, sum = 0; f <= changeTime + tire[0]; ++x) {
                sum += f;
                minSec[x] = Math.min(minSec[x], sum);
                f *= r;     // 更新下一圈的花费
            }
        }
		
		// 动态规划
        int[] dp = new int[numLaps + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = -changeTime;	// 初始化值 方便后面的循环
        for (int i = 1; i <= numLaps; ++i) {
            for (int j = 1; j <= Math.min(17, i); ++j) {
                // i从i-j转移过来
                dp[i] = Math.min(dp[i], dp[i - j] + minSec[j]);
            }
            dp[i] += changeTime;
        }
        return dp[numLaps];
    }
}

2209. 用地毯覆盖后的最少白色砖块⭐⭐⭐⭐⭐

2209. 用地毯覆盖后的最少白色砖块
在这里插入图片描述

提示:

1 <= carpetLen <= floor.length <= 1000
floor[i] 要么是 '0' ,要么是 '1' 。
1 <= numCarpets <= 1000

思路——考虑是否使用第i条地毯且其末尾覆盖第j块板砖

在这里插入图片描述

代码

class Solution {
    public int minimumWhiteTiles(String floor, int numCarpets, int carpetLen) {
        int m = floor.length();
        if (numCarpets * carpetLen >= m) return 0;  // 全都能覆盖
        int[][] dp = new int[numCarpets + 1][m];    // 用前i个地毯覆盖前j个格子时,保留的最少白色砖块
        dp[0][0] = floor.charAt(0) % 2;             // 第0个地毯不能使用,即不能覆盖

        for (int i = 1; i < m; ++i) {
            dp[0][i] = dp[0][i - 1] + floor.charAt(i) % 2;  // 类似求前缀和的过程
        }
        for (int i = 1; i <= numCarpets; ++i) {             // 地毯
            for (int j = carpetLen * i; j < m; ++j) {       // 枚举格子
                // 不放在j或者放在j
                dp[i][j] = Math.min(dp[i][j - 1] + floor.charAt(j) % 2, dp[i - 1][j - carpetLen]);
            }
        }
        return dp[numCarpets][m - 1];
    }
}

2218. 从栈中取出 K 个硬币的最大面值和(分组背包)🐂🐂🐂

2218. 从栈中取出 K 个硬币的最大面值和
在这里插入图片描述

将问题转化成分组背包,每一个栈为一组。
每个组只能取出一个元素块,一个元素块即为栈顶的若干个元素。

class Solution {
    public int maxValueOfCoins(List<List<Integer>> piles, int k) {
        int n = piles.size();   // 有n个组
        int[] dp = new int[k + 1];
        for (List<Integer> pile: piles) {
            for (int i = 1; i < pile.size(); ++i) {
                // 将元素的价值修改为前缀和
                pile.set(i, pile.get(i - 1) + pile.get(i));
            }
        }

        for (int x = 0; x < n; ++x) {       // 循环每一组
            for (int i = k; i >= 1; --i) {  // 循环背包容量
                for (int j = 1; j <= piles.get(x).size(); j++) {     // 循环该组的每一个物品
                    if (i >= j) {
                        dp[i] = Math.max(dp[i], dp[i - j] + piles.get(x).get(j - 1));
                    }
                }
            }
        }
        return dp[k];
    }
}

分组DP模板

for (int k = 1; k <= ts; k++)           // 循环每一组
  for (int i = m; i >= 0; i--) // 循环背包容量
    for (int j = 1; j <= cnt[k]; j++)   // 循环该组的每一个物品
      if (i >= w[t[k][j]])  // 背包容量充足
        dp[i] = max(dp[i], dp[i - w[t[k][j]]] + c[t[k][j]]);  // 像0-1背包一样状态转移

资料来源:https://oi-wiki.org/dp/knapsack/#%E5%88%86%E7%BB%84%E8%83%8C%E5%8C%85

2246. 相邻字符不同的最长路径(树形DP)

2246. 相邻字符不同的最长路径
在这里插入图片描述

思路——树形DP

关于树形DP可见:
【算法】树形DP ①(树的直径)
【算法】树形DP ② 打家劫舍Ⅲ(树上最大独立集)

一道典型的树形DP,要求相邻节点不能相同。

这里的路径长度定义就是路径上节点的数量。

代码

下面这种代码风格适用于这种每个节点可能有多个孩子的树。

class Solution {
    List<Integer>[] g;
    char[] s;
    int ans = 1;

    public int longestPath(int[] parent, String s) {
        int n = parent.length;
        g = new ArrayList[n];
        Arrays.setAll(g, e -> new ArrayList());
        for (int i = 1; i < n; ++i) {
            g[parent[i]].add(i);
        }
        this.s = s.toCharArray();
        dfs(0, -1);
        return ans;
    }

    public int dfs(int x, int fa) {
        int mxL = 1;    // 这个节点往下的最长路径
        for (int child: g[x]) {
            if (child == fa) continue;
            int len = dfs(child, x);
            if (s[x] != s[child]) {
                ans = Math.max(ans, mxL + len); // 更新答案
                mxL = Math.max(mxL, len + 1);   // 更新当前往下的最长路径
            }
        }
        return mxL;     // 返回值是往下的最长路径
    }
}

2262. 字符串的总引力⭐⭐⭐⭐⭐

2262. 字符串的总引力
在这里插入图片描述

提示:
1 <= s.length <= 10^5
s 由小写英文字母组成

思路——记录各个字母上次出现的位置,考虑增加的引力值

从左往右遍历,考虑将 s[i] 加到 s[i - 1] 末尾之后,以 s[i] 为末尾的字符的引力值在 以 s[i - 1] 为末尾的字符串的引力值的基础上增加了多少。

  • 如果 s[i] 的字符在此前都没有出现过,那么引力值会增加 i。
  • 如果出现过且下标为 j,那么引力值会增加 i - j。

代码

class Solution {
    public long appealSum(String s) {
        long ans = 0;
        int[] last = new int[26];
        Arrays.fill(last, -1);      // 记录各个字母上次出现的位置
        for (int i = 0, sumG = 0; i < s.length(); ++i) {
            int ch = s.charAt(i) - 'a';
            sumG += i - last[ch];	// i - last[ch]是增加的引力值
            ans += sumG;			
            last[ch] = i;
        }
        return ans;
    }
}

2266. 统计打字方案数

2266. 统计打字方案数
在这里插入图片描述

分组 + 线性DP + 乘法原理

把相同字符分为一组,每组内只有一种字符。

计算各组可能的方案,最后将各组方案相乘即可。

class Solution {
    final static int N = (int)1e5;
    final static long mod = (int)1e9 + 7;
    static long[] dp1 = new long[N], dp2 = new long[N];

    static {
        dp1[0] = dp2[0] = 1;
        dp1[1] = dp2[1] = 2;
        dp1[2] = dp2[2] = 4;
        dp1[3]= 7;
        dp2[3] = 8;
        for (int i = 4; i < N; ++i) {
            dp1[i] = (dp1[i - 1] + dp1[i - 2] + dp1[i - 3]) % mod;
            dp2[i] = (dp2[i - 1] + dp2[i - 2] + dp2[i - 3] + dp2[i - 4]) % mod;
        }
    }

    public int countTexts(String pressedKeys) {
        int n = pressedKeys.length();
        long ans = 1;
        for (int l = 0, r = 0; l < n; ++l) {
            char ch = pressedKeys.charAt(l);
            while (r < n && pressedKeys.charAt(r) == ch) r++;
            int len = r - l;
            if (ch == '7' || ch == '9') ans = (ans * dp2[len - 1]) % mod;
            else ans = (ans * dp1[len - 1]) % mod;
            l = r - 1;
        }
        return (int)ans;
    }
}

2272. 最大波动的子字符串⭐⭐⭐⭐⭐

2272. 最大波动的子字符串

在这里插入图片描述
提示:
1 <= s.length <= 10^4
s 只包含小写英文字母。

思路——枚举最多和最少的字符+最大子数组和

从 26 个字母中选出 2 个字母,分别作为最大值和最小值,一共需要枚举 A 26 2 = 26 × 25 = 650 A_{26}^{2} = 26 × 25 = 650 A262=26×25=650 种不同的字母组合。

对于每种组合,操作类似 求最大子数组和。(但是要求必须两种字母都要出现)。

在这里插入图片描述

代码

class Solution {
    public int largestVariance(String s) {
        int n = s.length(), ans = 0;
        for (char a = 'a'; a <= 'z'; ++a) {
            for (char b = 'a'; b <= 'z'; ++b) {
                if (a == b) continue;
                // diff维护a和b之差 diffWithB维护包含了b的a和b之差
                int diff = 0, diffWithB = -s.length();
                // a作为最大值 b作为最小值时的答案
                for (int i = 0; i < n; ++i) {
                    if (s.charAt(i) == a) {
                        ++diff;
                        ++diffWithB;
                    } else if (s.charAt(i) == b) {
                        diffWithB = --diff;
                        diff = Math.max(diff, 0);
                    }
                }
                ans = Math.max(ans, diffWithB);
            }
        }
        return ans;
    }
}

这里 diff 维护 a 和 b 之差, diffWithB 维护包含了 b 的 a 和 b 之差。

初始化时 diffWithB 设置成了一个很小的负值,所以就算跟着 diff 一直增加,如果 b 不出现的话,diffWithB 也不会更新成 --diff,也就不会影响答案的最大值了。

2305. 公平分发饼干(子集状态压缩DP)

2305. 公平分发饼干
在这里插入图片描述
提示:

2 <= cookies.length <= 8
1 <= cookies[i] <= 10^5
2 <= k <= cookies.length

解法1——dfs回溯+剪枝

看到数据范围很小只有 8,可以先尝试一下暴力一点的做法。
比如尝试每一种分配的情况,使用每一种情况的最大值更新当前的答案。

class Solution {
    int[] sum, cookies;
    int ans = Integer.MAX_VALUE, k;

    public int distributeCookies(int[] cookies, int k) {
        this.cookies = cookies;
        this.k = k;
        sum = new int[k];
        dfs(0);
        return ans;
    }

    public void dfs(int i) {
        if (i == cookies.length) {
            // 更新答案
            ans = Math.min(ans, Arrays.stream(sum).max().getAsInt());
            return;
        }   
        for (int j = 0; j < k; ++j) {
            if (sum[j] + cookies[i] >= ans) continue;   // 剪枝
            sum[j] += cookies[i];
            dfs(i + 1);
            sum[j] -= cookies[i];
        }
    }
}

但是如果真的是纯暴力的话还是会超时,因此加了一个剪枝,就是在枚举分配情况的过程中如果检测到当前的值已经大于答案 ans了,那么就没有必要再继续 dfs 下去了,因为它一定不会影响到答案了。

除此之外,还可以先对 cookies 排序,在回溯的过程中先放入比较大的饼干,这样更容易触发剪枝的条件。

解法2——子集状压DP⭐⭐⭐⭐⭐(很重要!值得一学)

dp数组的定义
dp[i][j] 表示将集合 j 分成 i 个集合时,这些集合的元素和的最大值的最小值是多少。

dp数组的递推
考虑 dp[i][j] 如何转移出来,
此时已经组成了 i 个集合,那么考虑它可以从 i - 1 个集合的形式中转移出来
dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][j ^ s], sum[s]))
这里的 Math.max(dp[i - 1][j ^ s], sum[s])) 即在求这种分集合的方式时,各个集合元素和的最大值。
而我们需要求的是各种分法中得出的这些最大值里面的最小值是多少。

class Solution {
    public int distributeCookies(int[] cookies, int k) {
        // 答案的顺序和输入的顺序无关
        // 有消耗的概念 集合的划分
        // 状压DP

        // f[i][j] 消耗了 k 个子序列,这些子序列组成了集合 j
        // 这 k 个子序列的元素和的最大值的最小值为 f[i][j]
        
        // f[i][j] = 枚举 j 的子集 s
        // min max(f[i - 1][j ^ s], sum[s]) for s in j

        int n = cookies.length;
        int[] sum = new int[1<<n];    // 记录各个子集的和
        for (int i = 1; i < 1<<n; ++i) {    // 枚举每个子集
            for (int j = 0; j < n; ++j) {   
            	// 检查这个子集中是否有cookies[j]
                if ((i >> j & 1) == 1) sum[i] += cookies[j];
            }
        }

        int[][] dp = new int[k][1<<n];
        dp[0] = sum;                    // 只消耗了一个序列 相当于它本身
        for (int i = 1; i < k; ++i) {   // 计算分成i个子序列的答案
            for (int mask = 1; mask < 1<<n; ++mask) {
                dp[i][mask] = 0x3f3f3f3f;
                // 枚举mask的所有子集
                for (int s = mask; s != 0; s = (s - 1) & mask) {    // &mask 保证了是mask的子集
                    dp[i][mask] = Math.min(dp[i][mask], Math.max(dp[i - 1][mask ^ s], sum[s]));    // 相当于分走了一个子集s给新的序列
                }
            }
        }
        // 表示k个子集组成了这个大子集
        return dp[k - 1][(1<<n) - 1];
    }
}

代码技巧——如何枚举一个集合的所有子集⭐🐂

在这道题目中是在枚举 mask 的所有子集 s。
代码体现为:

// &mask 保证了是mask的子集
for (int s = mask; s != 0; s = (s - 1) & mask) {    

}

令 s 从 mask 开始,不断减小,同时将其与 mask 进行 & 运算,使其保证是 mask 的一个子集。

这种方法可以保证 s 作为 mask 的子集 不重不漏

代码的空间优化

由于 dp[i] 只会从 dp[i - 1] 转移过来,因此可以删去第一个维度。

同时倒着枚举 mask。

修改之后的代码如下:

class Solution {
    public int distributeCookies(int[] cookies, int k) {
        // 答案的顺序和输入的顺序无关
        // 有消耗的概念 集合的划分
        // 状压DP

        // f[i][j] 消耗了 k 个子序列,这些子序列组成了集合 j
        // 这 k 个子序列的元素和的最大值的最小值为 f[i][j]
        
        // f[i][j] = 枚举 j 的子集 s
        // min max(f[i - 1][j ^ s], sum[s]) for s in j

        int n = cookies.length;
        int[] sum = new int[1<<n];    // 记录各个子集的和
        for (int i = 1; i < 1<<n; ++i) {    // 枚举每个子集
            for (int j = 0; j < n; ++j) {   // 检查这个子集中是否有cookies[j]
                if ((i >> j & 1) == 1) sum[i] += cookies[j];
            }
        }

        int[] dp = Arrays.copyOf(sum, 1 << n);
        for (int i = 1; i < k; ++i) {   // 计算分成i个子序列的答案
            for (int mask = (1 << n) - 1; mask >= 1; --mask) {
                // 枚举mask的所有子集
                for (int s = mask; s != 0; s = (s - 1) & mask) {    // &mask 保证了是mask的子集
                    dp[mask] = Math.min(dp[mask], Math.max(dp[mask ^ s], sum[s]));    // 相当于分走了一个子集s给新的序列
                }
            }
        }
        // 表示k个子集组成了这个大子集
        return dp[(1<<n) - 1];
    }
}

至于为什么要倒着枚举 mask,是因为它会在更小的 mask 转移过来,所以我们不能在使用其之前先将其覆盖了。

补充:相似题目练习——1723. 完成所有工作的最短时间

https://leetcode.cn/problems/find-minimum-time-to-finish-all-jobs/
在这里插入图片描述

class Solution {
    public int minimumTimeRequired(int[] jobs, int k) {
        int n = jobs.length;
        // dp[i][j]表示将集合j分成i个子集时最小的最大花费
        int[][] dp = new int[k][1 << n];

        // 计算各个集合对应的工作时间和
        int[] sum = new int[1 << n];
        for (int i = 1; i < 1<<n; ++i) {
            for (int j = 0; j < n; ++j) {
                if ((i >> j & 1) == 1) sum[i] += jobs[j];
            }
        }

        dp[0] = sum;    // 就是原数组作为一个集合
        for (int i = 1; i < k; ++i) {   // 枚举子集合数量
            for (int j = 1; j < 1 << n; ++j) {
                dp[i][j] = 0x3f3f3f3f;
                for (int s = j; s != 0; s = (s - 1) & j) {
                    dp[i][j] = Math.min(dp[i][j], Math.max(dp[i - 1][j ^ s], sum[s]));
                }
            }
        }

        return dp[k - 1][(1<<n) - 1];
    }
}

可以说跟上面那道题目是一模一样。

2312. 卖木头块⭐⭐⭐

2312. 卖木头块
在这里插入图片描述

提示:
1 <= m, n <= 200
1 <= prices.length <= 2 * 10^4
prices[i].length == 3
1 <= hi <= m
1 <= wi <= n
1 <= pricei <= 10^6
所有 (hi, wi) 互不相同 。

解法1——记忆化搜索

dp[i][j] 表示一个 i * j 的木块可以获得的最多钱数。

class Solution {
    long[][] dp;
    int[][] prices;
    Map<String, Integer> value = new HashMap();

    public long sellingWood(int m, int n, int[][] prices) {
        this.prices = prices;
        // dp[i][j] 表示一个 i * j 的木块可以获得的最多钱数
        dp = new long[m + 1][n + 1];
        for (int i = 0; i <= m; ++i) Arrays.fill(dp[i], -1);
        for (int[] p: prices) {
            value.put(p[0] + " " + p[1], p[2]);
        }
        return dfs(m, n);
    }

    public long dfs(int m, int n) {
        if (dp[m][n] != -1) return dp[m][n];
        long res = value.getOrDefault(m + " " + n, 0);
        for (int i = 1; i < m; ++i) res = Math.max(res, dfs(i, n) + dfs(m - i, n));
        for (int j = 1; j < n; ++j) res = Math.max(res, dfs(m, j) + dfs(m, n - j));
        return dp[m][n] = res;
    }
}

对于一块木头,我们可以选择横着将其切开或者竖着将其切开。

解法2——线性DP

使用 prices 对 dp 数组进行初始化。
由于 m 和 n 的数据范围是 200,因此可以使用三次循环。
关于数据范围可见:由数据范围反推算法复杂度以及算法内容

class Solution {
    public long sellingWood(int m, int n, int[][] prices) {
        long[][] dp = new long[m + 1][n + 1];
        for (int[] p: prices) dp[p[0]][p[1]] = p[2];
        for (int i = 1; i <= m; ++i) {
            for (int j = 1; j <= n; ++j) {
                for (int k = 1; k < i; ++k) dp[i][j] = Math.max(dp[i][j], dp[i - k][j] + dp[k][j]);
                for (int k = 1; k < j; ++k) dp[i][j] = Math.max(dp[i][j], dp[i][j - k] + dp[i][k]);
            }
        }
        return dp[m][n];
    }
}

2318. 不同骰子序列的数目

2318. 不同骰子序列的数目
在这里插入图片描述
提示:
1 <= n <= 10^4

解法1——三维DP

代码写起来很长,但是思路很清晰。

注意好 dp 数组的定义
dp[i][j][k] 表示 长度为 i,最后一个数字是 j ,倒数第二个数字是 k 的不同的序列个数

其中当前数字和上一个数字不能相同且最大公约数是 1, 当前数字和倒数第二个数字不能相同。

class Solution {
    static final int MOD = (int)1e9 + 7, MX = (int)1e4 + 1;
    static int[][][] dp = new int[MX][6][6];

    static {
        for (int i = 0; i < 6; ++i) {
            for (int j = 0; j < 6; ++j) {
                if (j != i && gcd(i + 1, j + 1) == 1) dp[2][i][j] = 1;
            }
        }

        for (int i = 3; i < MX; ++i) {             	// 枚举每个长度
            for (int j = 0; j < 6; ++j) {           // 枚举当前数字
                for (int k = 0; k < 6; ++k) {       // 枚举上一个数字
                    if (k != j && gcd(k + 1, j + 1) == 1) {     
                        for (int last = 0; last < 6; ++last) {    // 枚举上上个数字
                            if (last != j) {
                                dp[i][j][k] = (dp[i - 1][k][last] + dp[i][j][k]) % MOD;
                            }
                        }
                    }
                }
            }
        }
    }

    public int distinctSequences(int n) {
        if (n == 1) return 6;
        int ans = 0;
        for (int i = 0; i < 6; ++i) {
            for (int j = 0; j < 6; ++j) {
                ans = (ans + dp[n][i][j]) % MOD;
            }
        }
        return ans;
    }

    static int gcd(int a, int b) {
        return b == 0? a: gcd(b, a % b);
    }
}

解法2——二维DP⭐

TODO

在这里插入代码片

2320. 统计放置房子的方式数

2320. 统计放置房子的方式数
在这里插入图片描述
街道两侧的 dp 情况相同而又互不影响,只需计算其中一侧,最后结果是两边方案数的乘积。

代码1

class Solution {
    public int countHousePlacements(int n) {
        long[][] dp = new long[n][2];
        final long mod = (long)1e9 + 7;
        dp[0][0] = dp[0][1] = 1;
        for (int i = 1; i < n; ++i) {
        	// 这块不放,所以上块可以放也可以不放
            dp[i][0] = (dp[i - 1][0] + dp[i - 1][1]) % mod;
            // 这块放,所以上块不能放
            dp[i][1] = dp[i - 1][0];
        }
        long s = dp[n - 1][0] + dp[n - 1][1];
        return (int)(s * s % mod);
    }
}

代码2——变量代替dp数组

class Solution {
    public int countHousePlacements(int n) {
        long a = 1, b = 1;
        final long mod = (long)1e9 + 7;
        for (int i = 1; i < n; ++i) {
            long t = a;
            a = (a + b) % mod;
            b = t;
        }
        long s = a + b;
        return (int)(s * s % mod);
    }
}

代码3——static代码块预处理

class Solution {
    static final int mod = (int)1e9 + 7, N = (int)1e4 + 1;
    static final int[] dp = new int[N];

    static {
        dp[0] = 1;
        dp[1] = 2;
        for (int i = 2; i < N; ++i) dp[i] = (dp[i - 1] + dp[i - 2]) % mod;
    }

    public int countHousePlacements(int n) {
        return (int)((long)dp[n] * dp[n] % mod);
    }
}

2321. 拼接数组的最大分数

2321. 拼接数组的最大分数
在这里插入图片描述

转换成最大子数组和

转换成 53. 最大子数组和。
即计算两数组的差分数组的最大子数组和,即可找到可以选择的最佳 left 和 right 下标。

class Solution {
    public int maximumsSplicedArray(int[] nums1, int[] nums2) {
        int a = Arrays.stream(nums1).sum(), b = Arrays.stream(nums2).sum();
        return Math.max(a + op(nums1, nums2), b + op(nums2, nums1));
    }
	
	// op(a, b) 计算 b里面大的数字交给a
    public int op(int[] nums1, int[] nums2) {
        int ans = 0, n = nums1.length, sum = 0;
        for (int i = 0; i < n; ++i) {
            sum += nums2[i] - nums1[i];
            if (sum < 0) sum = 0;
            else ans = Math.max(ans, sum);
        }
        return ans;
    }
}

注意有可能是 1 换给 2 ,也有可能是 2 中大的元素换给 1。

LCP 53. 守护太空城(子集状压DP)⭐⭐⭐⭐⭐🚹🚹🚹🚹🚹

LCP 53. 守护太空城
在这里插入图片描述
https://leetcode.cn/problems/EJvmW4/solutions/1426981/by-endlesscheng-pk2q/

定义 dp[i][j] 表示考虑前 i 个舱室,且第 i 个舱室与第 i + 1 个舱室开启联合屏障的时间点集合为 j 时,所需的最小能量。

我们使用 union[i] 和 single[i] 分别记录开启 联合/单独 屏障的时间点集合恰好为 i 时,所需要的最少能量。

对于位置 0 ,联合保护罩的开启时间集合是 j ,则它的最小消耗就是 union[j] + single[((m - 1) ^ j) & rain[0]]。(即除去联合时间外,剩下且下雨的时间集合)

dp[i][j] 从 dp[i - 1][pre] 转移过来,其中 pre 是枚举 j 的补集。

class Solution {
    public int defendSpaceCity(int[] time, int[] position) {
        int n = Arrays.stream(position).max().getAsInt();
        int m = 1 << Arrays.stream(time).max().getAsInt();
        int[] rain = new int[n + 1];    // 记录每个位置下雨的时刻
        for (int i = 0; i < time.length; ++i) {
            rain[position[i]] |= 1 << (time[i] - 1);
        }

        // union和single分别表示开启时间点为j时所需的最小能量
        int[] union = new int[m], single = new int[m];
        for (int i = 1; i < m; ++i) {
            // j是去掉二进制最后一个1的i
            int lb = i & -i, j = i ^ lb, lb2 = j & -j;
            union[i] = union[j] + (lb == (lb2 >> 1)? 1: 3); // 检查i和j是否时间点相邻
            single[i] = single[j] + (lb == (lb2 >> 1)? 1: 2);
        }

        // dp[i][j] 表示考虑前 i 个舱室,且第 i 个舱室与第 i + 1 个舱室开启联合屏障的时间点集合为 j 时,所需的最小能量。
        int[][] dp = new int[n + 1][m];
        for (int j = 0; j < m; ++j) {
            dp[0][j] = union[j] + single[((m - 1) ^ j) & rain[0]];
        }
        for (int i = 1; i <= n; ++i) {
            Arrays.fill(dp[i], Integer.MAX_VALUE / 2);
            for (int j = 0; j < m; ++j) {	// 枚举位置i在时间集合j开启联合保护罩
                // 枚举 j 的补集 mask 中的子集 pre (即与j不重叠的所有其它时间集合pre)
                for (int mask = (m - 1) ^ j, pre = mask; ; pre = (pre - 1) & mask) {
                    int cost = dp[i - 1][pre] + union[j] + single[(mask ^ pre) & rain[i]];
                    dp[i][j] = Math.min(dp[i][j], cost);
                    if (pre == 0) break;	// 注意必须写在这里,不能在if里写pre != 0
                }
            }
        }
        return dp[n][0];
    }
}

DP 是真难呐!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/750734.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

java中json和对象之间相互转换的运用

1.目录结构 2.配置相关文件 2.1.引入相关的pom文件 pom.xml <dependencies><!-- JSON --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.12.3</vers…

linux 基于debian_ubuntu AB系统适配(三)- overlayroot

Overlayroot Overlayroot是一个实用工具,允许您创建一个只读的根文件系统和一个可写的覆盖文件系统。这对于创建一个更安全和稳定的系统很有用,因为对系统所做的任何更改都将存储在覆盖文件系统中,可以很容易地丢弃或重置。 在Debian下,分离的系统在/userdata/rootfs_ove…

浅谈OS命令注入漏洞(Shell注入漏洞)

一、什么是OS命令注入&#xff1f; 1. 基本概念 OS&#xff08;Operating system&#xff09;命令注入&#xff08;也称为 Shell 注入&#xff09;是一个 Web 安全漏洞&#xff0c;允许攻击者在运行应用程序的服务器上执行任意操作系统 &#xff08;OS&#xff09; 命令&#…

考虑充电负荷空间可调度特性的分布式电源与电动汽车充电站联合配置方法(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

DaVinci Resolve Studio 18对Mac和Windows系统的要求

DaVinci Resolve Studio 18 是一款功能强大的专业视频编辑和调色软件&#xff0c;它提供了全面的工具和功能&#xff0c;让用户能够完成从剪辑、调色到特效和音频处理等各个方面的任务。DaVinci Resolve Studio 18 在中文界面上进行了优化&#xff0c;使得中文用户能够更加方便…

【xxl-job】分布式任务调度系统xxl-job搭建

XXL-JOB是一个轻量级分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展、开箱即用。 更多介绍&#xff0c;请访问官网&#xff1a;分布式任务调度平台XXL-JOB 一、任务调度中心(基于docker)【Version 2.4.0】 前提条件&#xff1a;任务调度…

Vue3 Vite electron 开发桌面程序

Electron是一个跨平台的桌面应用程序开发框架&#xff0c;它允许开发人员使用Web技术&#xff08;如HTML、CSS和JavaScript&#xff09;构建桌面应用程序&#xff0c;这些应用程序可以在Windows、macOS和Linux等操作系统上运行。 Electron的核心是Chromium浏览器内核和Node.js…

个体化治疗策略:如何使用机器学习定制化药物?

一、引言 个体化治疗策略是一种基于患者个体特征和病情的定制化治疗方法&#xff0c;旨在提高治疗效果、减少药物副作用并优化患者的生命质量。传统的治疗方法往往采用标准化的治疗方案&#xff0c;忽视了个体差异和患者特定的需求。然而&#xff0c;每个患者的基因组、疾病特征…

JAVA——二维数组遍历二维数组的三种方法

目录 &#x1f352;java中二维数组的定义和赋值 &#x1f352;二维数组遍历的三种方法 &#x1f347;第一种&#xff1a;for循环遍历 &#x1f347;第二种方法&#xff1a;通过Arrays.deepToString()遍历 &#x1f347;第三种方法&#xff1a;通过for&#xff08; : &a…

七大排序算法——希尔排序,通俗易懂的思路讲解与图解(完整Java代码)

文章目录 一、排序的概念排序的概念排序的稳定性七大排序算法 二、希尔排序核心思想代码实现 三、性能分析四、七大排序算法性能对比 一、排序的概念 排序的概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0…

自动化测试与持续集成方案

目录 前言&#xff1a; 传统接口测试 接口测试自动化 接口自动化的持续集成 前言&#xff1a; 自动化测试和持续集成是软件开发过程中非常重要的环节。自动化测试可以提高测试效率和准确性&#xff0c;而持续集成则可以确保代码的稳定性和可靠性。 传统接口测试 不知道别…

使用WiFi测量仪进行机器人定位的粒子过滤器研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

50. Pow(x, n) 快速幂

题目链接&#xff1a;力扣 解题思路&#xff1a;直接使用for进行累乘会超时&#xff08;时间复杂度O(n)&#xff0c;n为指数n的大小&#xff09;&#xff0c;可以使用快速幂进行更快的幂运算(时间复杂度为O(logn)) 快速幂&#xff1a;核心思想就是每一次把指数缩小一半&#x…

华南农业大学|图像处理与分析技术综合设计|题目解答:读取电表示数

l 设计任务&#xff1a; ipa05.jpg是一幅电气柜上的电表图像&#xff0c;试采用图像处理与分析技术&#xff0c;设计适当的 算法和程序&#xff0c;找出电流表所在的区域&#xff0c;提取其指针位置&#xff0c;计算指针与表盘下沿 的夹角&#xff0c;进而判断当前电表的读数…

车载软件架构——车载诊断软件框架

我是穿拖鞋的汉子&#xff0c;魔都中坚持长期主义的汽车电子工程师。 老规矩&#xff0c;分享一段喜欢的文字&#xff0c;避免自己成为高知识低文化的工程师&#xff1a; 人们会在生活中不断攻击你。他们的主要武器是向你灌输对自己的怀疑&#xff1a;你的价值、你的能力、你的…

vue2+AntdesignVue a-input使用颜色选择器

不需要任何插件即可实现颜色选择器 a-input type设置为color即可 回调函数

vue进阶-消息的订阅与发布

&#x1f4d6;vue基础学习-组件 介绍了嵌套组件间父子组件通过 props 属性进行传参。子组件传递数据给父组件通过 $emit() 返回自定义事件&#xff0c;父组件调用自定义事件接收子组件返回参数。 &#x1f4d6;vue进阶-vue-route 介绍了路由组件传参&#xff0c;两种方式&…

Spring MVC异步上传、跨服务器上传和文件下载

一、异步上传 之前的上传方案&#xff0c;在上传成功后都会跳转页面。而在实际开发中&#xff0c;很多情况下上传后不进行跳转&#xff0c;而是进行页面的局部刷新&#xff0c;比如&#xff1a;上传头像成功后将头像显示在网页中。这时候就需要使用异步文件上传。 1.1 JSP页面 …

PT:report_timing实用技巧

report_timing -start_end_pair 默认report_timing -to 会报告到endpoint最差一条violation path。 用report_timing -max_path X就会报告到endpoint的X条path&#xff0c;每组startpoint /endpoint只报告最差的一条(在X范围内有多少报多少&#xff0c;下面同理)。 用report…

Android 生成pdf文件

Android 生成pdf文件 1.使用官方的方式 使用官方的方式也就是PdfDocument类的使用 1.1 基本使用 /**** 将tv内容写入到pdf文件*/RequiresApi(api Build.VERSION_CODES.KITKAT)private void newPdf() {// 创建一个PDF文本对象PdfDocument document new PdfDocument();//创建…