T1320.二指输入的的最小距离
dp, https://leetcode.cn/problems/minimum-distance-to-type-a-word-using-two-fingers/

二指输入法定制键盘在 X-Y 平面上的布局如上图所示,其中每个大写英文字母都位于某个坐标处。
- 例如字母 A 位于坐标 (0,0),字母 B 位于坐标 (0,1),字母 P 位于坐标 (2,3) 且字母 Z 位于坐标 (4,1)。
给你一个待输入字符串 word,请你计算并返回在仅使用两根手指的情况下,键入该字符串需要的最小移动总距离。
坐标(x1,y1)和 (x2,y2) 之间的 距离 是 |x1 - x2| + |y1 - y2|。
注意,两根手指的起始位置是零代价的,不计入移动总距离。你的两根手指的起始位置也不必从首字母或者前两个字母开始。
示例 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]都是一个大写英文字母。
状态定义
我们定义
:当前已经输入到了 word的第个字符(从 0 开始)。 :第一根手指当前所在的字母下标(0-25 代表 A-Z,26 代表尚未放在键盘上)。 :第二根手指当前所在的字母下标(0-25 代表 A-Z,26 代表尚未放在键盘上)。
转移逻辑
假设我们已经输入了前 word[k]):
移动第一根手指:从位置
移动到 word[k],第二根手指留在不动。 - 新状态:
- 代价:
- 新状态:
移动第二根手指:从位置
移动到 word[k],第一根手指留在不动。 - 新状态:
- 代价:
注意:根据规则,如果手指从初始状态(26)开始移动,代价为 0。
Python 代码实现
为了节省空间,我们注意到
只依赖于 ,所以可以用滚动数组的思想,只保留当前状态的二维矩阵。 - 新状态:
class Solution:
def minimumDistance(self, word: str) -> int:
n = len(word)
# 计算曼哈顿距离的辅助函数
def get_dist(p1, p2):
if p1 == 26 or p2 == 26: # 26 代表手指还没放在键盘上,代价为 0
return 0
r1, c1 = divmod(p1, 6)
r2, c2 = divmod(p2, 6)
return abs(r1 - r2) + abs(c1 - c2)
# 单词转为数字 0-25
chars = [ord(c) - ord('A') for c in word]
# 初始化 DP 表,dp[i][j] 表示一根手指在 i,另一根在 j 的最小代价
# 初始时,两根手指都在 26 位置,距离为 0
dp = {(26, 26): 0}
for char_idx in chars:
new_dp = {}
for (f1, f2), dist in dp.items():
# 选择 1: 移动第一根手指到 char_idx
d1 = dist + get_dist(f1, char_idx)
if (char_idx, f2) not in new_dp or d1 < new_dp[(char_idx, f2)]:
new_dp[(char_idx, f2)] = d1
# 选择 2: 移动第二根手指到 char_idx
d2 = dist + get_dist(f2, char_idx)
if (f1, char_idx) not in new_dp or d2 < new_dp[(f1, char_idx)]:
new_dp[(f1, char_idx)] = d2
dp = new_dp
return min(dp.values())为什么这种定义更容易理解?
- 具象化:它完整记录了两个“实体”(手指)的位置。你可以想象两个点在棋盘上跳动。
- 无跳跃逻辑:每一步迭代都对应输入
word的一个字符,逻辑链条非常完整:word[0]敲完了,手指在哪里?word[1]敲完了,手指在哪里?- ...以此类推。
- 自然处理起始代价:通过引入第 27 个位置(索引 26),把“手指还没放上去”这个特殊情况变成了普通的坐标计算,不需要在循环里写复杂的
if-else判断。
复杂度分析
- 时间复杂度:
,其中 是单词长度, 是字母表大小(26)。虽然理论上是 ,但实际上在每一层 ,其中一根手指必然在 word[k-1]上,所以有效的状态数其实只有个。 - 空间复杂度:
。使用字典或二维数组存储当前状态。即使不优化掉 维度,空间复杂度 也在可接受范围内。
这是一个典型的动态规划(DP)问题。
核心思路:
状态定义: 当我们键入字符串
word的第个字符时,其中一根手指必然位于 word[i]。因此,我们只需要记录另一根手指的位置即可。 定义dp[j]为:键入当前字符后,另一根手指位于字母时的最小移动距离。 其中 的取值范围是 (代表 A-Z),额外增加一个状态(例如 )表示这根手指尚未放置在键盘上(起始距离为 0)。 状态转移: 假设当前键入到了
word[i-1],现在要键入word[i]。 设prev = word[i-1],curr = word[i]。 在处理curr之前,我们的状态是dp[other],表示一根手指在prev,另一根在other。 键入curr有两种选择:- 移动已经在
prev的手指:从prev移动到curr。另一根手指位置不变。 新距离 =dp[other] + distance(prev, curr) - 移动在
other的手指:从other移动到curr。原本在prev的手指现在变成了“另一根手指”。 新距离 =dp[other] + distance(other, curr)
- 移动已经在
坐标映射: 字母对应的数字为
n = ord(char) - ord('A')。 行坐标:r = n // 6,列坐标:c = n % 6。Python 代码实现:
class Solution:
def minimumDistance(self, word: str) -> int:
# 计算两个字母下标之间的曼哈顿距离
def get_dist(idx1, idx2):
if idx1 == 26 or idx2 == 26:
return 0
r1, c1 = divmod(idx1, 6)
r2, c2 = divmod(idx2, 6)
return abs(r1 - r2) + abs(c1 - c2)
# 将单词转换为 0-25 的数字序列
nums = [ord(c) - ord('A') for c in word]
# dp[j] 表示当一根手指在当前字符上时,另一根手指在字母 j 上的最小距离
# 初始状态:第一根手指在 nums[0],第二根手指还没放(用 26 表示)
# 此时总距离为 0
dp = {26: 0}
for i in range(len(nums) - 1):
curr_pos = nums[i]
next_pos = nums[i+1]
new_dp = {}
for other_pos, total_dist in dp.items():
# 情况 1:移动原本在 curr_pos 的手指去 next_pos
# 另一根手指位置不变,仍为 other_pos
d1 = total_dist + get_dist(curr_pos, next_pos)
if d1 < new_dp.get(other_pos, float('inf')):
new_dp[other_pos] = d1
# 情况 2:移动原本在 other_pos 的手指去 next_pos
# 另一根手指位置变为 curr_pos
d2 = total_dist + get_dist(other_pos, next_pos)
if d2 < new_dp.get(curr_pos, float('inf')):
new_dp[curr_pos] = d2
dp = new_dp
return min(dp.values())复杂度分析:
- 时间复杂度:
,其中 是字符串的长度, 是字母表的大小(26)。在每一步中,我们最多遍历 26 个状态。 ,效率非常高。 - 空间复杂度:
。我们只维护了当前步和上一步的状态,空间占用极小。
关键点解释:
- 初始零代价:代码中通过
if idx1 == 26 or idx2 == 26: return 0完美处理了“手指起始位置零代价”的规则。当第二根手指从未被使用时,它从位置 26 移动到任何地方的距离都是 0。 - 状态压缩:由于每一位字符都必须被按下,所以一根手指的位置是确定的(就是当前的
word[i]),这使得我们可以把二维 DP 降阶为一维。