视频游戏“辐射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
的下一个字符(下一阶段), 直至完成所有拼写。示例:
输入: ring = “godding”, key = “gd”
输出: 4
解释:
对于 key 的第一个字符 ‘g’,已经在正确的位置, 我们只需要1步来拼写这个字符。 对于 key 的第二个字符
‘d’,我们需要逆时针旋转 ring “godding” 2步使它变成 “ddinggo”。 当然, 我们还需要1步进行拼写。
因此最终的输出是 4。 提示:ring 和 key 的字符串长度取值范围均为 1 至 100;
两个字符串中都只有小写字符,并且均可能存在重复字符;
字符串 key 一定可以由字符串 ring 旋转拼出。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/freedom-trail
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
一开始以为只能王一个方向转想这么简单怎么是困难题
后来发现要求往左右两个方向都可以转…那就得dp了
一开始想的是自底向上的方法。定义矩阵dp[k.length() + 1][ring.length()]
dp[i][j]表示初始ring指向j的情况下要匹配key中第i个和以后所有字母所需的最短旋转次数(按钮次数是固定的,等于key的长度,最后再加)。
为了方便定义一个计算旋转次数的函数:
int minRotate(int from, int to, int l) {
//字符串长度为l 从from到to最短需要转几下
int diff = Math.abs(from - to);
return Math.min(diff, l - diff);
}
一开始想到的递推是,对于dp[pk][pr],把key中字母所有可能的匹配试一下,看看哪一个最后的旋转次数最少。维克能够快速找到所有匹配的字母,先进行一下预处理,把每个字母所在的位置放到一个数据结构中:
List<Integer>[] ringLetter = new List[26];
for(int i = 0; i < 26; ++i) {
ringLetter[i] = new ArrayList<>();
}
for(int i = 0; i < ring.length(); ++i) {
ringLetter[ring.charAt(i) - 'a'].add(i);
}
递推的代码:
int k = key.charAt(pk) - 'a';
for(int pr = 0; pr < ring.length(); ++pr) {
int min = Integer.MAX_VALUE;
for(int i : ringLetter[k]) {
min = Math.min(min, minRotate(i, pr, rl) + dp[pk + 1][i]);
}
dp[pk][pr] = min;
}
完整代码:
class Solution {
int minRotate(int from, int to, int l) {
//字符串长度为l 从from到to最短需要转几下
int diff = Math.abs(from - to);
return Math.min(diff, l - diff);
}
public int findRotateSteps(String ring, String key) {
//范围都不大100 * 100也就10000 允许顺时针或逆时针就比较麻烦了 先试试迭代的dp
int[][] dp = new int[key.length() + 1][ring.length()];//dp[i][j] 初始ring指向j的情况下要匹配key中第i个字母所需的最短距离
//不考虑要按那几下最后再加
List<Integer>[] ringLetter = new List[26];
int rl = ring.length();
for(int i = 0; i < 26; ++i) {
ringLetter[i] = new ArrayList<>();
}
for(int i = 0; i < ring.length(); ++i) {
ringLetter[ring.charAt(i) - 'a'].add(i);
}
for(int pk = key.length() - 1; pk >= 0; --pk) {
int k = key.charAt(pk) - 'a';
for(int pr = 0; pr < ring.length(); ++pr) {
int min = Integer.MAX_VALUE;
for(int i : ringLetter[k]) {
min = Math.min(min, minRotate(i, pr, rl) + dp[pk + 1][i]);
}
dp[pk][pr] = min;
}
}
return dp[0][0] + key.length();
}
}
过了但是事件比较慢45ms 10%不到。尝试优化
第一个尝试是可以贪心地考虑这个问题,在考察sp中每一个元素地时候,如果这一步不用转,肯定是不转更好;如果不得不转,也只有可能是向左转或者向右转最近地两个之一。正确性证明简单说说吧
对于前半句
不转的dp值是dp[pk + 1][pr]
转的dp值是minRotate(i, pr, rl) + dp[pk + 1][i]
而对于dp[pk + 1][pr]来说,方案之一是先转到i 再接着往后转,所以
dp[pk + 1][pr] <= minRotate(i, pr, rl) + dp[pk + 1][i]
所以如果能不转就不需要考虑转的情形
能转转最近的应该和上面的证明差不多,懒得写了
用贪心的方式,更新的代码变为:
List<Integer> l = ringLetter[key.charAt(pk) - 'a'];
int lIndex = 0;
//想开点 其实每一步是可以贪心的 能不转就不转 能转的话也只需要考察最近的左右 正确性证明缓缓
for(int pr = 0; pr < ring.length(); ++pr) {
if(lIndex == l.size()) {
//头一个和最后一个
int i1 = l.get(lIndex - 1);
int i2 = l.get(0);
dp[pk][pr] = Math.min(pr - i1 + dp[pk + 1][i1], rl - (pr - i2) + dp[pk + 1][i2]);
} else if(pr == l.get(lIndex)) {
//不用转了
dp[pk][pr] = dp[pk + 1][pr];
++lIndex;
} else {
int i1 = l.get(lIndex);//一定大于pr
int i2 = l.get(lIndex == 0 ? l.size() - 1 : lIndex - 1);
dp[pk][pr] = Math.min(i1 - pr + dp[pk + 1][i1], minRotate(i2, pr, rl) + dp[pk + 1][i2]);
}
}
这样之许哟啊遍历一次pr就好 事件复杂度从O(n3)变成O(n2)。
还是慢,试试自顶向下的方法。因为就k == 0的哪一行来说,其实我只需要一个数就行,其他的算了也白算,所以采用记忆化搜索的方式。更新策略还是贪心的策略:
class Solution {
int minRotate(int from, int to) {
//字符串长度为l 从from到to最短需要转几下
int diff = Math.abs(from - to);
return Math.min(diff, ring.length() - diff);
}
String ring, key;
Integer[][] dp;
List<Integer>[] ringLetter;
//试试自顶向下
int rec(int pr, int pk) {
if(pk == key.length()) {
return 0;
}
if(dp[pk][pr] != null) {
return dp[pk][pr];
}
//初始位置在pr 匹配key的第pk个字符和之后的旋转数
//恰好相等则直接搜索下一步
if(key.charAt(pk) == ring.charAt(pr)) {
dp[pk][pr] = rec(pr, pk + 1);
return dp[pk][pr];
}
List<Integer> l = ringLetter[key.charAt(pk) - 'a'];
if(l.size() == 1) {
//只能转到那个位置了
int t = l.get(0);
dp[pk][pr] = rec(t, pk + 1) + minRotate(t, pr);
} else if(pr < l.get(0) || pr > l.get(l.size() - 1)) {
//不扣细节了 总之是头一个和后一个
int i1 = l.get(0), i2 = l.get(l.size() - 1);
dp[pk][pr] = Math.min(rec(i1, pk + 1) + minRotate(i1, pr), rec(i2, pk + 1) + minRotate(i2, pr));
} else {
//二叉搜索出位置 然后前一个后一个
int ii = - Collections.binarySearch(l, pr) - 1;
int i1 = l.get(ii), i2 = l.get(ii - 1);
dp[pk][pr] = Math.min(rec(i1, pk + 1) + minRotate(i1, pr), rec(i2, pk + 1) + minRotate(i2, pr));
}
return dp[pk][pr];
}
public int findRotateSteps(String ring, String key) {
this.ring = ring;
this.key = key;
//范围都不大100 * 100也就10000 允许顺时针或逆时针就比较麻烦了 先试试迭代的dp
dp = new Integer[key.length() + 1][ring.length()];//dp[i][j] 初始ring指向j的情况下要匹配key中第i个字母所需的最短距离
//不考虑要按那几下最后再加
ringLetter = new List[26];
for(int i = 0; i < 26; ++i) {
ringLetter[i] = new ArrayList<>();
}
for(int i = 0; i < ring.length(); ++i) {
ringLetter[ring.charAt(i) - 'a'].add(i);
}
rec(0, 0);
return dp[0][0] + key.length();
}
}
快了好多好多 时间复杂度不是很好分析…(中间还有个二分查找尤其讨厌)
其实把dp矩阵打印一下就知道问什么会快这么多
输入为
ring : “godding”
key : “gdong”
搜索完毕后的dp矩阵为
[
[7, null, null, null, null, null, null],
[7, null, null, null, null, null, null],
[null, null, 5, 6, null, null, null],
[null, 4, null, null, null, null, null],
[null, null, null, null, null, 1, null],
[null, null, null, null, null, null, null]
]
其实只需要算少的可怜的几个 一开始把这个矩阵全算出来确实有些缺心眼…