跳到主要内容

131 篇博文 含有标签「数组」

查看所有标签

· 阅读需 5 分钟

1、题干

给你一个整数数组 nums 。如果 nums 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集 。

  • 比方说,如果 nums = [1, 2, 3, 4] :
    • [2, 3] ,[1, 2, 3] 和 [1, 3] 是  子集,乘积分别为 6 = 2*3 ,6 = 2*3 和 3 = 3 。
    • [1, 4] 和 [4] 不是  子集,因为乘积分别为 4 = 2*2 和 4 = 2*2 。

请你返回 nums 中不同的  子集的数目对 109 + 7 取余 的结果。

nums 中的 子集 是通过删除 nums 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。

 

示例 1:

输入:nums = [1,2,3,4]
输出:6
解释:好子集为:
- [1,2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [1,2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
- [1,3]:乘积为 3 ,可以表示为质数 3 的乘积。
- [2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
- [3]:乘积为 3 ,可以表示为质数 3 的乘积。

示例 2:

输入:nums = [4,2,3,15]
输出:5
解释:好子集为:
- [2]:乘积为 2 ,可以表示为质数 2 的乘积。
- [2,3]:乘积为 6 ,可以表示为互不相同质数 2 和 3 的乘积。
- [2,15]:乘积为 30 ,可以表示为互不相同质数 2,3 和 5 的乘积。
- [3]:乘积为 3 ,可以表示为质数 3 的乘积。
- [15]:乘积为 15 ,可以表示为互不相同质数 3 和 5 的乘积。

 

提示:

  • 1 <= nums.length <= 105
  • 1 <= nums[i] <= 30

2、解题思路

  • 状态压缩:对数组 nums 使用哈希表计数,所有的键都会集中在 [1,30][1,30] 这个区间
  • 从哈希表中剔除存在多个相同因子的数,比如4,8,12
  • 使用 BFS 思路对符合要求的数字进行好子集组合,第一层为1个数的组合,第二层为2个数的组合,以此类推
  • 组合过程中记得剔除掉存在最大公约数不为1的情况,另外注意去重
  • 计算每层好子集数量并累加
    • 好子集数量等于子集各元素个数相乘
    • 若子集中存在 11,不是乘以 11 的数量 c1c1,而应该乘以 11 的组合总数 2c112^{c1}-1,由于 c1c1 可能很大直接计算会溢出,可以使用累乘并模 1e9+71e9+7

这道题最大的坑在于对数字 11 的特殊处理,这个解法和代码都不是最优的,抽空再优化了


3、代码

var numberOfGoodSubsets = function (nums) {
const MOD = 1e9 + 7;
const nMap = nums.reduce((a, c) => a.set(c, (a.get(c) || 0) + 1), new Map());
const bNums = [4, 9, 16, 25, 8, 27, 16];
const gNums = [...nMap.keys()].filter(n => bNums.every((b) => n % b)).sort((a, b) => a - b);
const gcd = (a, b) => (b ? gcd(b, a % b) : a);

let cn1 = 1;
for (let c1 = nMap.get(1); c1; c1--) cn1 = 2 * cn1 % MOD;

let count = 0, queue = gNums.map((n) => [n]);
while (queue.length) {
const nextQueue = [];
for (const arr of queue) {
for (const g of gNums) {
if (g <= arr[arr.length - 1] || arr.some((a) => gcd(a, g) > 1)) continue;
nextQueue.push([...arr, g]);
}

if (arr.length === 1 && arr[0] === 1) continue;
count += arr.reduce((a, c) => {
const cn = c > 1 ? nMap.get(c) : cn1 - 1;
return a * cn % MOD;
}, 1);
count = count % MOD;
}
queue = nextQueue;
}

return count;
};

4、执行结果

  • 执行用时: 468 ms
  • 内存消耗: 63.9 MB

· 阅读需 2 分钟

1、题干

有两种特殊字符:

  • 第一种字符可以用一比特 0 表示
  • 第二种字符可以用两比特(10 或 11)表示

给你一个以 0 结尾的二进制数组 bits ,如果最后一个字符必须是一个一比特字符,则返回 true

 

示例 1:

输入: bits = [1, 0, 0]
输出: true
解释: 唯一的解码方式是将其解析为一个两比特字符和一个一比特字符。
所以最后一个字符是一比特字符。

示例 2:

输入:bits = [1,1,1,0]
输出:false
解释:唯一的解码方式是将其解析为两比特字符和两比特字符。
所以最后一个字符不是一比特字符。

 

提示:

  • 1 <= bits.length <= 1000
  • bits[i]01

2、思路

遍历,遇到 0 走一步,遇到 1 走两步


3、代码

var isOneBitCharacter = function (bits) {
let n = bits.length;
for (let i = 0; i < bits.length - 1; n--, i++) if (bits[i]) n--, i++;
return n === 1;
};

var isOneBitCharacter = function (bits) {
while (bits.length > 1) bits[0] ? bits.splice(0, 2) : bits.shift();
return bits.length === 1;
};

4、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个 m * n 的矩阵,矩阵中的数字 各不相同 。请你按 任意 顺序返回矩阵中的所有幸运数。

幸运数 是指矩阵中满足同时下列两个条件的元素:

  • 在同一行的所有元素中最小
  • 在同一列的所有元素中最大

 

示例 1:

输入:matrix = [[3,7,8],[9,11,13],[15,16,17]]
输出:[15]
解释:15 是唯一的幸运数,因为它是其所在行中的最小值,也是所在列中的最大值。

示例 2:

输入:matrix = [[1,10,4,2],[9,3,8,7],[15,16,17,12]]
输出:[12]
解释:12 是唯一的幸运数,因为它是其所在行中的最小值,也是所在列中的最大值。

示例 3:

输入:matrix = [[7,8],[1,2]]
输出:[7]
解释:7是唯一的幸运数字,因为它是行中的最小值,列中的最大值。

 

提示:

  • m == mat.length
  • n == mat[i].length
  • 1 <= n, m <= 50
  • 1 <= matrix[i][j] <= 10^5
  • 矩阵中的所有元素都是不同的

最近JS的运行环境是不是调整了,执行结果的内存消耗普遍比年前高了一大截,明明空间复杂度是 O(1)O(1) 结果一提交发现击败7%,这到底是为什么啊喂?

wecom-temp-7e880fd1f913b7acf9e1efa1143efdd8.gif


2、解题思路

双层遍历,外层按行号 i 逐行遍历,内层先遍历找出第 i 行最小列号 minCol;再遍历第 minCol 列所有元素,检查 matrix[i][minCol] 是否该列最大值,若是则加入结果数组 res


3、代码

var luckyNumbers = function (matrix) {
const res = [];
for (let i = 0; i < matrix.length; i++) {
const minCol = matrix[i].reduce((a, c, j) => c < matrix[i][a] ? j : a, 0);
for (let r = 0; r < matrix.length; r++) {
if (matrix[r][minCol] > matrix[i][minCol]) break;
if (r === matrix.length - 1) res.push(matrix[i][minCol]);
}
}
return res;
};

4、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

5、执行结果

image.png

· 阅读需 3 分钟

1、题干

给你一个字符串 s 和一个字符 c ,且 cs 中出现过的字符。

返回一个整数数组 answer ,其中 answer.length == s.lengthanswer[i]s 中从下标 i 到离它 最近 的字符 c距离

两个下标 ij 之间的 距离abs(i - j) ,其中 abs 是绝对值函数。

 

示例 1:

输入:s = "loveleetcode", c = "e"
输出:[3,2,1,0,1,0,0,1,2,2,1,0]
解释:字符 'e' 出现在下标 3、5、6 和 11 处(下标从 0 开始计数)。
距下标 0 最近的 'e' 出现在下标 3 ,所以距离为 abs(0 - 3) = 3 。
距下标 1 最近的 'e' 出现在下标 3 ,所以距离为 abs(1 - 3) = 2 。
对于下标 4 ,出现在下标 3 和下标 5 处的 'e' 都离它最近,但距离是一样的 abs(4 - 3) == abs(4 - 5) = 1 。
距下标 8 最近的 'e' 出现在下标 6 ,所以距离为 abs(8 - 6) = 2 。

示例 2:

输入:s = "aaab", c = "b"
输出:[3,2,1,0]

 

提示:
  • 1 <= s.length <= 104
  • s[i]c 均为小写英文字母
  • 题目数据保证 cs 中至少出现一次

题目简单,但是处理各种边界情况还是有点繁琐,代码量很难压缩,

71d74efddaafe529dc032181988da153.gif


2、解题思路

lr 分别表示左边和右边最近出现字符 c 的下标,遍历区间范围 [l,r)[l,r) 内的字符,记录字符离边界最近的距离即可


注意处理好边界情况

  • 左边没有字符 c 时,l 取值为 -Infinity
  • 右边没有字符 c 时,r 取值为 Infinity
  • 内层循环要控制好 i 的范围,不能越出 [0,s.length)[0,s.length) 这个范围

3、代码

var shortestToChar = function (s, c) {
const res = [];
for (let l = -Infinity, r = s.indexOf(c); l < s.length;) {
for (let i = Math.max(0, l); i < Math.min(s.length, r); i++) {
res.push(Math.min(r - i, i - l));
}
l = r, r = s.indexOf(c, r + 1), r = r < 0 ? Infinity : r;
}
return res;
};

4、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

5、执行结果

image.png

· 阅读需 6 分钟

1、题干

在大小为 n x n 的网格 grid 上,每个单元格都有一盏灯,最初灯都处于 关闭 状态。

给你一个由灯的位置组成的二维数组 lamps ,其中 lamps[i] = [rowi, coli] 表示 打开 位于 grid[rowi][coli] 的灯。即便同一盏灯可能在 lamps 中多次列出,不会影响这盏灯处于 打开 状态。

当一盏灯处于打开状态,它将会照亮 自身所在单元格 以及同一 、同一 和两条 对角线 上的 所有其他单元格

另给你一个二维数组 queries ,其中 queries[j] = [rowj, colj] 。对于第 j 个查询,如果单元格 [rowj, colj] 是被照亮的,则查询结果为 1 ,否则为 0 。在第 j 次查询之后 [按照查询的顺序] ,关闭 位于单元格 grid[rowj][colj] 上及相邻 8 个方向上(与单元格 grid[rowi][coli] 共享角或边)的任何灯。

返回一个整数数组 ans 作为答案, ans[j] 应等于第 j 次查询 queries[j] 的结果,1 表示照亮,0 表示未照亮。

 

示例 1:

输入:n = 5, lamps = [[0,0],[4,4]], queries = [[1,1],[1,0]]
输出:[1,0]
解释:最初所有灯都是关闭的。在执行查询之前,打开位于 [0, 0] 和 [4, 4] 的灯。第 0 次查询检查 grid[1][1] 是否被照亮(蓝色方框)。该单元格被照亮,所以 ans[0] = 1 。然后,关闭红色方框中的所有灯。

第 1 次查询检查 grid[1][0] 是否被照亮(蓝色方框)。该单元格没有被照亮,所以 ans[1] = 0 。然后,关闭红色矩形中的所有灯。

示例 2:

输入:n = 5, lamps = [[0,0],[4,4]], queries = [[1,1],[1,1]]
输出:[1,1]

示例 3:

输入:n = 5, lamps = [[0,0],[0,4]], queries = [[0,4],[0,1],[1,4]]
输出:[1,1,0]

 

提示:

  • 1 <= n <= 109
  • 0 <= lamps.length <= 20000
  • 0 <= queries.length <= 20000
  • lamps[i].length == 2
  • 0 <= rowi, coli < n
  • queries[j].length == 2
  • 0 <= rowj, colj < n

今天题目挺简单的,感觉会写CRUD就能AC,只需要按照题目意思存储、查询以及修改亮灯状态就行

5b4f2239f899f748dd1fd07a8d78a201.gif


2、解题思路

  • 由于网格行列的最大值是 10910^9,要遍历网格进行状态查改是不太可能了
  • 本题关键点是使用多个哈希表存储亮灯状态,另外要注意灯的状态实际有3种:关闭、打开、被照亮
    • 使用哈希集合存储打开的灯,记为 lightSet,其中键为行列号拼接成的字符串,形如 i-j
    • 使用哈希映射存储被照亮的行和列,分别记为 rowMapcolMap,其中键为行号或列号,值为亮灯数量
    • 使用哈希映射存储被照亮的两条对角线 yx=dy-x=dy+x=dy+x=d,分别记为 dglMap1dglMap2,其中键为行列号的差或和,值为亮灯数量
  • 接着根据题意模拟即可
    • 将初始亮灯状态 lamps 存入各哈希表中
    • 遍历查询数组 queries ,查询亮灯状态并记录到结果数组 ans,查询时注意:只要行列或对角线上亮灯数量大于0,亮灯状态就是 1
    • 每次查询后关闭九宫格内的灯,关灯时注意:打开的灯才需要关闭,被照亮的灯无法关闭

3、代码

var gridIllumination = function (n, lamps, queries) {
// 亮灯哈希集合
const lightSet = new Set();
// 亮灯行、亮灯列
const rowMap = new Map(), colMap = new Map();
// 亮灯对角线1:y-x=d、亮灯对角线2:y+x=d
const dglMap1 = new Map(), dglMap2 = new Map();

// 将初始亮灯状态存入各哈希表中
for (const [i, j] of lamps) {
if (lightSet.has(`${i}-${j}`)) continue;
lightSet.add(`${i}-${j}`);
rowMap.set(i, (rowMap.get(i) || 0) + 1);
colMap.set(j, (colMap.get(j) || 0) + 1);
const d1 = j - i, d2 = j + i;
dglMap1.set(d1, (dglMap1.get(d1) || 0) + 1);
dglMap2.set(d2, (dglMap2.get(d2) || 0) + 1);
}

// 关灯函数:关闭九宫格内的灯
function turnOff(fi, fj) {
for (let di = -1; di < 2; di++) {
for (let dj = -1; dj < 2; dj++) {
const i = fi + di, j = fj + dj;
if (!lightSet.has(`${i}-${j}`)) continue;
lightSet.delete(`${i}-${j}`);
rowMap.set(i, (rowMap.get(i) || 0) - 1);
colMap.set(j, (colMap.get(j) || 0) - 1);
const d1 = j - i, d2 = j + i;
dglMap1.set(d1, (dglMap1.get(d1) || 0) - 1);
dglMap2.set(d2, (dglMap2.get(d2) || 0) - 1);
}
}
}

// 遍历查询数组,计算亮灯状态后进行关灯操作
const ans = queries.map(() => 0);
for (let qi = 0; qi < queries.length; qi++) {
const [i, j] = queries[qi];
const d1 = j - i, d2 = j + i;
if (lightSet.has(`${i}-${j}`) || rowMap.get(i) || colMap.get(j) || dglMap1.get(d1) || dglMap2.get(d2)) ans[qi] = 1;
turnOff(i, j);
}

return ans;
};

4、复杂度

  • 时间复杂度:O(n)O(n)
  • 空间复杂度:O(n)O(n)

5、执行结果

image.png

· 阅读需 7 分钟

1、题干

你正在参加一个多角色游戏,每个角色都有两个主要属性:攻击防御 。给你一个二维整数数组 properties ,其中 properties[i] = [attacki, defensei] 表示游戏中第 i 个角色的属性。

如果存在一个其他角色的攻击和防御等级 都严格高于 该角色的攻击和防御等级,则认为该角色为 弱角色 。更正式地,如果认为角色 i 弱于 存在的另一个角色 j ,那么 attackj > attackidefensej > defensei

返回 弱角色 的数量。

 

示例 1:

输入:properties = [[5,5],[6,3],[3,6]]
输出:0
解释:不存在攻击和防御都严格高于其他角色的角色。

示例 2:

输入:properties = [[2,2],[3,3]]
输出:1
解释:第一个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。

示例 3:

输入:properties = [[1,5],[10,4],[4,3]]
输出:1
解释:第三个角色是弱角色,因为第二个角色的攻击和防御严格大于该角色。

 

提示:

  • 2 <= properties.length <= 105
  • properties[i].length == 2
  • 1 <= attacki, defensei <= 105

没想到官解那么巧妙的解法,提供一个普通思路。。。

1a709d284924ca5858f365c4e6f6f112.gif


2、解法1

方便起见将角色属性数组长度 properties.length 记为 nn


总体思路

  • 先升序排序:对属性数组 properties 按攻击力升序排序
  • 再查找强角色:假设攻击力没有相同值,对于任意角色 i 如果能在区间 [i+1,n1][i+1, n-1] 内找到一个防御力更大的角色 j,就说明 i 是一个弱角色
  • 预统计区间内最大防御力,加速查找:问题是在区间 [i+1,n1][i+1, n-1] 内找一个比较大的值需要遍历,时间复杂度不允许,因此可以先统计区间内的最大值,在查找的时候直接使用最大值进行比较即可

大体步骤

  • 先对属性数组 properties 按攻击力升序排序
  • 创建数组 maxDefs 用于记录区间最大防御力,对于任意元素 maxDefs[i] 表示区间 [i,n1][i, n-1] 内的最大防御力
    • 这里需要倒序遍历 properties,对于任意索引 imaxDefs[i] 取当前防御力 properties[i][1] 与后续区间最大防御力 maxDefs[i + 1] 中的最大值
    • 最后 maxDefs 会形成一个单调递减的数列
  • 最后顺序遍历 properties ,对于任意角色属性 properties[i],如果能在区间 [i+1,n1][i+1, n-1] 找到一个攻击力防御力都更大的角色属性 properties[j],即 properties[i][0] < properties[j][0] && properties[i][1] < maxDefs[j],则说明这是一个弱角色
    • 注意这里需要跳过攻击力相同的角色

3、代码

var numberOfWeakCharacters = function (properties) {
properties.sort((a, b) => a[0] - b[0]);

const n = properties.length, maxDefs = new Array(n).fill(0);
for (let i = n - 1; i > -1; i--) {
maxDefs[i] = Math.max(maxDefs[i + 1] || 0, properties[i][1]);
}

let count = 0;
for (let i = 0; i < n - 1; i++) {
let j = i + 1;
// 跳过攻击力相同的角色
while (j < n && properties[i][0] === properties[j][0]) j++;
if (properties[i][1] < maxDefs[j]) count++;
}

return count;
};

4、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)O(n2)O(n^2) 之间
  • 空间复杂度:O(n)O(n)

5、执行结果

  • 执行用时: 344 ms,超97%
  • 内存消耗: 75.2 MB,超7%

6、解法1-优化

问题:跳过攻击力相同的角色是后置的,在查找过程中通过遍历的方式逐一跳过,这里增加了时间复杂度 优化:可以参考官解的排序方式:依然按攻击力升序排序,但是当攻击力相同时按防御力降序排序。这里很巧妙地规避了需要跳过攻击力相同角色的问题,因为攻击力相同时按防御力降序排序,意味着后续区间如果出现更大的防御力必定不属于攻击力相同的角色


7、代码

var numberOfWeakCharacters = function (properties) {
properties.sort((a, b) => a[0] === b[0] ? b[1] - a[1] : a[0] - b[0]);

const n = properties.length, maxDefs = new Array(n).fill(0);
for (let i = n - 1; i > -1; i--) {
maxDefs[i] = Math.max(maxDefs[i + 1] || 0, properties[i][1]);
}

let count = 0;
for (let i = 0; i < n - 1; i++) {
if (properties[i][1] < maxDefs[i + 1]) count++;
}

return count;
};

8、复杂度

  • 时间复杂度:O(nlogn)O(nlogn)
  • 空间复杂度:O(n)O(n)

9、执行结果

  • 执行用时: 336 ms,超97%
  • 内存消耗: 74.8 MB,超7%

如果把优化后的代码调整为按攻击力降序排序或者按倒序遍历查找弱角色,就跟官解一样了,记录区间最大值不再需要数组,只需要一个变量,空间复杂度可以降到 O(1)O(1),时间复杂度相同。

· 阅读需 4 分钟

1、题干

给定一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n

 

示例 1:

输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.

示例 2:

输入:nums = [0]
输出:0

示例 3:

输入:nums = [2,4]
输出:6

示例 4:

输入:nums = [8,10,2]
输出:10

示例 5:

输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127

 

提示:

  • 1 <= nums.length <= 2 * 105
  • 0 <= nums[i] <= 231 - 1

 

进阶:你可以在 O(n) 的时间解决这个问题吗?

 

注意:本题与主站 421 题相同: https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/

题目比较难,没能想到官解那些优秀解法,尝试了暴力解法并两次剪枝优化后,不仅AC还超过82%提交

2、解题思路

解题关键是:最大异或结果一定是由 二进制位数最多的一个数另一个数 进行异或运算得来。 以 [3,10,5,25,2,8] 为例,用二进制表示是:[11, 1010, 101, 11001, 10, 1000],其中 二进制位数最多 的只有1个数 25(11001),接着计算 25 跟其他数的异或结果并取最大值即可。


暴力解法步骤

  • 先对数组进行降序排序
  • 对所有数进行异或运算并取最大值

剪枝优化

  • 优化1:异或结果的最大可能是:二进制位数最多的数异或之后所有二进制位变成 1,假设为 MAX_NUM。如果运算结果已达到最大可能 MAX_NUM,则直接退出程序。(效果显著)
  • 优化2:异或运算时,第一个数取 二进制位数最多的数 ,第二个数取 二进制位数更少的数 。第一个数不变的情况下,第二个数的二进制位数与其相同则异或会导致最高位变为 0,结果必然小于第二个数的二进制位数更少的情况。

3、代码

// 优化1
var findMaximumXOR = function (nums) {
nums.sort((a, b) => b - a);
const c = Math.log2(nums[0]) >> 0;
let idx = nums.findIndex((n) => n < 1 << c);
if (idx < 0) idx = nums.length;

const MAX_NUM = (1 << (c + 1)) - 1;
let res = 0;

for (let i = 0; i < idx; i++) {
for (let j = i + 1; j < nums.length; j++) {
res = Math.max(res, nums[i] ^ nums[j]);
if (res === MAX_NUM) return res;
}
}

return res;
};

// 优化1 + 优化2
var findMaximumXOR = function (nums) {
nums.sort((a, b) => b - a);
const c = Math.log2(nums[0]) >> 0;
const idx = nums.findIndex((n) => n < 1 << c);

const MAX_NUM = (1 << (c + 1)) - 1;
let res = 0;

if (idx > -1) {
for (let i = 0; i < idx; i++) {
for (let j = idx; j < nums.length; j++) {
res = Math.max(res, nums[i] ^ nums[j]);
if (res === MAX_NUM) return res;
}
}
} else {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) res = Math.max(res, nums[i] ^ nums[j]);
}
}

return res;
};

