
Problem: 2850. 将石头分散到网格图的最少移动次数
思路
一看标签,动态规划,觉得没啥思路,一看矩阵,固定大小3*3,好!咱们可以试试dfs,然后再用记忆化搜索优化一下时间复杂度。
碎碎念
力扣的新题很多,题干描述也是千奇百怪,我们怎么才能在面对一道新题时,能够利用自己所学的知识,解决它呢,我们怎么才能让自己不需要看题解,也能想到思路呢?
上述的内容是我自己在面对新题目的时候,常常会在脑海里闪过的念头。
我从5月份开始到现在,刷了290题了,这个题量虽然不算多,但是说实话还是积累了一些经验的。通过这个题量,我也能对自己擅长什么和不擅长什么有一个大致的概念了。
- 我比较擅长dfs、二叉树、滑动窗口等类型的题目
- 我不太擅长动态规划、位运算、堆、并查集等类型的题目
因此当我遇到一道新题目的时候,我会先尝试着把它转变成我熟悉的题目,拉到我熟悉的领域,再用我擅长的技能去解决它。
改造题目
理解题干信息,然后转换问题,改造题目。
题目要求我们计算移动矩阵中的石子使矩阵每个格子都有1颗石子的最小的步数 。
最小,最值问题,我们可以联想一下我们以前写过的类似的最值问题的解法,都有哪些可以解决的方式?
比如:
- 322.零钱兑换,让我们计算最小的硬币个数。这是利用动态规划去解决的。
- 209.长度最小的子数组,这是一道不定长滑动窗口的题目。
- 3040. 相同分数的最大操作数目 II,这是利用记忆化搜索,去求最大的操作数目。
等等....
由我上方举的例子,可以看到,解决最值问题是有挺多种方式的。因此针对本题,我们看看是否也可以让它往上面的几个例子靠一靠?
移动,这个操作肯定需要一个起始点位和一个终止点位。
- 石头数量大于1的点位,就是起始点位。
- 石头数量等于0的点位,就是终止点位。
以示例2举例,我们可以得到2个数组,一个数组保存全部的起始点位,一个数组保存全部的终止点位。

- 终止点位:
[ [ 0, 2 ], [ 1, 1 ], [ 1, 2 ], [ 2, 1 ] ] - 起始点位:
[ [ 0, 1 ], [ 2, 2 ] ]
那么石头移动的问题是不是就可以转变成:
计算起始点位 数组中元素 到达终止点位 数组中各元素的最短距离?
举个例子,我们可以这样选择一条路:
-
0, 1 \] =\> \[ 0, 2
-
0, 1 \] =\> \[ 1, 1
-
2, 2 \] =\> \[ 1, 2
-
2, 2 \] =\> \[ 2, 1
由此可得的路径长度为:1+1+1+1 = 4
还可以举个例子,我们选另外的一条路:
-
0, 1 \] =\> \[ 1, 2
-
0, 1 \] =\> \[ 2, 1
-
2, 2 \] =\> \[ 0, 2
-
2, 2 \] =\> \[ 1, 1
由此可得的路径长度为:2+2+2+2 = 8
我们还可以继续举出很多不同的路径。
也就是说,问题进一步变成了我们枚举全部的路线 ,然后选中最短的那条路线。
这是不是即摆脱了我们不是很想处理的矩阵问题,也抛开了什么所谓石头移动问题,而将本题变成了非常直白的一道枚举数组组合的问题?
那怎么去枚举呢?没错,就是我最喜欢的老朋友,dfs。
解题过程
针对每一个终止点位,都让它和起始点位中的元素进行一次组合,组合后,将此点位过滤,进入下一次操作,下一次操作同上次,挑选未被选择的终止点位,去让它起始点位中的元素进行一次组合,重复,直到终止点位中所有元素都被选择过了,则计算最终的路径长度,并将其和
ans比较,选择较小的那一项。
树形结构

代码逻辑,先枚举终止点位
js
const dfs = (zeroIndex, step) => {
// 边界条件,当所有点都被选过了,则结束。
if (zeroIndex.length === 0) {
ans = Math.min(step, ans)
return;
}
for (const zPoint of zeroIndex) {
// do something
}
}
}
代码逻辑,后枚举起始点位
js
for (const zPoint of zeroIndex) {
// 计算起始点位到当前终止点位的距离
for (const largePoint of largeIndex) {
let [x, y] = largePoint
let [x2, y2] = zPoint
// 如果起始点位上的石头数量大于1,说明还可以移动
// 否则,不移动,这个点位上没有额外的石头给别人了。
if (hash.get(`${x}-${y}`) > 1) {
hash.set(`${x}-${y}`, hash.get(`${x}-${y}`) - 1);
// 进入下一次操作时,得把已选的终止点位过滤掉
memoDfs(zeroIndex.filter(item => JSON.stringify(item) !== JSON.stringify(zPoint)), step + Math.abs(x - x2) + Math.abs(y - y2));
// 选完起始点位后,在递归完成时,再把之前消耗的
// 石头加回去
hash.set(`${x}-${y}`, hash.get(`${x}-${y}`) + 1)
}
}
闭包实现记忆化搜索
js
let memoDfs = cache(dfs)
// 使用闭包和高阶函数来实现缓存装饰器
function cache(func) {
const memo = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (!memo.has(key)) {
memo.set(key, func.apply(this, args));
}
return memo.get(key);
};
}
复杂度
- 时间复杂度

- 空间复杂度: O(1)
Code
js
/**
* @param {number[][]} grid
* @return {number}
*/
var minimumMoves = function (grid) {
let zeroIndex = [];
let largeIndex = [];
let hash = new Map();
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (grid[i][j] === 0) {
zeroIndex.push([i, j])
}
if (grid[i][j] > 1) {
largeIndex.push([i, j])
hash.set(`${i}-${j}`, grid[i][j])
}
}
}
if (largeIndex.length === 1) {
return zeroIndex.reduce((pre, cur) => {
let [x, y] = largeIndex[0]
let [x2, y2] = cur
pre = pre + Math.abs(x - x2) + Math.abs(y - y2)
return pre;
}, 0)
}
else if (largeIndex.length === 0) {
return 0;
}
else {
let ans = Infinity;
const dfs = (zeroIndex, step) => {
if (zeroIndex.length === 0) {
ans = Math.min(step, ans)
return;
}
for (const zPoint of zeroIndex) {
for (const largePoint of largeIndex) {
let [x, y] = largePoint
let [x2, y2] = zPoint
if (hash.get(`${x}-${y}`) > 1) {
hash.set(`${x}-${y}`, hash.get(`${x}-${y}`) - 1);
memoDfs(zeroIndex.filter(item => JSON.stringify(item) !== JSON.stringify(zPoint)), step + Math.abs(x - x2) + Math.abs(y - y2));
hash.set(`${x}-${y}`, hash.get(`${x}-${y}`) + 1)
}
}
}
}
let memoDfs = cache(dfs)
memoDfs(zeroIndex, 0)
return ans;
}
};
// 使用闭包和高阶函数来实现缓存装饰器
function cache(func) {
const memo = new Map();
return function (...args) {
const key = JSON.stringify(args);
if (!memo.has(key)) {
memo.set(key, func.apply(this, args));
}
return memo.get(key);
};
}