|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1005. K 次取反后最大化的数组和](https://leetcode-cn.com/problems/maximize-sum-of-array-after-k-negations/solution/gong-shui-san-xie-jian-dan-fen-qing-kuan-6qwu/)** ,难度为 **简单**。 |
| 4 | + |
| 5 | +Tag : 「优先队列(堆)」、「模拟」、「贪心」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个整数数组 `nums` 和一个整数 `k` ,按以下方法修改该数组: |
| 10 | + |
| 11 | +* 选择某个下标 `i` 并将 `nums[i]` 替换为 `-nums[i]` 。 |
| 12 | + |
| 13 | +重复这个过程恰好 `k` 次。可以多次选择同一个下标 `i` 。 |
| 14 | + |
| 15 | +以这种方式修改数组后,返回数组 可能的最大和 。 |
| 16 | + |
| 17 | +示例 1: |
| 18 | +``` |
| 19 | +输入:nums = [4,2,3], k = 1 |
| 20 | +
|
| 21 | +输出:5 |
| 22 | +
|
| 23 | +解释:选择下标 1 ,nums 变为 [4,-2,3] 。 |
| 24 | +``` |
| 25 | +示例 2: |
| 26 | +``` |
| 27 | +输入:nums = [3,-1,0,2], k = 3 |
| 28 | +
|
| 29 | +输出:6 |
| 30 | +
|
| 31 | +解释:选择下标 (1, 2, 2) ,nums 变为 [3,1,0,2] 。 |
| 32 | +``` |
| 33 | +示例 3: |
| 34 | +``` |
| 35 | +输入:nums = [2,-3,-1,5,-4], k = 2 |
| 36 | +
|
| 37 | +输出:13 |
| 38 | +
|
| 39 | +解释:选择下标 (1, 4) ,nums 变为 [2,3,-1,5,4] 。 |
| 40 | +``` |
| 41 | + |
| 42 | +提示: |
| 43 | +* $1 <= nums.length <= 10^4$ |
| 44 | +* $-100 <= nums[i] <= 100$ |
| 45 | +* $1 <= k <= 10^4$ |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +### 贪心 + 分情况讨论 + 模拟 |
| 50 | + |
| 51 | +假设取反前的总和为 $sum$,取反一个任意值 $x$ 后,对 $sum$ 的影响为 $- 2 * x$。 |
| 52 | + |
| 53 | +即取反一个负数会使得结果变大,取反正数会使结果变小,取反 $0$ 值对结果没有影响。 |
| 54 | + |
| 55 | +因此,为了让取反后的结果尽可能的大,我们应当取反 $-2*x$ 尽可能大的数值。即按照「负数从小到大的顺序进行取反」。 |
| 56 | + |
| 57 | +对取反次数 $k$ 和 负数个数 $cnt$ 进行分情况讨论: |
| 58 | + |
| 59 | +* $k <= cnt$:按照负数从小到大的顺序进行取反即可; |
| 60 | +* $k > cnt$:按照负数从小到大的顺序进行取反后,根据「是否存在 $0$ 值」和「剩余取反次数的奇偶性」进行分情况讨论: |
| 61 | + * 存在 $0$ 值 或 剩余取反次数为偶数:直接返回当前取反数组的总和( $0$ 值可抵消任意次数的取反操作,将偶数次的取反操作应用在同一数值上,结果不变); |
| 62 | + * 不存在 $0$ 值且剩余取反次数为奇数:此时从当前数值中取一个绝对值最小值(使用 $idx$ 记录该值下标)进行取反,得到最终的取反数组。 |
| 63 | + |
| 64 | +最后对取反数组进行求和操作即可。 |
| 65 | + |
| 66 | +代码: |
| 67 | +```Java |
| 68 | +class Solution { |
| 69 | + public int largestSumAfterKNegations(int[] nums, int k) { |
| 70 | + int n = nums.length, idx = 0; |
| 71 | + PriorityQueue<Integer> q = new PriorityQueue<>((a,b)->nums[a]-nums[b]); |
| 72 | + boolean zero = false; |
| 73 | + for (int i = 0; i < n; i++) { |
| 74 | + if (nums[i] < 0) q.add(i); |
| 75 | + if (nums[i] == 0) zero = true; |
| 76 | + if (Math.abs(nums[i]) < Math.abs(nums[idx])) idx = i; |
| 77 | + } |
| 78 | + if (k <= q.size()) { |
| 79 | + while (k-- > 0) nums[q.peek()] = -nums[q.poll()]; |
| 80 | + } else { |
| 81 | + while (!q.isEmpty() && k-- > 0) nums[q.peek()] = -nums[q.poll()]; |
| 82 | + if (!zero && k % 2 != 0) nums[idx] = -nums[idx]; |
| 83 | + } |
| 84 | + int ans = 0; |
| 85 | + for (int i : nums) ans += i; |
| 86 | + return ans; |
| 87 | + } |
| 88 | +} |
| 89 | +``` |
| 90 | +* 时间复杂度:对 $nums$ 进行遍历,得到 $idx$ 以及优先队列的复杂度为 $O(n\log{n})$;从优先队列中取出元素进行取反操作的复杂度为 $O(k\log{n})$;对取反数组进行求和复杂度为 $O(n)$。整体复杂度为 $O(n\log{n})$ |
| 91 | +* 空间复杂度:$O(n)$ |
| 92 | + |
| 93 | +--- |
| 94 | + |
| 95 | +### 优化优先队列 |
| 96 | + |
| 97 | +由于 $nums$ 长度范围为 $10000$,但值域范围在 $[-100, 100]$,因此我们可以使用计数数组 $cnt$ 来替代优先队列的作用。 |
| 98 | + |
| 99 | +注意由于 $nums[i]$ 存在负数,因此需要增 $100$ 的偏移量。同时对于翻转操作,仅需要修改相应计数即可。 |
| 100 | + |
| 101 | +代码: |
| 102 | +```Java |
| 103 | +class Solution { |
| 104 | + public int largestSumAfterKNegations(int[] nums, int k) { |
| 105 | + int[] cnts = new int[210]; |
| 106 | + for (int i : nums) cnts[i + 100]++; |
| 107 | + for (int i = -100; i < 0 && k > 0; i++) { |
| 108 | + while (cnts[i + 100] != 0 && k-- > 0) { |
| 109 | + cnts[i + 100]--; cnts[-i + 100]++; |
| 110 | + } |
| 111 | + } |
| 112 | + if (cnts[0 + 100] == 0 && k > 0 && k % 2 != 0) { |
| 113 | + int val = 1; |
| 114 | + while (cnts[val + 100] == 0) val++; |
| 115 | + cnts[val + 100]--; cnts[-val + 100]++; |
| 116 | + } |
| 117 | + int ans = 0; |
| 118 | + for (int i = -100; i <= 100; i++) ans += i * cnts[i + 100]; |
| 119 | + return ans; |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | +* 时间复杂度:需要对 $nums$ 以及大小为 $C = 210$ 的计数数组进行常数次扫描,复杂度为 $O(n + C)$ |
| 124 | +* 空间复杂度:$O(C)$ |
| 125 | + |
| 126 | +--- |
| 127 | + |
| 128 | +### 最后 |
| 129 | + |
| 130 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1005` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 131 | + |
| 132 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 133 | + |
| 134 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 135 | + |
| 136 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 137 | + |
0 commit comments