4、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

5、执行结果

image.png

· 阅读需 4 分钟

1、题干

给你一个整数数组 nums ,返回 nums[i] XOR nums[j] 的最大运算结果,其中 0 ≤ i ≤ j < n

 

示例 1:

输入:nums = [3,10,5,25,2,8]
输出:28
解释:最大运算结果是 5 XOR 25 = 28.

示例 2:

输入:nums = [14,70,53,83,49,91,36,80,92,51,66,70]
输出:127

 

提示:

  • 1 <= nums.length <= 2 * 105
  • 0 <= nums[i] <= 231 - 1

 

注意:本题与主站 421 题相同: https://leetcode-cn.com/problems/maximum-xor-of-two-numbers-in-an-array/

题目比较难,没能想到官解那些优秀解法,尝试了暴力解法并两次剪枝优化后,不仅AC还超过82%提交

2、解题思路

解题关键是:最大异或结果一定是由 二进制位数最多的一个数另一个数 进行异或运算得来。 以 [3,10,5,25,2,8] 为例,用二进制表示是:[11, 1010, 101, 11001, 10, 1000],其中 二进制位数最多 的只有1个数 25(11001),接着计算 25 跟其他数的异或结果并取最大值即可。


暴力解法步骤

  • 先对数组进行降序排序
  • 对所有数进行异或运算并取最大值

