每日一题 力扣 1320. 二指输入的的最小距离 动态规划 C++ 题解

文章目录

题目描述

题目链接:力扣 1320. 二指输入的的最小距离

示例 1:

输入:word = "CAKE"

输出:3

解释:

使用两根手指输入 "CAKE" 的最佳方案之一是:

手指 1 在字母 'C' 上 -> 移动距离 = 0

手指 1 在字母 'A' 上 -> 移动距离 = 从字母 'C' 到字母 'A' 的距离 = 2

手指 2 在字母 'K' 上 -> 移动距离 = 0

手指 2 在字母 'E' 上 -> 移动距离 = 从字母 'K' 到字母 'E' 的距离 = 1

总距离 = 3
示例 2:

输入:word = "HAPPY"

输出:6

解释:

使用两根手指输入 "HAPPY" 的最佳方案之一是:

手指 1 在字母 'H' 上 -> 移动距离 = 0

手指 1 在字母 'A' 上 -> 移动距离 = 从字母 'H' 到字母 'A' 的距离 = 2

手指 2 在字母 'P' 上 -> 移动距离 = 0

手指 2 在字母 'P' 上 -> 移动距离 = 从字母 'P' 到字母 'P' 的距离 = 0

手指 1 在字母 'Y' 上 -> 移动距离 = 从字母 'A' 到字母 'Y' 的距离 = 4

总距离 = 6
提示:

2 <= word.length <= 300

每个 word[i] 都是一个大写英文字母。

思路简述

这道题的核心是求全局最优的移动路径,每一步的决策会直接影响后续的移动成本,是非常典型的动态规划适用场景。下面我们逐步拆解完整的思路推导:

状态定义

我们定义三维DP数组 dp[i][a][b],其含义为:处理完字符串中第 i 个字符后,左手落在对应编号 a 的字母上、右手落在对应编号 b 的字母上,此时累计的最小移动总距离

其中字母编号规则为:A-Z 对应 0-25,方便数组索引操作。

前置准备

由于键盘布局固定,我们可以用哈希表先预存每个字母对应的坐标,再封装一个距离计算函数,快速得到两个字母之间的曼哈顿距离,避免重复计算。

状态转移方程

当我们要处理第 i 个字符时,设当前需要按下的字母对应的编号为 c。上一步的状态是处理完第 i-1 个字符后,左手在 a、右手在 b,累计最小距离为 dp[i-1][a][b]

对于当前字符 c,我们只有两种合法决策,对应两条状态转移路径:

  1. 移动左手按下当前字符 c

    按下后,左手位置更新为 c,右手位置保持 b 不变。新增的移动成本为「上一步左手位置 a 到当前字符 c 的曼哈顿距离」。

    转移方程:dp[i][c][b] = min(dp[i][c][b], dp[i-1][a][b] + 距离(a, c))

  2. 移动右手按下当前字符 c

    按下后,右手位置更新为 c,左手位置保持 a 不变。新增的移动成本为「上一步右手位置 b 到当前字符 c 的曼哈顿距离」。

    转移方程:dp[i][a][c] = min(dp[i][a][c], dp[i-1][a][b] + 距离(b, c))

数组的初始化

首先将整个DP数组初始化为无穷大,代表初始时所有状态均不可达,避免干扰最小值的计算。

对于第 0 个字符(字符串的第一个字符),由于手指起始位置无代价,我们有两种初始选择:

  • 用左手按下第一个字符:左手位置固定为第一个字符的编号 f,右手可以放在任意位置,此时累计距离为 0,即 dp[0][f][k] = 0k 遍历 0-25)
  • 用右手按下第一个字符:右手位置固定为第一个字符的编号 f,左手可以放在任意位置,此时累计距离为 0,即 dp[0][k][f] = 0k 遍历 0-25)

最终结果

当处理完字符串的最后一个字符(下标为 n-1)后,所有可能的状态 dp[n-1][a][b]ab 遍历 0-25)中的最小值,就是我们要求的二指输入最小总移动距离。

代码实现

cpp 复制代码
class Solution {
public:
    // 预存键盘每个字母对应的坐标,5×6 固定布局
    unordered_map<char, pair<int, int>> hashi = {
        {'A',{0,0}},{'B',{0,1}},{'C',{0,2}},{'D',{0,3}},{'E',{0,4}},{'F',{0,5}},
        {'G',{1,0}},{'H',{1,1}},{'I',{1,2}},{'J',{1,3}},{'K',{1,4}},{'L',{1,5}},
        {'M',{2,0}},{'N',{2,1}},{'O',{2,2}},{'P',{2,3}},{'Q',{2,4}},{'R',{2,5}},
        {'S',{3,0}},{'T',{3,1}},{'U',{3,2}},{'V',{3,3}},{'W',{3,4}},{'X',{3,5}},
        {'Y',{4,0}},{'Z',{4,1}}
    };

    // 计算两个字母之间的曼哈顿距离
    int dist(char x, char y) {
        auto [x1,y1] = hashi[x];
        auto [x2,y2] = hashi[y];
        return abs(x1 - x2) + abs(y1 - y2);
    }

