首先看题目
电子游戏“辐射4”中,任务 “通向自由” 要求玩家到达名为 “Freedom Trail Ring” 的金属表盘,并使用表盘拼写特定关键词才能开门。
给定一个字符串 ring ,表示刻在外环上的编码;给定另一个字符串 key ,表示需要拼写的关键词。您需要算出能够拼写关键词中所有字符的最少步数。
最初,ring 的第一个字符与 12:00 方向对齐。您需要顺时针或逆时针旋转 ring 以使 key 的一个字符在 12:00 方向对齐,然后按下中心按钮,以此逐个拼写完 key 中的所有字符。
旋转 ring ****拼出 key 字符 key[i] ****的阶段中:
- 您可以将 ring 顺时针或逆时针旋转 一个位置 ,计为1步。旋转的最终目的是将字符串
ring的一个字符与12:00方向对齐,并且这个字符必须等于字符key[i]。 - 如果字符
key[i]已经对齐到12:00方向,您需要按下中心按钮进行拼写,这也将算作 1 步。按完之后,您可以开始拼写 key 的下一个字符(下一阶段), 直至完成所有拼写。 **
示例 1:**
输入: ring = "godding", key = "gd"
输出: 4
解释:
对于 key 的第一个字符 'g',已经在正确的位置, 我们只需要1步来拼写这个字符。
对于 key 的第二个字符 'd',我们需要逆时针旋转 ring "godding" 2步使它变成 "ddinggo"。
当然, 我们还需要1步进行拼写。
因此最终的输出是 4。
示例 2:
输入: ring = "godding", key = "godding"
输出: 13
首先,我们要思考题目的几个难点
- 如何旋转才能最快转到我们需要的字符
- 如果ring中出现了相同的字符,该选择哪一个字符才能保证后续的操作更少
- 如果出现了相同的操作如(当前圆环指针指向'G',需要移动到'D',之前计算过最短路劲,后续如果又出现了这样的操作),如何减少计算量。
接下来,我们在代码中在实现需求的过程中,想办法把之前的问题解决
function findRotateSteps(ring, key) {
计算出最短的路径
return 最短路径
}
接下来,我们解决第一个问题:如何旋转才能最快转到我们需要的字符 这时候,我们可以先把ring中的每一个字符都提取出来做一个映射
function findRotateSteps(ring, key) {
const keyMap = {}
for(let i=0;i<ring.length;i++){
const k = ring[i]
if(keyMap[k]){
keyMap[k].push(i)//如果ring中存在相同字符,也一并把下标存入映射表中
}else{
keyMap=[i]
}
}
return 最短路径
}
这样,我们就把ring中的每个字符都拆分出来并做了映射关系,要寻找制定字符的话,可以直接从映射表中取出下标进行计算。
接下来,我们尝试把计算字符之间的最短路径的方法写出来
/**
* 计算最短路径
* @param {*} ringI ring下标
* @param {*} keyI key下标
* @returns
*/
const dfs = (ringI,keyI){
//通过key的下标取出目标字符
const cur = key[keyI];
let res = Infinity;
//ring中可能存在相同字符,所以把映射表遍历一遍,看看有相同字符的情况下哪个距离更短
for(const targetI of keyMap[cur]){
//计算出两个不同方向的激励,并取最小值
let d1 = Math.abs(ringI-targetI)
let d2 = ring.length-d1
res = Math.min(d1, d2)
}
return res
}
这样,我们就把当前字符和目标字符的最短距离计算出来了,但是,我们的key可不止一个字符,我们还需要把后面每一个字符都走一遍,并计算出最短距离出来,所以,我们要继续完善我们的方法
/**
* 计算最短路径
* @param {*} ringI ring下标
* @param {*} keyI key下标
* @returns
*/
const dfs = (ringI,keyI){
const cur = key[keyI];
let res = Infinity;
for(const targetI of keyMap[cur]){
let d1 = Math.abs(ringI-targetI)
let d2 = ring.length-d1
//这里,我们把当前计算单一字符的最短路径记录下来
const curMin = Math.min(d1, d2)
//因为已经计算了一个字符,当前指针已经指向了targetI,我们就以这个作为下标计算后面的值
res = curMin + dfs(targetI, keyI+1)
}
return res
}
这样,最短路径就计算出来了。但是,因为ring中可能存在相同字符,这样我们遍历完映射表keyMap时,后面出现的字符的计算结果总会覆盖前面计算的结果,但是,这两个结果我们只能取最小值,取完最小值后,我们把代码片段放回原函数中看看
function findRotateSteps(ring, key) {
// 用于存储ring中的索引信息
const keyMap = {};
for(let i = 0; i < ring.length; i++) {
const k = ring[i];
if(keyMap[k]) {
keyMap[k].push(i)
} else {
keyMap[k] = [i]
}
}
/**
* 递归计算最短路径
* @param {*} ringI ring下标
* @param {*} keyI key下标
* @returns
*/
const dfs = ( ringI, keyI ) => {
if(keyI == key.length) return 0;//key下标超出时,结束遍历,并向上传递结果
const cur = key[keyI];
// 返回的结果
let res = Infinity;
for(const targetI of keyMap[cur]) {
// 正向位置
let d1 = Math.abs(ringI - targetI),
d2 = ring.length - d1;
const curMin = Math.min(d1, d2)
// keyI一直在增加,总会超出原本key的长度
res = Math.min(res, curMin + dfs(targetI, keyI+1))
}
return res;
}
return dfs(0,0) + key.length;
};
这时候,我们已经解决了问题1和问题2了,接下来,我们想办法解决问题3,如何避免重复的遍历计算。
什么样的情况会出现重复的遍历呢?我来做一个假设
假设ring="abcsefgb",key="bcdef"。
因为ring中有两个'b',每个'b'→'c'的距离不同,,但是'c'→'d'→'e'→'f'的距离是固定的,这段距离在我们的方法函数中就不可避免的被重复计算了,为了避免重复的计算,我们可以把这个计算结果保存起来,如果后续遇到了相同的运算,我们可以直接把结果返回,接下来,我们在代码中去实现一下。
function findRotateSteps(ring, key) {
const keyMap = {};
for(let i = 0; i < ring.length; i++) {
const k = ring[i];
if(keyMap[k]) {
keyMap[k].push(i)
} else {
keyMap[k] = [i]
}
}
// 创建一个二维数组,用于存放已经计算过的遍历结果
const memo = new Array(ring.length);
for(let i = 0; i < ring.length; i++) {
//因为遍历计算的结果不会小于0,所以给数组的每一项设置为-1
memo[i] = new Array(key.length).fill(-1)
}
const dfs = ( ringI, keyI ) => {
if(keyI == key.length) return 0;
//如果遇到了已经计算过的遍历结果,直接返回该结果
if( memo[ringI][keyI] !== -1 ) return memo[ringI][keyI]
const cur = key[keyI];
let res = Infinity;
for(const targetI of keyMap[cur]) {
let d1 = Math.abs(ringI - targetI),
d2 = ring.length - d1;
const curMin = Math.min(d1, d2)
res = Math.min(res, curMin + dfs(targetI, keyI+1))
}
//把当前遍历计算结果存入memo中
memo[ringI][keyI] = res;
return res;
}
return dfs(0,0) + key.length;
};
这样,我们就算是把这个最短路径给计算出来了
题目来源:力扣(LeetCode)
链接:leetcode-cn.com/problems/fr…