剪枝优化

  • 优化1:异或结果的最大可能是:二进制位数最多的数异或之后所有二进制位变成 1,假设为 MAX_NUM。如果运算结果已达到最大可能 MAX_NUM,则直接退出程序。(效果显著)
  • 优化2:异或运算时,第一个数取 二进制位数最多的数 ,第二个数取 二进制位数更少的数 。第一个数不变的情况下,第二个数的二进制位数与其相同则异或会导致最高位变为 0,结果必然小于第二个数的二进制位数更少的情况。

3、代码

// 优化1
var findMaximumXOR = function (nums) {
nums.sort((a, b) => b - a);
const c = Math.log2(nums[0]) >> 0;
let idx = nums.findIndex((n) => n < 1 << c);
if (idx < 0) idx = nums.length;

const MAX_NUM = (1 << (c + 1)) - 1;
let res = 0;

for (let i = 0; i < idx; i++) {
for (let j = i + 1; j < nums.length; j++) {
res = Math.max(res, nums[i] ^ nums[j]);
if (res === MAX_NUM) return res;
}
}

return res;
};

// 优化1 + 优化2
var findMaximumXOR = function (nums) {
nums.sort((a, b) => b - a);
const c = Math.log2(nums[0]) >> 0;
const idx = nums.findIndex((n) => n < 1 << c);

const MAX_NUM = (1 << (c + 1)) - 1;
let res = 0;

if (idx > -1) {
for (let i = 0; i < idx; i++) {
for (let j = idx; j < nums.length; j++) {
res = Math.max(res, nums[i] ^ nums[j]);
if (res === MAX_NUM) return res;
}
}
} else {
for (let i = 0; i < nums.length; i++) {
for (let j = i + 1; j < nums.length; j++) res = Math.max(res, nums[i] ^ nums[j]);
}
}

return res;
};

