3495. Minimum Operations to Make Array Elements Zero
题目简介
这是一道 hard,题目给我们一个二维数组,数组中包含多个 [left, right]
形式的数组,代表从 left 到 right 的正整数
题目要求我们每次操作都从 l 到 r 中任意选择两个数字,并将其处以 4 后向下取整,直到数组中所有数字都变为 0
最后返回所有数组需要的操作次数
解题思路
这道题有两种解法,但是都服务于一个相同的思路,下面我们先聊聊解题思路
首先我们先来分析一下,任意一个数字被 4 整除之后向下取整需要几步:
- 对于 1, 2, 3 来说,只需要一步,因为他们小于 $4^1$
- 对于 4, 5, ..., 15 来说,需要两步,因为他们小于 $4^2$
- 对于 16, 17, ..., 63 来说,需要三步,因为他们小于 $4^3$
- 以此类推
知道了这个规律之后,我们不难想到可以针对 l 到 r 的正整数序列进行分堆,然后针对每一堆的数量乘以他们需要的步骤之后相加,就可以求得任意一个 [left, right]
组合中每一个数字变为 0 需要的步骤了,最后再将其除以 2 之后向上取整,就能得到题目所求的操作次数了
举个例子:有一个数组是 [3, 17]
我们可以将其分为三组:
- 3 自己一组,因为它只需要一次除法
- 4~15 一组,因为他们需要两次除法
- 16, 17 一组,因为需要三次除法
我们可以得到需要 31 (1*1 + (15-4+1) * 2 + 2 * 3
) 次除法才能够将所有数字变为 0,那么题目需要的操作次数就是 Ceil(31/2) = 16
了解了这个思路之后,接下来有两种解决方案:
- 用数字来寻找相同组
- 遍历所有组,并找到各组内的数字
用数字寻找组
用数字寻找范围的精髓在于,通过题目给出的数字,来判断他们分别属于哪些范围,然后再进行求和计算
为了算法的通用性,我们可以设计一个能够计算 [1, n]
这个范围的算法,这样一来如果题目要求是 [left, right]
,我们可以用 [1, right]
需要的除法计算减去 [1, left-1]
需要的除法次数,然后再除以 2 向上取整就可以计算出题目所需的操作数量了
Javascript
/**
* @param {number[][]} queries
* @return {number}
*/
var minOperations = function (queries) {
let result = 0n
queries.forEach(([l, r]) => {
const leftOpCount = totalNeedOfOpsToNum(l - 1)
const rightOpCount = totalNeedOfOpsToNum(r)
result += (rightOpCount - leftOpCount + 1n) / 2n
})
return Number(result)
};
// 将数字分组,最后加总所需除法次数
const totalNeedOfOpsToNum = num => {
let res = 0n;
let needOfOps = 1;
let base = 1;
while (base <= num) {
const end = Math.min(base * 2 - 1, num);
res += BigInt(Math.floor((needOfOps + 1) / 2)) * BigInt(end - base + 1);
needOfOps++;
base *= 2;
}
return res;
}
用组寻找数字
换一个角度来思考题目,如果我们可以知道总共有多少组,我们是不是就可以通过遍历所有的组来寻找各自组别有的数字了呢?
我们可以看到题目对 left 跟 right 的 $1 \le l \lt r \le 10^9$
题目要求的除数是 4,所以我们可以知道这个组最多不超过 $log_{4}10^9 \lt 15$
于是我们就可以针对每一组 [l, r]
,进行一次循环,从而找出所需要进行除法的次数
Javascript
/**
* @param {number[][]} queries
* @return {number}
*/
var minOperations = function (queries) {
let res = 0;
queries.forEach(([left, right]) => {
let needOfOps = 0
for (let i = 1; i <= 15; i++) {
// the upper bound must be 4^i - 1
const upperBound = (1 << (2 * i)) - 1
if (upperBound < left) continue
const upper = Math.min(upperBound, right)
needOfOps += (upper - left + 1) * i
if (upper === right) break;
left = upperBound + 1
}
res += Math.ceil(needOfOps / 2)
})
return res
};