自由之路:拨动表盘ring,按出密码key,所需最小步数或最小代价是多少?
提示:dfs的经典运用
文章目录
题目
电子游戏“辐射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步进行拼写,也就是按下button按钮。
因此最终的输出是 4。
示例2:
输入: ring = “godding”, key = “godding”
输出: 13
二、解题
观察,ring中可能有重复的字符,因此我们在按之前,要旋转多少步,肯定是旋转最少的步骤,
不妨设,你现在12点中方向对着ring中的i位置【比如g】,ring总共有N个字符【godding,N=7】,
你下次要旋转到目标字符(key[k])【gd中要寻找去对d字符的话】在ring中j位置【在godding中,j=2是最近的位置】,
那你旋转的最小代价其实很好算:j-i的绝对值即可
当然,你不能排除往左(逆时针找)找的情况,也就是先向左走i个位置,再顺着N-1往左走N-j个位置
看图:
因此,我们知道了ring的长度N,知道了此刻ring对准12点方向的位置是ring的i,则求下一个key[k]在ring中的位置j以后,我们可以算出最小代价为:min(|j-i|,i-0 + N-j),即顺时针(右边)找,逆时针(左边)找的的代价最小值:
//假设12点顶针在i处,需要将j转到顶针处,请问需要旋转多少步,实际上不是顺时针就是逆时针
//如果str的长为N,则从i转到j有两条路:
//顺时针:|j-i|就是步数
//逆时针先去0,再绕到N-1那,|N-j|+|i-0|就是
//也就是N-max(i,j)+min(i,j)-0,很简单的一个公式
public static int dial(int i, int j, int N){
return Math.min(Math.abs(i - j),
N - Math.max(i, j) + Math.min(i, j));
}
本题的解题算法大流程是这样的:
最开始,观察ring,12点指向0字符,也就是说i=0,我们将这个目前被对齐12点的位置i改称preIndex,
你有一个key字符串,肯定第一次要找哪个字符呢?自然找key[0],找哪个字符,我们定义为找keyIndex那个,即第一次从keyIndex=0开始找;
那很显然,来一个字符key[keyIndex],ring中可能存在多个key[keyIndex]字符,
那我们需要挨个都算一下代价,然后取最小代价,
因为你不知道究竟拨动到哪个字符更近?拨动的代价更小?
那就得在宏观调度中,挨个遍历一下所有可能的位置,然后取最小代价。
【也就是dfs一条道找,找完一个j,算一次代价,然后每次找完都更新最小代价】。
这就涉及到我们要摸清楚ring中究竟哪些位置j是key[keyIndex]字符的位置,也就是找到可能存在的j,
这些j,自然要用列表保存在哈希表中:
map:key是字符,value取这个key字符对应的ring中的所有下标【列表】
HashMap<Character, List<Integer>> map = new HashMap<>();
有了这个map
那么要找下一个字符key[keyIndex]
就只需要遍历ring中key[keyIndex]字符对应的位置下标j们,拨动ring,从preIndex去j,算一个拨动代价,按动button的代价,然后+ 再找下一个字符的最小代价。
与其他的位置j得到的代价,相比,取最小代价返回。
注意,当keyIndex=N之后,没有字符,代价为0,返回即可,整体返回需要int。
仔细领略下面代码:从i位置拨动,找key中keyIndex字符在ring中的位置j们,计算每一个j的代价,对比最小返回
//递归流程:
//从顶针i出发,去拨动出key的keyIndex处的字符,
// 随身携带map,key,和N,ring的个数,表盘字符个数,能得到的最小代价是
public static int minStep(int i, int keyIndex, char[] key,
HashMap<Character, List<Integer>> map, int N){
if (keyIndex == key.length) return 0;//搞定了所有不要代价返回
//从顶针i位置去拨动,到j,这些j位置是map中放的key[keyIndex]所记录的位置,DFS遍历找到所有情况
//然后计算代价的最小值
int min = Integer.MAX_VALUE;//来到每个顶针i,都只要i到j的最小值
for(Integer j : map.get(key[keyIndex])){
//枚举所有可能的位置
int cost = 1 + dial(i, j, N) +
minStep(j, keyIndex + 1, key, map, N);//搞定keyIndex这个字符代价+后续字符的代价
min = Math.min(min, cost);//每个j位置要代价的最低值
}
//搞定以后,返回min
return min;
}
主函数,先把ring各个字符的位置们放入map
然后调用递归
//主函数,拿着ring和key处理
public static int findMinSteps(String ring, String key){
char[] r = ring.toCharArray();
char[] k = key.toCharArray();
int N = r.length;
HashMap<Character, List<Integer>> map = new HashMap<>();
for (int i = 0; i < N; i++) {
if (!map.containsKey(r[i])) map.put(r[i], new ArrayList<>());
map.get(r[i]).add(i);//直接抽出列表,添加就行了
}
return minStep(0, 0, k, map, N);
}
修改暴力递归为傻缓存代码:
//改为记忆化搜索DP
//递归流程:
//从顶针i出发,去拨动出key的keyIndex处的字符,随身携带map,key,和N,ring的个数,表盘字符个数,能得到的最小代价是
public static int minStepDP(int i, int keyIndex, char[] key, HashMap<Character, List<Integer>> map, int N,
int[][] dp){
if (dp[i][keyIndex] != -1) return dp[i][keyIndex];
if (keyIndex == key.length) {
dp[i][keyIndex] = 0;
return dp[i][keyIndex];//搞定了所有不要代价返回
}
//从顶针i位置去拨动,到j,这些j位置是map中放的key[keyIndex]所记录的位置,DFS遍历找到所有情况
//然后计算代价的最小值
int min = Integer.MAX_VALUE;//来到每个顶针i,都只要i到j的最小值
for(Integer j : map.get(key[keyIndex])){
//枚举所有可能的位置
int cost = 1 + dial(i, j, N) + minStepDP(j, keyIndex + 1, key, map, N, dp);//搞定keyIndex这个字符代价+后续字符的代价
min = Math.min(min, cost);//每个j位置要代价的最低值
}
dp[i][keyIndex] = min;
//搞定以后,返回min
return dp[i][keyIndex];
}
//主函数,拿着ring和key处理
public static int findMinStepsDP(String ring, String key){
char[] r = ring.toCharArray();
char[] k = key.toCharArray();
int N = r.length;
int M = k.length;
HashMap<Character, List<Integer>> map = new HashMap<>();
for (int i = 0; i < N; i++) {
if (!map.containsKey(r[i])) map.put(r[i], new ArrayList<>());
map.get(r[i]).add(i);//直接抽出列表,添加就行了
}
//i从0---N-1,出发,拨到key的keyIndex位置字符从0--M,最小的代价是多少,返回dp[0][0]
int[][] dp = new int[N][M + 1];
for (int i = 0; i < N; i++) {
for (int j = 0; j <= M; j++) {
dp[i][j] = -1;//没有填写过
}
}
return minStepDP(0, 0, k, map, N, dp);
}
测试代码:
public static void test(){
String ring = "abcde";//N=5
String key = "ac";//按一下a,转2步,按下c4
// char[] str = ring.toCharArray();
// System.out.println(dial(0, 3, str.length));
System.out.println(findMinSteps(ring, key));
System.out.println(findMinStepsDP(ring, key));
}
public static void main(String[] args) {
test();
}
总结
提示:重要经验:
1)熟悉本题,这是一个很常规的题目,循环拨动表盘,算最小代价,自然要遍历相同字符的可能所有位置j,取最小值返回
2)map记录字符的位置j们,这里要熟悉;