4、复杂度

  • 时间复杂度:O(n2)O(n^2)
  • 空间复杂度:O(1)O(1)

5、执行结果

image.png

· 阅读需 7 分钟

1、题干

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:一个机器人每次只能向下或者向右移动一步。

 

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

 

注意:本题与主站 64 题相同: https://leetcode-cn.com/problems/minimum-path-sum/

2、解法1-暴力DFS

看完题目之后没多想误认为是走迷宫的题,于是写了一版暴力 DFS ,结果超时了。代码逻辑是从左上角到右下角进行深度遍历,并且累计路径和代入下一步计算。


超时的原因在于 :通常走迷宫的题不会重复经过某个节点,因此时间复杂度是 O(mn)O(mn),以 m,n<=200m,n <= 200 来算不会超时; 但这个题目的暴力 DFS 可以看做是遍历一棵高度为 n+m1n+m-1 的二叉树,时间复杂度接近指数级的 2n+m12^{n+m-1},超时也不足为奇。


从网格左上角到右下角的最短路径长度为 n+m1n+m-1,因此把它当成树的话高度为 n+m1n+m-1


暴力DFS超时代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity;

function dfs(i, j, sum) {
if (i >= m || j >= n) return;
sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

3、暴力DFS-优化

通常暴力 DFS 可以通过剪枝、记忆化等方式进行优化,这里采用了剪枝优化,优化后AC了但耗时 3208 ms 依然很高。


这里的问题在于

  • 上述DFS代码属于暴力遍历所有情况并在终点时计算最小值,没有求局部最优解的倾向
  • 剪枝优化不稳定:这里剪枝的方式是记录节点前序路径和,当再次访问该节点时判断当前情况的前序路径和是否更小,以决定是否继续遍历以及是否更新该前序路径和,这种剪枝方式存在不稳定性,因为记录的前序路径和不是最小,后续遍历可能会有更小的前序路径和出现,所以这种剪枝没办法排除掉最小值以外的所有情况

4、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity, minPreSum = new Map();

function dfs(i, j, sum) {
const key = i + '-' + j;
if (i >= m || j >= n || sum >= (minPreSum.get(key) || Infinity)) return;
minPreSum.set(key, sum);

sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

5、执行结果

  • 执行用时: 3208 ms
  • 内存消耗: 53 MB

6、解法2-记忆化DFS

这个解法思路接近于动态规划, dfs 函数的意图是求局部最优解最终得到全局最优解,其中 dfs(i, j) 代表从终点 (m, n) 到点 (i, j) 的最小路径和,然后通过哈希表记录点 (i, j) 的最小路径和,后续再次遍历到该节点直接返回最小路径和,无需多余遍历与计算。


7、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const minSum = new Map();

function dfs(i, j) {
if (i >= m || j >= n) return Infinity;
if (i === m - 1 && j === n - 1) return grid[i][j];

const key = i + '-' + j;
if (minSum.has(key)) return minSum.get(key);

const min = Math.min(dfs(i + 1, j), dfs(i, j + 1)) + grid[i][j];
return minSum.set(key, min), min;
}

return dfs(0, 0);
};

8、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 49.2 MB

9、解法3-动态规划

解法2采用的是逆序累加和求解,这里为了方便理解采用顺序累加和进行求解。从题意可以看出对于任意节点 (i, j),其最小路径和取决于上方节点 (i-1, j) 和左方节点 (i, j-1),其值为左上两个节点中最小路径和的较小值加上自身的值。

  • dp 数组含义:dp 数组是一个 (m+1)*(n+1) 的数组,其中 dp[i][j] 表示从起点 (0, 0) 到点 (i, j) 的最小路径和,其中第一行和第一列只是为了方便计算,没有实际含义
  • 状态转移方程是:
    • i === 1 && j === 1 时:dp[i][j] = grid[i - 1][j - 1]
    • 其他情况:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
  • dp[i][j] 对应的节点是 grid[i - 1][j - 1],因此遍历 dp 数组时 ij 都从下标 1 开始

10、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(Infinity));

