|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[1775. 通过最少操作次数使数组的和相等量](https://www.acoier.com/2022/12/09/1775.%20%E9%80%9A%E8%BF%87%E6%9C%80%E5%B0%91%E6%93%8D%E4%BD%9C%E6%AC%A1%E6%95%B0%E4%BD%BF%E6%95%B0%E7%BB%84%E7%9A%84%E5%92%8C%E7%9B%B8%E7%AD%89%EF%BC%88%E4%B8%AD%E7%AD%89%EF%BC%89/)** ,难度为 **中等**。 |
| 4 | + |
| 5 | +Tag : 「枚举」、「贪心」、「数学」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你两个长度可能不等的整数数组 `nums1` 和 `nums2` 。两个数组中的所有值都在 `1` 到 `6` 之间(包含 `1` 和 `6`)。 |
| 10 | + |
| 11 | +每次操作中,你可以选择 任意 数组中的任意一个整数,将它变成 `1` 到 `6` 之间 任意 的值(包含 `1` 和 `6`)。 |
| 12 | + |
| 13 | +请你返回使 `nums1` 中所有数的和与 `nums2` 中所有数的和相等的最少操作次数。如果无法使两个数组的和相等,请返回 `-1` 。 |
| 14 | + |
| 15 | +示例 1: |
| 16 | +``` |
| 17 | +输入:nums1 = [1,2,3,4,5,6], nums2 = [1,1,2,2,2,2] |
| 18 | +
|
| 19 | +输出:3 |
| 20 | +
|
| 21 | +解释:你可以通过 3 次操作使 nums1 中所有数的和与 nums2 中所有数的和相等。以下数组下标都从 0 开始。 |
| 22 | +- 将 nums2[0] 变为 6 。 nums1 = [1,2,3,4,5,6], nums2 = [6,1,2,2,2,2] 。 |
| 23 | +- 将 nums1[5] 变为 1 。 nums1 = [1,2,3,4,5,1], nums2 = [6,1,2,2,2,2] 。 |
| 24 | +- 将 nums1[2] 变为 2 。 nums1 = [1,2,2,4,5,1], nums2 = [6,1,2,2,2,2] 。 |
| 25 | +``` |
| 26 | +示例 2: |
| 27 | +``` |
| 28 | +输入:nums1 = [1,1,1,1,1,1,1], nums2 = [6] |
| 29 | +
|
| 30 | +输出:-1 |
| 31 | +
|
| 32 | +解释:没有办法减少 nums1 的和或者增加 nums2 的和使二者相等。 |
| 33 | +``` |
| 34 | +示例 3: |
| 35 | +``` |
| 36 | +输入:nums1 = [6,6], nums2 = [1] |
| 37 | +
|
| 38 | +输出:3 |
| 39 | +
|
| 40 | +解释:你可以通过 3 次操作使 nums1 中所有数的和与 nums2 中所有数的和相等。以下数组下标都从 0 开始。 |
| 41 | +- 将 nums1[0] 变为 2 。 nums1 = [2,6], nums2 = [1] 。 |
| 42 | +- 将 nums1[1] 变为 2 。 nums1 = [2,2], nums2 = [1] 。 |
| 43 | +- 将 nums2[0] 变为 4 。 nums1 = [2,2], nums2 = [4] 。 |
| 44 | +``` |
| 45 | + |
| 46 | +提示: |
| 47 | +* $1 <= nums1.length, nums2.length <= 10^5$ |
| 48 | +* $1 <= nums1[i], nums2[i] <= 6$ |
| 49 | + |
| 50 | +--- |
| 51 | + |
| 52 | +### 枚举 + 贪心 + 数学 |
| 53 | + |
| 54 | +令 `nums1` 的长度为 `n`,`nums2` 的长度为 `m`,根据题意两数组的值域分别为 $[n, 6n]$ 和 $[m, 6m]$,可分别视为数轴上的两条线段。 |
| 55 | + |
| 56 | +为了方便,我们人为固定 $n\leq m$,若不满足则交换两数组,返回 `minOperations(nums2, nums1)` 即可。 |
| 57 | + |
| 58 | +先来考虑无解的情况:当 $6n < m$ 时,说明两线段不重合,必然无法通过变换使得总和相等,直接返回 `-1`。 |
| 59 | + |
| 60 | +由于 $\max(n, m)$ 的范围为 $1e5$,且 $nums[i]$ 的值域大小 $C = 6$,因此我们可以通过枚举最终目标和 `x`(两线段的重合部分)来做,枚举范围不超过 $6 \times 1e5$。 |
| 61 | + |
| 62 | +于是问题转换为:**对于一个原总和为 `sum` 的数组 `nums` 而言,按照题目的变换规则,至少经过多少次变换,才能将其总和变为 `x`**。 |
| 63 | + |
| 64 | +根据原总和 `sum` 和目标结果 `x` 的大小关系进行分情况讨论(将两者差值绝对值记为 `d`): |
| 65 | + |
| 66 | +* 当 $sum < x$ 时,对于原数为 $nums[i]$ 的数而言,其能变为不超过 $nums[i] - 1$ 的任意数。 |
| 67 | + |
| 68 | + 例如 $6$ 能够变化为 $[1, 5]$ 中的任意数,即单个数值 $6$ 最多能够抵消 $6 - 1$ 个差值,不失一般性的可概括为原数为 $nums[i]$ 所能抵消的差值为 $nums[i] - 1$。 |
| 69 | + |
| 70 | + 因此,我们贪心的使用较大数进行变换(从 $6$ 往 $2$ 枚举 `i`),对于每个数值 `i` 而言,其所能提供的个数为 $\min(\left \lceil \frac{d}{i - 1} \right \rceil, cnst[i])$。 |
| 71 | + |
| 72 | +* 当 $sum > x$ 时,同理,原数为 $nums[i]$ 所能提供的最大抵消数为 $6 - nums[i]$,因此我们贪心使用较小数进行变换(从 $1$ 往 $5$ 枚举 `i`),对于每个数值 `i` 而言,其所能提供的个数为 $\min(\left \lceil \frac{d}{6 - i} \right \rceil, cnst[i])$。 |
| 73 | + |
| 74 | +如此一来,我们通过枚举两线段重合点 `x`,复杂度为 $O(C \times \max(n, m))$,并通过复杂度为 $O(C)$ 的数学方法来得知将两原数组总和变为 `x` 所需要的操作次数 `cnt`,在所有的 `cnt` 取最小值即是答案。整体计算量为 $3.6 \times 10^6$,可以过。 |
| 75 | + |
| 76 | + |
| 77 | +Java 代码: |
| 78 | +```Java |
| 79 | +class Solution { |
| 80 | + int[] c1 = new int[10], c2 = new int[10]; |
| 81 | + int s1, s2; |
| 82 | + public int minOperations(int[] nums1, int[] nums2) { |
| 83 | + int n = nums1.length, m = nums2.length; |
| 84 | + if (n > m) return minOperations(nums2, nums1); |
| 85 | + if (m > 6 * n) return -1; |
| 86 | + for (int x : nums1) { |
| 87 | + c1[x]++; s1 += x; |
| 88 | + } |
| 89 | + for (int x : nums2) { |
| 90 | + c2[x]++; s2 += x; |
| 91 | + } |
| 92 | + int ans = n + m; |
| 93 | + for (int i = m; i <= 6 * n; i++) ans = Math.min(ans, getCnt(c1, s1, i) + getCnt(c2, s2, i)); |
| 94 | + return ans; |
| 95 | + } |
| 96 | + int getCnt(int[] cnts, int sum, int x) { |
| 97 | + int ans = 0; |
| 98 | + if (sum > x) { |
| 99 | + for (int i = 6, d = sum - x; i >= 2 && d > 0; i--) { |
| 100 | + int c = Math.min((int) Math.ceil(d * 1.0 / (i - 1)), cnts[i]); |
| 101 | + ans += c; d -= c * (i - 1); |
| 102 | + } |
| 103 | + } else if (sum < x) { |
| 104 | + for (int i = 1, d = x - sum; i <= 5 && d > 0; i++) { |
| 105 | + int c = Math.min((int) Math.ceil(d * 1.0 / (6 - i)), cnts[i]); |
| 106 | + ans += c; d -= c * (6 - i); |
| 107 | + } |
| 108 | + } |
| 109 | + return ans; |
| 110 | + } |
| 111 | +} |
| 112 | +``` |
| 113 | +Python 代码: |
| 114 | +```Python |
| 115 | +class Solution: |
| 116 | + def minOperations(self, nums1: List[int], nums2: List[int]) -> int: |
| 117 | + n, m = len(nums1), len(nums2) |
| 118 | + if n > m: |
| 119 | + return self.minOperations(nums2, nums1) |
| 120 | + if m > 6 * n: |
| 121 | + return -1 |
| 122 | + c1, c2 = Counter(nums1), Counter(nums2) |
| 123 | + s1, s2 = sum(nums1), sum(nums2) |
| 124 | + def getCnt(cnts, tot, x): |
| 125 | + ans = 0 |
| 126 | + if tot > x: |
| 127 | + d = tot - x |
| 128 | + for i in range(6, 1, -1): |
| 129 | + if d <= 0: |
| 130 | + break |
| 131 | + c = min(math.ceil(d / (i - 1)), cnts[i]) |
| 132 | + ans, d = ans + c, d - c * (i - 1) |
| 133 | + elif tot < x: |
| 134 | + d = x - tot |
| 135 | + for i in range(1, 6): |
| 136 | + if d <= 0: |
| 137 | + break |
| 138 | + c = min(math.ceil(d / (6 - i)), cnts[i]) |
| 139 | + ans, d = ans + c, d - c * (6 - i) |
| 140 | + return ans |
| 141 | + ans = n + m |
| 142 | + for i in range(m, 6 * n + 1): |
| 143 | + ans = min(ans, getCnt(c1, s1, i) + getCnt(c2, s2, i)) |
| 144 | + return ans |
| 145 | +``` |
| 146 | +* 时间复杂度:$O(C \times \max(n, m) \times C)$,其中 $C = 6$ 为 $nums[i]$ 的值域大小 |
| 147 | +* 空间复杂度:$O(C)$ |
| 148 | + |
| 149 | +--- |
| 150 | + |
| 151 | +### 最后 |
| 152 | + |
| 153 | +这是我们「刷穿 LeetCode」系列文章的第 `No.1775` 篇,系列开始于 2021/01/01,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 154 | + |
| 155 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 156 | + |
| 157 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 158 | + |
| 159 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 160 | + |
0 commit comments