Hi 你们好,我是张小猪。欢迎来到『宝宝也能看懂』系列之 leetcode 周赛题解。git
这里是第 171 期的第 4 题,也是题目列表中的第 1320 题 -- 『二指输入的的最小距离』github
二指输入法定制键盘在 XY 平面上的布局如上图所示,其中每一个大写英文字母都位于某个坐标处,例如字母 A 位于坐标 (0,0),字母 B 位于坐标 (0,1),字母 P 位于坐标 (2,3) 且字母 Z 位于坐标 (4,1)。算法
给你一个待输入字符串 word
,请你计算并返回在仅使用两根手指的状况下,键入该字符串须要的最小移动总距离。坐标 (x1,y1) 和 (x2,y2) 之间的距离是 |x1 - x2| + |y1 - y2|。shell
注意,两根手指的起始位置是零代价的,不计入移动总距离。你的两根手指的起始位置也没必要从首字母或者前两个字母开始。segmentfault
示例 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
示例 3:优化
输入:word = "NEW" 输出:3
示例 4:spa
输入:word = "YEAR" 输出:7
提示:code
2 <= word.length <= 300
word[i]
都是一个大写英文字母。HARD
题目的内容很是简单,就是用两根手指,输入一串给定的字符串,要求返回手指移动的最短距离。其中键盘布局已经经过图片给出来了。
看完题目以后,小猪蹄子一蹬,尾巴一翘,由于一般描述很简单的题目,多是真的特别简单,又或者可能就是思考起来比较复杂。而这道题的难度是 HARD...是时候再玩几盘吃鸡放松一下了 是时候认真了呢,哼唧 >.<
先不思考题目逻辑,单看需求,第一个很是明显的就是咱们须要知道手指在键盘上从一个字母移动到另外一个字母的开销。那咱们就先来实现这一个 helper 方法吧。
因为英文字母是按照顺序排列的,因此咱们能够很容易的想到取 char code 来作计算。结合题目给出的移动距离公式 |x1 - x2| + |y1 - y2|
,咱们能够很天然的想到,把距离拆解成横向和纵向两个方向计算,最后再求和。因为每一行的字母数量是固定的,因此对于纵向距离来讲,咱们能够分别求出两个字母所在的行数,而后相减便可。而对于横向距离,一样由于每一行的字符数量固定,咱们能够直接进行取模运算,而后相减便可。
这里须要注意的一点就是别忘了减完以后要取绝对值。具体代码可能相似下面这样:
function distance(a, b) { const x = word.charCodeAt(a) - 65; const y = word.charCodeAt(b) - 65; return Math.abs((x % 6) - (y % 6)) + Math.abs(((x / 6) << 0) - ((y / 6) << 0)); }
接下来就到了核心问题,若是肯定解法的逻辑。这里若是一时之间以为无从下手的话,能够先举几个例子看看:
distance(a, a)
,也就是 0
。distance(a, a) + distance(b, b)
,也是 0
。若是有三个字符,那么咱们的开销有多是如下几种状况:
distance(a, a) + distance(b, b) + distance(b, c)
。distance(a, a) + distance(b, b) + distance(a, c)
。distance(a, a) + distance(a, b) + distance(c, c)
。若是有四个字符,那么咱们的开销有多是如下几种状况:
distance(a, a) + distance(b, b) + distance(b, c) + distance(c, d)
。distance(a, a) + distance(b, b) + distance(b, c) + distance(a, d)
。distance(a, a) + distance(b, b) + distance(a, c) + distance(c, d)
。distance(a, a) + distance(b, b) + distance(a, c) + distance(b, d)
。distance(a, a) + distance(a, b) + distance(c, c) + distance(c, d)
。distance(a, a) + distance(a, b) + distance(c, c) + distance(b, d)
。distance(a, a) + distance(a, b) + distance(b, c) + distance(d, d)
。小伙伴们能够稍微仔细的看一下上面的几个例子,我把顺序写的挺故意的,就是想更轻易的展现其中的信息。下面罗列一下咱们能够发现的一些事情:
第三个信息就是,咱们若是已经使用了两根手指(嗯?怎么怪怪的...),那么继续下一步的最优解可能会有两种状况:
上面四个信息可能看起来很是的不值一提,不过这是咱们后续推导的根基。接下来咱们进行正式的逻辑部分。
咱们上面获得了一些步骤之间手指的基本信息,那么这时候可能会有小伙伴提出这样的设想。咱们直接把第一根手指放在开始,第二根手指放在离它最远的地方。而后每次字符移动的时候用离目标最近的一根手指移动,这样是否是就解决了呢?有了这种设想以后,咱们能够用题目给的例子来试一下,例如 "CAKE", "HAPPY" 等,咱们会发现能获得正确的结果。那么这样看起来,这道题彷佛很简单鸭。
别着急,咱们再来看另一个例子 "ZKNBZ"。按照上面的思路,咱们会把手指放在 "Z" 和离它最远的 "K",而后根据离目标最近的原则,继续作如下的移动 "Z" => "N", "N" => "B", "B" => "Z"。最终移动的总距离为 8。但是,咱们回头看看这个方案,仍旧把手指放在 "Z" 和 "K",而后进行如下的移动 "K" => "N", "N" => B","Z" => "Z"。这样最终的总移动距离为 6。这也就证实了,咱们前面的那种设想是有问题的。
那么问题出在哪里呢?这里引入两个概念,局部最优解和全局最优解。前者指的是在当前情况下咱们获得的最优解,例如上面设想中的每一次移动都使用距离最近的手指,这样在当前这一步咱们确实获得了最优解。然后者指的是在总体流程结束后咱们获得的最优解,例如上面例子中最后获得了 6 这个解。那么很显然,咱们获得了局部最优解,但是没有获得全局最优解。
相信有的小伙伴这时候已经反应过来了,上面的那种设想不就是一种贪心算法的实现么,也就是企图用局部最优解最终推导出全局最优解。但是这是有条件的,对于咱们这里的题目,局部最优解就不能推导出全局最优解。因此,咱们应该用动态规划的方式尝试基于步骤之间的关系来推导出全局最优解。
那么接下来咱们来看看步骤之间的关系吧。这里先解释后面会用到的几个数据的意思:
x
和 y
,其中 x < y
,那么对于咱们从位置 0
开始一直到 y
这一段字符串,当在一根手指放在 x
的条件下时,最优解值称为 dp[x][y]
。x
移动到 y
的距离称为 distance(x, y)
。0
移动到位置 1
,并持续移动到位置 x
,所须要的距离的总和称为 sum(x)
。那么接下来咱们来看看 dp[x][y]
的状况,咱们能够先举几个具体的例子方便思考:
dp[3][6]
,它的来源只多是 dp[3][5] + distance(5, 6)
。dp[5][6]
,它的来源就比较复杂了,可能来自于 dp[0][5] + distance(0, 6)
,也多是 dp[1][5] + distance(1, 6)
,一直到 dp[4][5] + distance(4, 6)
,另外千万别忘了还多是 sum(5) + distance(6, 6)
。看到这里小伙伴们有没有发现咱们最开始举的几个例子和获得的那几个信息的意义啦。正所谓古语有云:重要信息,不要钱 4 个,嘿嘿 >.<
那么把上面这个例子再抽象成 dp[x][y]
,咱们能够获得如下计算方法:
dp[x][y] = x !== y - 1 ? dp[x][y - 1] + distance(y - 1, y) : Math.min(sum(x), dp[0][x] + distance(0, y), dp[1][x] + distance(1, y), ..., dp[x - 1][x] + distance(x - 1, y))
这时候咱们看看题目所须要求的那个结果是什么,若是用上面咱们的几个值来表达,其实就是 Math.min(sum(n - 1), dp[0][n - 1], dp[1][n - 1], ..., dp[n - 2][n - 1])
。
如今咱们有了递推公式,也有了最终结果的取值,咱们只须要用代码实现其中的计算便可。具体流程以下:
dp
数组和 sum
数组。dp
数组中咱们所须要的值。基于这个流程,咱们能够实现相似这样的代码:
const minimumDistance = word => { const LEN = word.length; const dp = new Array(LEN - 1); const sum = new Uint16Array(LEN); for (let i = 1; i < LEN; ++i) { dp[i - 1] = new Uint16Array(LEN); sum[i] += sum[i - 1] + distance(i - 1, i); } for (let j = 2; j < LEN; ++j) { let min = sum[j - 1]; for (let i = 0; i < j - 1; ++i) { const min2 = dp[i][j - 1] + distance(i, j); if (min2 < min) min = min2; dp[i][j] = dp[i][j - 1] + distance(j - 1, j); } dp[j - 1][j] = min; } let min = sum[LEN - 1]; for (let i = 0; i < LEN - 1; ++i) { if (dp[i][LEN - 1] < min) min = dp[i][LEN - 1]; } return min; function distance(a, b) { const x = word.charCodeAt(a) - 65; const y = word.charCodeAt(b) - 65; return Math.abs((x % 6) - (y % 6)) + Math.abs(((x / 6) << 0) - ((y / 6) << 0)); } };
按照惯例,咱们仍是尝试把 dp
这个二维数组优化为一个一维数组。顺便再吐槽一下 JS 中申明多维数组真是麻烦。
这里的优化就很是简单了,咱们上面的 dp[x][y]
中,y
表示的是当前右侧手指的位置。这个位置其实也就是当前推演进行到了哪一位字符。而这个推演实际上是一轮一轮进行的,与咱们的循环直接绑定。而且咱们后续的计算中再也不须要更早期的推演值了。说到这里,相信小伙伴们也发现了,咱们其实根本不须要这一维度的值,由于咱们只须要记录最新的值便可。
因而瓜熟蒂落的,咱们能够获得相似下面的代码:
const minimumDistance = word => { const LEN = word.length; const dp = new Uint16Array(LEN - 1); const sum = new Uint16Array(LEN); for (let i = 1; i < LEN; ++i) { sum[i] += sum[i - 1] + distance(i - 1, i); } for (let j = 2; j < LEN; ++j) { let min = sum[j - 1]; for (let i = 0; i < j - 1; ++i) { const min2 = dp[i] + distance(i, j); dp[i] = dp[i] + distance(j - 1, j); if (min2 < min) min = min2; } dp[j - 1] = min; } return Math.min(...dp, sum[LEN - 1]); function distance(a, b) { const x = word.charCodeAt(a) - 65; const y = word.charCodeAt(b) - 65; return Math.abs((x % 6) - (y % 6)) + Math.abs(((x / 6) << 0) - ((y / 6) << 0)); } };
这段代码目前跑出了 56ms,暂时 beats 100%。
在上面的分析过程当中,咱们提到了局部最优解、全局最优解、贪心算法、动态规划。不过这里咱们并无作详细的展开,只是点到为止。待往后具体的专题内容时候,咱们再详细的说吧。小伙伴们要是期待的话就多催更哟,催多了小猪才有动力爆肝鸭,哈哈哈哈 >.<
另外,其实动态规划的思路能够有不少种,例如自上而下、自下而上其实均可以。而且递推的内容和具体的逻辑也和咱们设置的 dp
数组有关。欢迎小伙伴们积极补充其余的方案哟。Yo~ Put your hands up~