for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (i === 1 && j === 1) dp[i][j] = grid[i - 1][j - 1];
else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}

return dp[m][n];
};

11、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 44 MB

12、最后

题解区有更优秀的动态规划解法,有降维 DP(一维)、有把原始数组 grid 作为 DP 数组等,主要降低了空间复杂度,很值得学习。

· 阅读需 7 分钟

1、题干

给定一个包含非负整数的 m x n 网格 grid ,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

说明:一个机器人每次只能向下或者向右移动一步。

 

示例 1:

输入:grid = [[1,3,1],[1,5,1],[4,2,1]]
输出:7
解释:因为路径 1→3→1→1→1 的总和最小。

示例 2:

输入:grid = [[1,2,3],[4,5,6]]
输出:12

 

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 200
  • 0 <= grid[i][j] <= 100

 

注意:本题与主站 64 题相同: https://leetcode-cn.com/problems/minimum-path-sum/

2、解法1-暴力DFS

看完题目之后没多想误认为是走迷宫的题,于是写了一版暴力 DFS ,结果超时了。代码逻辑是从左上角到右下角进行深度遍历,并且累计路径和代入下一步计算。


超时的原因在于 :通常走迷宫的题不会重复经过某个节点,因此时间复杂度是 O(mn)O(mn),以 m,n<=200m,n <= 200 来算不会超时; 但这个题目的暴力 DFS 可以看做是遍历一棵高度为 n+m1n+m-1 的二叉树,时间复杂度接近指数级的 2n+m12^{n+m-1},超时也不足为奇。