    int minimumDistance(string word) {
        int n = word.size();
        // 边界处理:单个字符无需移动,距离为0
        if (n <= 1) return 0;

        // 三维DP数组定义:dp[i][a][b] 处理完第i个字符,左手在a,右手在b的最小距离
        const int INF = INT_MAX / 2; // 防加法溢出,设置合理的无穷大
        int dp[n][26][26];

        // 初始化:所有状态先设为无穷大(不可达)
        for (int i = 0; i < n; i++)
            for (int a = 0; a < 26; a++)
                for (int b = 0; b < 26; b++)
                    dp[i][a][b] = INF;

        // 初始状态:处理第0个字符,起始位置无代价
        char firstChar = word[0];
        int firstIdx = firstChar - 'A';
        for (int k = 0; k < 26; k++) {
            dp[0][firstIdx][k] = 0;  // 左手按第一个字符,右手任意位置
            dp[0][k][firstIdx] = 0;  // 右手按第一个字符,左手任意位置
        }

        // 遍历字符串,逐字符进行状态转移
        for (int i = 1; i < n; i++) {
            char currChar = word[i];
            int currIdx = currChar - 'A';

            // 遍历上一步所有可能的左右手状态
            for (int a = 0; a < 26; a++) {
                for (int b = 0; b < 26; b++) {
                    // 跳过不可达的状态
                    if (dp[i-1][a][b] == INF) continue;

                    // 选择1:移动左手按下当前字符
                    dp[i][currIdx][b] = min(dp[i][currIdx][b],
                                      dp[i-1][a][b] + dist('A' + a, currChar));

                    // 选择2:移动右手按下当前字符
                    dp[i][a][currIdx] = min(dp[i][a][currIdx],
                                      dp[i-1][a][b] + dist('A' + b, currChar));
                }
            }
        }

        // 提取结果:最后一个字符处理完后,所有状态中的最小值
        int ret = INF;
        for (int a = 0; a < 26; a++)
            for (int b = 0; b < 26; b++)
                ret = min(ret, dp[n-1][a][b]);

        return ret;
    }
};

复杂度分析

  • 时间复杂度O(n * 26 * 26) = O(n)

    其中 n 是字符串 word 的长度。我们需要遍历字符串的每个字符(共 n 次),对于每个字符,需要遍历上一步所有 26*26 种左右手位置组合(26是大写字母总数,为固定常数),每个状态的转移都是 O(1) 的操作,因此整体时间复杂度为线性的 O(n)。

  • 空间复杂度O(n * 26 * 26) = O(n)

    我们开辟了三维数组 dp[n][26][26],其中 26 为固定常数,因此空间复杂度为线性的 O(n)。若需要进一步优化空间,可使用滚动数组(仅保留上一步的状态),将空间复杂度降至 O(1),本实现为了贴合思路、保证可读性,保留了三维DP的完整结构。

踩坑记录

  1. 无穷大取值需避免整数溢出

    不能直接使用 INT_MAX 作为无穷大,因为状态转移中会给DP值加上距离,直接使用 INT_MAX 会导致加法后整数溢出,出现负数或错误结果。这里设置为 INT_MAX / 2,既保证远大于正常的最大总距离(字符串最长300,单次移动最大距离不超过10,总距离最大3000),又能完全避免溢出问题。

  2. 状态遍历必须覆盖所有可能的左右手组合

    每一步状态转移时,必须完整遍历上一步所有 26*26 种左右手位置组合,不能遗漏任何可达状态,否则会错过最优的转移路径,导致最终结果偏大。

  3. 结果变量的初始化必须正确

    最终用于存储结果的变量,必须初始化为无穷大,再遍历最后一步的所有状态取最小值。若初始值设置过小,会直接导致结果错误。

如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!

相关推荐
实心儿儿2 小时前
C++ —— C++11(2)
开发语言·c++
加油JIAX2 小时前
C++11特性
c++
wfbcg2 小时前
每日算法练习:LeetCode 76. 最小覆盖子串 ✅
算法·leetcode·职场和发展
Wect2 小时前
LeetCode 149. 直线上最多的点数:题解深度剖析
前端·算法·typescript
qianpeng8972 小时前
运动声源的到达结构仿真
算法
费曼学习法2 小时前
线段树:区间查询的"终极武器",一文看透高效范围统计
算法
wayz112 小时前
Day 2:线性回归原理与正则化
算法·机器学习·数据分析·回归·线性回归
QQ676580082 小时前
基于yolo26算法的水下目标检测图像数据集 海洋生物识别 海胆识别 海龟识别数据集 海洋生物监测与保护工作 潜水作业安全辅助系 水下环境感知第10408期
算法·目标检测·水下目标检测·海洋生物识别·海胆 海龟识别·海洋生物监测与保护工作·潜水作业安全辅助 水下环境感知
七颗糖很甜3 小时前
基于 OpenCV 的 FY2 云顶图云块追踪算法实现
人工智能·opencv·算法