【记忆化搜索】条条大路通AC,走好适合你的那一条,走到后再考虑走得快

Problem: 2850. 将石头分散到网格图的最少移动次数

  1. 思路

    1. 碎碎念
    2. 改造题目
  2. 解题过程

    1. 树形结构
    2. 代码逻辑,先枚举终止点位
    3. 代码逻辑,后枚举起始点位
    4. 闭包实现记忆化搜索
  3. 复杂度

  4. Code

思路

一看标签,动态规划,觉得没啥思路,一看矩阵,固定大小3*3,好!咱们可以试试dfs,然后再用记忆化搜索优化一下时间复杂度。

碎碎念

力扣的新题很多,题干描述也是千奇百怪,我们怎么才能在面对一道新题时,能够利用自己所学的知识,解决它呢,我们怎么才能让自己不需要看题解,也能想到思路呢?

上述的内容是我自己在面对新题目的时候,常常会在脑海里闪过的念头。

我从5月份开始到现在,刷了290题了,这个题量虽然不算多,但是说实话还是积累了一些经验的。通过这个题量,我也能对自己擅长什么和不擅长什么有一个大致的概念了。

  • 我比较擅长dfs、二叉树、滑动窗口等类型的题目
  • 我不太擅长动态规划、位运算、堆、并查集等类型的题目

因此当我遇到一道新题目的时候,我会先尝试着把它转变成我熟悉的题目,拉到我熟悉的领域,再用我擅长的技能去解决它。

改造题目

理解题干信息,然后转换问题,改造题目。

题目要求我们计算移动矩阵中的石子使矩阵每个格子都有1颗石子的最小的步数

最小,最值问题,我们可以联想一下我们以前写过的类似的最值问题的解法,都有哪些可以解决的方式?

比如:

等等....

由我上方举的例子,可以看到,解决最值问题是有挺多种方式的。因此针对本题,我们看看是否也可以让它往上面的几个例子靠一靠?

移动,这个操作肯定需要一个起始点位和一个终止点位。

  • 石头数量大于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);
    };
}
相关推荐
海石1 小时前
1500分的题目,确实有实力,不过还是我略胜一筹
算法·leetcode
Jerry3 小时前
LeetCode 151. 反转字符串中的单词
算法
a1117765 小时前
LM 算法迭代过程动画演示(SLAM)
算法
头茬韭菜5 小时前
Context 的生死抉择:四层压缩、截断算法与 Session Memory
算法·ai
Jerry6 小时前
LeetCode 541. 反转字符串 II
算法
Jerry6 小时前
LeetCode 344. 反转字符串
算法
搞科研的小刘选手6 小时前
【香港大学主办&IEEE出版】第六届计算机视觉、应用与算法国际学术会议(CVAA 2026)
算法·计算机视觉·应用·学术会议
Java小白笔记6 小时前
Codex config.toml配置实战指南
人工智能·算法·chatgpt·ai编程·集成学习
王老师青少年编程6 小时前
2026年6月GESP真题及题解(C++七级):消消乐
数据结构·c++·算法·真题·gesp·2026年6月