从网格左上角到右下角的最短路径长度为 n+m1n+m-1,因此把它当成树的话高度为 n+m1n+m-1


暴力DFS超时代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity;

function dfs(i, j, sum) {
if (i >= m || j >= n) return;
sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

3、暴力DFS-优化

通常暴力 DFS 可以通过剪枝、记忆化等方式进行优化,这里采用了剪枝优化,优化后AC了但耗时 3208 ms 依然很高。


这里的问题在于

  • 上述DFS代码属于暴力遍历所有情况并在终点时计算最小值,没有求局部最优解的倾向
  • 剪枝优化不稳定:这里剪枝的方式是记录节点前序路径和,当再次访问该节点时判断当前情况的前序路径和是否更小,以决定是否继续遍历以及是否更新该前序路径和,这种剪枝方式存在不稳定性,因为记录的前序路径和不是最小,后续遍历可能会有更小的前序路径和出现,所以这种剪枝没办法排除掉最小值以外的所有情况

4、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
let min = Infinity, minPreSum = new Map();

function dfs(i, j, sum) {
const key = i + '-' + j;
if (i >= m || j >= n || sum >= (minPreSum.get(key) || Infinity)) return;
minPreSum.set(key, sum);

sum += grid[i][j]

if (i === m - 1 && j === n - 1) min = Math.min(min, sum);

dfs(i + 1, j, sum);
dfs(i, j + 1, sum);
}

dfs(0, 0, 0);
return min;
};

