📅  最后修改于: 2023-12-03 15:12:37.789000             🧑  作者: Mango
本题是2021年计算机科学毕业资格考试(GATE CS 2021)第二部分的第16个问题。本题对编程语言的理解及对算法的实现有一定的要求。
给定一个包含n个正整数的数组,将其划分为m个非空连续子数组。定义每个子数组的值为其中的最小值。要求最大化这些子数组的值之和。
例如,对于数组 arr = [1, 2, 6, 5, 4, 3]
,将其划分为 4 个子数组,使得每个子数组中的元素都是连续的。其中的最小值为 [1], [2], [6], [5, 4, 3],最大化这些子数组的值之和为 1+2+6+5=14。
请实现一个函数 int max_sum(int *a, int n, int m) ,其中 a 是一个指向整型数组的指针,n 是数组的长度,m 是需要划分的子数组数量。
输入:
arr = [1, 2, 6, 5, 4, 3], m = 4
输出:
14
本问题与单调栈有关。我们用一个栈来存储数组元素的下标。对于前缀 [0, i],我们先用单调栈来求出当前元素 arr[i]
左边的第一个小于其值的下标 prev[i]
,记为 -1 代表不存在小于其值的元素。这样的话,对于右端点 i
,左端点只能在 [prev[i]+1, i]
区间内选择。这时我们需要用动态规划来维护答案。
设 dp[i][j]
表示将数组前 i
个元素划分为 j
段的最大子数组值之和,则有:
dp[i][j] = max(dp[k][j-1] + min{i, i-1, ..., k+1}*j) for k = prev[i]+1, ..., i-1
简单解释一下上述式子的含义:对于前 i
个数,划分为 j
段的最大子数组和,即为前 k
个数划分为 j-1
段的最大子数组和加上一个长度为 i-k
、每个元素值为 min{i, i-1, ..., k+}
的 子数组。其中 min{i, i-1, ..., k+}
可以在预处理时用单调栈求解。
最终的答案就是 dp[n][m]
。
由于单调栈只需要遍历一次数组,时间复杂度为 O(N);由于动态规划中 j
的取值范围为 1 到 m
,共需求解 m
次,因此时间复杂度为 O(Nm)。
空间复杂度为 O(Nm),与动态规划数组有关。
下面是C++语言实现的代码片段,实现方式为单调栈+动态规划:
int max_sum(int *arr, int n, int m) {
vector<int> left(n), right(n);
stack<int> stk;
for (int i = 0; i < n; ++i) {
while (!stk.empty() && arr[i] <= arr[stk.top()]) stk.pop();
left[i] = stk.empty() ? -1 : stk.top();
stk.push(i);
}
stk = stack<int>();
for (int i = n-1; i >= 0; --i) {
while (!stk.empty() && arr[i] <= arr[stk.top()]) stk.pop();
right[i] = stk.empty() ? n : stk.top();
stk.push(i);
}
vector<vector<int>> dp(n+1, vector<int>(m+1, 0));
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
if (j == 1) {
dp[i][j] = arr[left[i-1]+1] * i;
} else {
for (int k = left[i-1]+1; k < i; ++k) {
dp[i][j] = max(dp[i][j], dp[k][j-1] + (i-k)*arr[i-1]);
}
}
}
}
return dp[n][m];
}
需要注意到,本题的四个选项中没有答案与本题求解方法相符的,因此在实际实现时需要判断是否选项A、B、C、D 中有能对应解法的选项。