文章目录

题目描述
题目链接:力扣 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,我们只有两种合法决策,对应两条状态转移路径:
-
移动左手按下当前字符
c按下后,左手位置更新为
c,右手位置保持b不变。新增的移动成本为「上一步左手位置a到当前字符c的曼哈顿距离」。转移方程:
dp[i][c][b] = min(dp[i][c][b], dp[i-1][a][b] + 距离(a, c)) -
移动右手按下当前字符
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] = 0(k遍历 0-25) - 用右手按下第一个字符:右手位置固定为第一个字符的编号
f,左手可以放在任意位置,此时累计距离为 0,即dp[0][k][f] = 0(k遍历 0-25)
最终结果
当处理完字符串的最后一个字符(下标为 n-1)后,所有可能的状态 dp[n-1][a][b](a、b 遍历 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的完整结构。
踩坑记录
-
无穷大取值需避免整数溢出
不能直接使用
INT_MAX作为无穷大,因为状态转移中会给DP值加上距离,直接使用INT_MAX会导致加法后整数溢出,出现负数或错误结果。这里设置为INT_MAX / 2,既保证远大于正常的最大总距离(字符串最长300,单次移动最大距离不超过10,总距离最大3000),又能完全避免溢出问题。 -
状态遍历必须覆盖所有可能的左右手组合
每一步状态转移时,必须完整遍历上一步所有 26*26 种左右手位置组合,不能遗漏任何可达状态,否则会错过最优的转移路径,导致最终结果偏大。
-
结果变量的初始化必须正确
最终用于存储结果的变量,必须初始化为无穷大,再遍历最后一步的所有状态取最小值。若初始值设置过小,会直接导致结果错误。
如果这篇博客对你有帮助,别忘了点赞支持一下~也可以收藏起来,方便后续刷题复习时随时翻看。要是能顺手点个关注,爱弥斯还能得到漂泊者批准的游戏时间哦!