5、执行结果

  • 执行用时: 3208 ms
  • 内存消耗: 53 MB

6、解法2-记忆化DFS

这个解法思路接近于动态规划, dfs 函数的意图是求局部最优解最终得到全局最优解,其中 dfs(i, j) 代表从终点 (m, n) 到点 (i, j) 的最小路径和,然后通过哈希表记录点 (i, j) 的最小路径和,后续再次遍历到该节点直接返回最小路径和,无需多余遍历与计算。


7、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const minSum = new Map();

function dfs(i, j) {
if (i >= m || j >= n) return Infinity;
if (i === m - 1 && j === n - 1) return grid[i][j];

const key = i + '-' + j;
if (minSum.has(key)) return minSum.get(key);

const min = Math.min(dfs(i + 1, j), dfs(i, j + 1)) + grid[i][j];
return minSum.set(key, min), min;
}

return dfs(0, 0);
};

8、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 49.2 MB

9、解法3-动态规划

解法2采用的是逆序累加和求解,这里为了方便理解采用顺序累加和进行求解。从题意可以看出对于任意节点 (i, j),其最小路径和取决于上方节点 (i-1, j) 和左方节点 (i, j-1),其值为左上两个节点中最小路径和的较小值加上自身的值。

  • dp 数组含义:dp 数组是一个 (m+1)*(n+1) 的数组,其中 dp[i][j] 表示从起点 (0, 0) 到点 (i, j) 的最小路径和,其中第一行和第一列只是为了方便计算,没有实际含义
  • 状态转移方程是:
    • i === 1 && j === 1 时:dp[i][j] = grid[i - 1][j - 1]
    • 其他情况:dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1]
  • dp[i][j] 对应的节点是 grid[i - 1][j - 1],因此遍历 dp 数组时 ij 都从下标 1 开始

10、代码

var minPathSum = function (grid) {
const m = grid.length, n = grid[0].length
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(Infinity));

for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (i === 1 && j === 1) dp[i][j] = grid[i - 1][j - 1];
else dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + grid[i - 1][j - 1];
}
}

return dp[m][n];
};

11、执行结果

  • 执行用时: 76 ms
  • 内存消耗: 44 MB

12、最后

题解区有更优秀的动态规划解法,有降维 DP(一维)、有把原始数组 grid 作为 DP 数组等,主要降低了空间复杂度,很值得学习。