Hard
题目描述
给你一个长度为 n 的环形整数数组 nums。
如果索引 i 的值严格大于其相邻元素,则索引 i 是一个峰值:
i的前一个邻居是nums[i - 1](如果i > 0),否则是nums[n - 1]。i的后一个邻居是nums[i + 1](如果i < n - 1),否则是nums[0]。
你可以执行以下操作任意次数:
- 选择任意索引
i并将nums[i]增加 1。
返回使数组包含至少 k 个峰值所需的最少操作次数。如果不可能,返回 -1。
示例 1:
输入:nums = [2,1,2], k = 1
输出:1
解释:
为了实现至少 k = 1 个峰值,我们可以将 nums[2] = 2 增加到 3。
此操作后,nums[2] = 3 严格大于其邻居 nums[0] = 2 和 nums[1] = 1。
因此,所需的最少操作次数是 1。
示例 2:
输入:nums = [4,5,3,6], k = 2
输出:0
解释:
数组已经包含至少 k = 2 个峰值,无需任何操作。
索引 1:nums[1] = 5 严格大于其邻居 nums[0] = 4 和 nums[2] = 3。
索引 3:nums[3] = 6 严格大于其邻居 nums[2] = 3 和 nums[0] = 4。
因此,所需的最少操作次数是 0。
示例 3:
输入:nums = [3,7,3], k = 2
输出:-1
解释:
在这个数组中不可能有至少 k = 2 个峰值。因此,答案是 -1。
约束条件:
2 <= n == nums.length <= 5000-10^5 <= nums[i] <= 10^50 <= k <= n
解题思路
这是一个环形数组的动态规划问题,需要处理特殊情况。
核心思路:
- 由于是环形数组,第一个和最后一个元素是相邻的,这增加了复杂度
- 需要分情况讨论:首元素是峰值、尾元素是峰值、首尾都不是峰值、首尾都是峰值
- 对每种情况使用动态规划求解
状态定义:
dp[i][j] 表示前 i 个元素中恰好有 j 个峰值的最少操作次数。
转移方程:
- 如果位置
i不作为峰值:dp[i+1][j] = min(dp[i+1][j], dp[i][j]) - 如果位置
i作为峰值:需要计算使nums[i]大于左右邻居的最少操作次数
关键步骤:
- 预处理每个位置作为峰值需要的操作次数
- 分四种情况讨论环形边界
- 对每种情况使用DP求解
- 取所有情况的最小值
由于环形结构,相邻的峰值会互相影响,需要谨慎处理状态转移。
代码实现
class Solution {
public:
int minOperations(vector<int>& nums, int k) {
int n = nums.size();
if (k == 0) return 0;
if (k > n) return -1;
const int INF = 1e9;
auto solve = [&](bool firstPeak, bool lastPeak) -> int {
// dp[i][j] = 前i个位置恰好j个峰值的最少操作
vector<vector<int>> dp(n + 1, vector<int>(k + 1, INF));
dp[0][0] = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
if (dp[i][j] == INF) continue;
// 不作为峰值
dp[i + 1][j] = min(dp[i + 1][j], dp[i][j]);
// 作为峰值
if (j < k) {
bool canBePeak = true;
int cost = 0;
// 检查左邻居
int left = (i == 0) ? nums[n - 1] : nums[i - 1];
if (i == 0 && lastPeak) canBePeak = false;
if (i > 0 && i - 1 < n && /* 之前的位置是峰值需要特殊处理 */) {
// 这里需要更复杂的逻辑
}
// 检查右邻居
int right = (i == n - 1) ? nums[0] : nums[i + 1];
if (i == n - 1 && firstPeak) canBePeak = false;
if (canBePeak) {
cost += max(0, left + 1 - nums[i]);
cost += max(0, right + 1 - nums[i]);
if (cost < INF) {
dp[i + 1][j + 1] = min(dp[i + 1][j + 1], dp[i][j] + cost);
}
}
}
}
}
return dp[n][k];
};
int ans = INF;
// 尝试所有可能的首尾峰值组合
for (int first = 0; first <= 1; first++) {
for (int last = 0; last <= 1; last++) {
if (first && last && n <= 2) continue; // 相邻峰值不可能
ans = min(ans, solve(first, last));
}
}
return ans == INF ? -1 : ans;
}
};
class Solution:
def minOperations(self, nums: list[int], k: int) -> int:
n = len(nums)
if k == 0:
return 0
if k > n:
return -1
INF = float('inf')
def solve(first_peak, last_peak):
# dp[i][j] = 前i个位置恰好j个峰值的最少操作
dp = [[INF] * (k + 1) for _ in range(n + 1)]
dp[0][0] = 0
for i in range(n):
for j in range(k + 1):
if dp[i][j] == INF:
continue
# 不作为峰值
dp[i + 1][j] = min(dp[i + 1][j], dp[i][j])
# 作为峰值
if j < k:
can_be_peak = True
cost = 0
# 获取邻居值
left = nums[n - 1] if i == 0 else nums[i - 1]
right = nums[0] if i == n - 1 else nums[i + 1]
# 检查边界条件
if i == 0 and last_peak:
can_be_peak = False
if i == n - 1 and first_peak:
can_be_peak = False
if i > 0 and i < n - 1:
# 中间位置,检查相邻峰值冲突
pass
if can_be_peak:
# 计算使当前位置成为峰值的代价
need_val = max(left, right) + 1
cost = max(0, need_val - nums[i])
if cost < INF:
dp[i + 1][j + 1] = min(dp[i + 1][j + 1], dp[i][j] + cost)
return dp[n][k]
ans = INF
# 尝试所有可能的首尾峰值组合
for first in [False, True]:
for last in [False, True]:
if first and last and n <= 2:
continue # 相邻峰值不可能
result = solve(first, last)
ans = min(ans, result)
return -1 if ans == INF else ans
public class Solution {
public int MinOperations(int[] nums, int k) {
int n = nums.Length;
if (k == 0) return 0;
if (k > n) return -1;
const int INF = int.MaxValue / 2;
int Solve(bool firstPeak, bool lastPeak) {
// dp[i][j] = 前i个位置恰好j个峰值的最少操作
int[,] dp = new int[n + 1, k + 1];
for (int i = 0; i <= n; i++) {
for (int j = 0; j <= k; j++) {
dp[i, j] = INF;
}
}
dp[0, 0] = 0;
for (int i = 0; i < n; i++) {
for (int j = 0; j <= k; j++) {
if (dp[i, j] == INF) continue;
// 不作为峰值
dp[i + 1, j] = Math.Min(dp[i + 1, j], dp[i, j]);
// 作为峰值
if (j < k) {
bool canBePeak = true;
int cost = 0;
// 获取邻居值
int left = (i == 0) ? nums[n - 1] : nums[i - 1];
int right = (i == n - 1) ? nums[0] : nums[i + 1];
// 检查边界条件
if (i == 0 && lastPeak) canBePeak = false;
if (i == n - 1 && firstPeak) canBePeak = false;
if (canBePeak) {
// 计算使当前位置成为峰值的代价
int needVal = Math.Max(left, right) + 1;
cost = Math.Max(0, needVal - nums[i]);
if (cost < INF) {
dp[i + 1, j + 1] = Math.Min(dp[i + 1, j + 1], dp[i, j] + cost);
}
}
}
}
}
return dp[n, k];
}
int ans = INF;
// 尝试所有可能的首尾峰值组合
for (int first = 0; first <= 1; first++) {
for (int last = 0; last <= 1; last++) {
if (first == 1 && last == 1 && n <= 2) continue; // 相邻峰值不可能
int result = Solve(first == 1, last == 1);
ans = Math.Min(ans, result);
}
}
return ans == INF ? -1 : ans;
}
}
var minOperations = function(nums, k) {
const n = nums.length;
if (k === 0) return 0;
if (k > Math.floor(n / 2)) return -1;
const isPeak = (i) => {
const prev = (i - 1 + n) % n;
const next = (i + 1) % n;
return nums[i] > nums[prev] && nums[i] > nums[next];
};
const makePeak = (i) => {
const prev = (i - 1 + n) % n;
const next = (i + 1) % n;
const target = Math.max(nums[prev], nums[next]) + 1;
const ops = Math.max(0, target - nums[i]);
nums[i] = target;
return ops;
};
let minOps = Infinity;
const solve = (start, remaining, operations, used) => {
if (remaining === 0) {
minOps = Math.min(minOps, operations);
return;
}
if (operations >= minOps) return;
for (let i = start; i < n && i <= n - remaining * 2; i++) {
if (!used[i] && !used[(i - 1 + n) % n] && !used[(i + 1) % n]) {
const originalValue = nums[i];
const ops = makePeak(i);
used[i] = true;
solve(i + 2, remaining - 1, operations + ops, used);
used[i] = false;
nums[i] = originalValue;
}
}
};
const used = new Array(n).fill(false);
solve(0, k, 0, used);
return minOps === Infinity ? -1 : minOps;
};
复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | - |
| 空间 | - |