问题链接
样例解释
看完题目后,只有样例一有输入输出的解释,我们可以让 MarsCode 顺便解释下其他两个样例:
以样例 2 为例
输入:
n = 5, m = 3, s = 2, a = [2, 3, 1, 4, 6]
输出:17输入输出解释如下:
我们尝试将前两个水果(2, 3)和后三个水果(1, 4, 6)分别装成果篮:
-
将前两个水果(2, 3)装成一个果篮。
- 最大体积
u = 3 - 最小体积
v = 2 - 水果数量
k = 2 - 成本计算:
k * ⌊(u + v) / 2⌋ + s = 2 * ⌊(3 + 2) / 2⌋ + 2 = 2 * 2 + 2 = 6
- 最大体积
-
将后三个水果(1, 4, 6)装成另一个果篮。
- 最大体积
u = 6 - 最小体积
v = 1 - 水果数量
k = 3 - 成本计算:
k * ⌊(u + v) / 2⌋ + s = 3 * ⌊(6 + 1) / 2⌋ + 2 = 3 * 3 + 2 = 11
- 最大体积
总成本:6 + 11 = 17
给点思路提示
样例现在是看懂了,但好像还是没什么思路,隐隐感觉可能是动态规划的题,那现在只好寻求 MarsCode 给点思路提示。
这里摘取最核心的部分:
思路整理
状态定义
定义dp[i]为前i个水果打包成若干果篮的最小总成本
状态转移
由于要求果篮里的水果必须是连续的,因此我们可以假设一个分割点j,从 0 到 j 的水果被打包成若干个果篮,从 j+1 到 i 的水果打包成一个果篮。这样我们就可以实现从n到n + 1的状态转移,目标转化为找到那个最佳分割点 j。
状态转移方程如下:dp[i] = min(dp[i], dp[j] + cost(j + 1, i))
i - j <= m:果篮里的水果数不能超过 mcost(j + 1, i)表示从j+1到i的水果打包成一个果篮的成本
边界条件
dp[0] = 0,即没有水果时的成本为 0
最终结果
dp[n]
初版代码
public class Main {
private static int cost(int left, int right, int s, int[] a) {
int minVal = a[left], maxVal = a[right];
for(int i = left; i <= right; i++) {
minVal = Math.min(minVal, a[i]);
maxVal = Math.max(maxVal, a[i]);
}
return (maxVal + minVal) / 2 * (right - left + 1) + s;
}
public static int solution(int n, int m, int s, int[] a) {
// write code here
int[] dp = new int[n + 1];
for(int i = 1; i <= n; i++) {
dp[i] = Integer.MAX_VALUE / 2;
}
for(int i = 0; i < n; i++) {
for(int j = i; j >= 0 && j >= i - m + 1; j--) {
dp[i + 1] = Math.min(dp[i + 1], dp[j] + cost(j, i, s, a));
}
}
return dp[n];
}
public static void main(String[] args) {
System.out.println(solution(6, 4, 3, new int[]{1, 4, 5, 1, 4, 1}) == 21);
System.out.println(solution(5, 3, 2, new int[]{2, 3, 1, 4, 6}) == 17);
System.out.println(solution(7, 4, 5, new int[]{3, 6, 2, 7, 1, 4, 5}) == 35);
}
}
没有优化,我们交一个试试:
em... 给通过了,本来想着超时然后让 AI 再帮我们优化一下,这下尴尬了...
没事,过了也能优化!
代码优化
其实我们可以发现,cost 计算部分,进行了很多冗余计算,我们试试 MarsCode 能给我们提供哪些优化建议
OK,这里的核心就是利用滑动窗口进行预处理,我们看下 MarsCode 的预处理部分实现:
// 预处理最大和最小体积
int[][] maxVolume = new int[n][n];
int[][] minVolume = new int[n][n];
for (int i = 0; i < n; i++) {
maxVolume[i][i] = a[i];
minVolume[i][i] = a[i];
for (int j = i + 1; j < n; j++) {
maxVolume[i][j] = Math.max(maxVolume[i][j - 1], a[j]);
minVolume[i][j] = Math.min(minVolume[i][j - 1], a[j]);
}
}
但我们其实可以发现,窗口大小完全没必要设置为 n,因为果篮最大容积是 m,我们只需要计算到 m 即可。
那这里我们就不浪费 API 重新生成了,直接开始上手!!!
最终的代码如下:
public class Main {
public static int solution(int n, int m, int s, int[] a) {
// write code here
int[] dp = new int[n + 1];
// 预处理最大和最小体积
int[][] maxVolume = new int[n][n];
int[][] minVolume = new int[n][n];
for (int i = 0; i < n; i++) {
maxVolume[i][i] = a[i];
minVolume[i][i] = a[i];
for (int j = i + 1; j < n && j < i + m; j++) {
maxVolume[i][j] = Math.max(maxVolume[i][j - 1], a[j]);
minVolume[i][j] = Math.min(minVolume[i][j - 1], a[j]);
}
}
for(int i = 1; i <= n; i++) {
dp[i] = Integer.MAX_VALUE / 2;
}
for(int i = 0; i < n; i++) {
for(int j = i; j >= 0 && j >= i - m + 1; j--) {
int k = i - j + 1;
int cost = k * ((maxVolume[j][i] + minVolume[j][i]) / 2) + s;
dp[i + 1] = Math.min(dp[i + 1], dp[j] + cost);
}
}
return dp[n];
}
public static void main(String[] args) {
System.out.println(solution(6, 4, 3, new int[]{1, 4, 5, 1, 4, 1}) == 21);
System.out.println(solution(5, 3, 2, new int[]{2, 3, 1, 4, 6}) == 17);
System.out.println(solution(7, 4, 5, new int[]{3, 6, 2, 7, 1, 4, 5}) == 35);
}
}
总结
核心考点
最后,让 MarsCode 帮我们总结下这部分的核心考点:
这题的核心考点主要包括以下几个方面:
1. 动态规划(Dynamic Programming, DP)
- 状态定义:使用
dp[i]表示前i个水果打包成若干果篮的最小总成本。 - 状态转移:通过遍历所有可能的分组方式,计算每个分组的成本,并更新
dp[i]。 - 初始化:
dp[0] = 0,表示没有水果时的成本为0。
2. 预处理技巧
- 预处理最大和最小体积:通过预处理出每个位置的最大和最小体积,避免在每次计算
cost时重复遍历果篮中的所有水果。 - 二维数组:使用二维数组
maxVolume和minVolume来存储从i到j的最大和最小体积,提高计算效率。
3. 滑动窗口
- 滑动窗口技术:在计算
dp[i]时,使用滑动窗口技术来维护当前果篮的最大和最小体积,避免重复计算。
4. 成本计算公式
- 成本公式:理解并正确应用成本公式
cost = k * ⌊(u + v) / 2⌋ + s,其中k是果篮中水果的数量,u是果篮中水果的最大体积,v是果篮中水果的最小体积,s是一个常数。
5. 边界条件处理
- 边界条件:处理好边界条件,确保在计算
dp[i]时不会越界,并且正确处理只有一个水果的情况。
6. 优化思路
- 优化计算:通过预处理和滑动窗口技术,减少重复计算,提高代码的性能。
让 MarsCode 拷打下我们
MarsCode 拷打的内容太多了,我来简单总结一下:
- 对状态定义这一块还不能很好的进行切入。如果能想到正确的状态定义,后面的状态转移就显得很自然了。
- 代码细节方面:可以用
Arrays.fill(dp, Integer.MAX_VALUE)进行数组初始化
试试能不能让他给我推荐一些题
hhh,好像不大妙
最后
总体来说,有了 MarsCode 辅助刷题,确实是不用担心样例什么看不懂的情况,没有思路的话,也可以试着让 AI 提供点思路,刷题体验会好一点(就这,我还能再刷几道 Hard)。
就是有时候想让他提供一丢丢启发式的思路即可,他把完整思路 + 完整代码都提供了,感觉有点不大妙。