Question
In the video game Fallout 4, the quest "Road to Freedom" requires players to reach a metal dial called the "Freedom Trail Ring", and use the dial to spell a specific keyword in order to open the door.Given a string ring, which represents the code engraved on the outer ring and another string key, which represents the keyword needs to be spelled. You need to find the minimum number of steps in order to spell all the characters in the keyword.
Initially, the first character of the ring is aligned at 12:00 direction. You need to spell all the characters in the string key one by one by rotating the ring clockwise or anticlockwise to make each character of the string key aligned at 12:00 direction and then by pressing the center button.
At the stage of rotating the ring to spell the key character key[i]:
- You can rotate the ring clockwise or anticlockwise one place, which counts as 1 step. The final purpose of the rotation is to align one of the string ring's characters at the 12:00 direction, where this character must equal to the character key[i].
- If the character key[i] has been aligned at the 12:00 direction, you need to press the center button to spell, which also counts as 1 step. After the pressing, you could begin to spell the next character in the key (next stage), otherwise, you've finished all the spelling.
Example:
Output: 4
Explanation:
For the first key character 'g', since it is already in place, we just need 1 step to spell this character.
For the second key character 'd', we need to rotate the ring "godding" anticlockwise by two steps to make it become "ddinggo".
Also, we need 1 more step for spelling.
So the final output is 4.
Note:
- Length of both ring and key will be in range 1 to 100.
- There are only lowercase letters in both strings and might be some duplcate characters in both strings.
- It's guaranteed that string key could always be spelled by rotating the string ring.
Analysis
这个题目很容易想到使用贪心算法求解,但是这个题其实是典型的容易陷入局部最优的题型,leetcode的测试集也很应景的设置了很多贪心算法无法正确求解的测试用例。贪心算法的求解方法如下:Solution 贪心算法(fail)
class Solution {
public:
int findRotateSteps(string ring, string key) {
int len = ring.length();
int ringid = 0;
// int keyid = 0;
int count = 0;
vector<vector<int>>pool(26, vector<int>{});
for(int i = 0; i < len; ++i){
pool[ring[i] - 'a'].push_back(i);
}
for(int i = 0; i < key.length(); ++i){
int minstep = INT_MAX;
int newid = ringid;
for(int j = 0; j < pool[key[i] - 'a'].size(); ++j){
int step = (ringid > pool[key[i] - 'a'][j] ?
min(ringid - pool[key[i] - 'a'][j], len - ringid + pool[key[i] - 'a'][j]) :
min(pool[key[i] - 'a'][j] - ringid, len + ringid - pool[key[i] - 'a'][j]) );
if(step < minstep){
minstep = step;
newid = pool[key[i] - 'a'][j];
}
}
ringid = newid;
count += minstep + 1;
}
return count;
}
};
Analysis
先分析一下为什么贪心算法不可行。如果ring中有若干个相同字母,不妨设为key[i]=x,他们的位置分别在j0和j1,而碰巧key[i-1]处在(j0 + j1)/2的位置,那么对贪心算法来说,下一步选择j0还是j1都是等价的,但是考虑到后续的字母key[i + 1]可能距离j0近也可能距离j1近,所以在key[i]这一步算法有必要同时检测所有的可能性,结合后面的字母综合考虑。所以上面的算法会提交失败。举个例子,假设ring=abxxxxcbxx,key=abc,那么如果用贪心算法(如上面的算法所示),会选择第二个字母b而不是倒数第三个字母b,此时的总操作数为9;但是如果选择的倒数第三个字母b,总步长为7。用一句古话来说,人无远虑,必有近忧。
说到这里我想停下来复习一下 动态规划和 贪心算法。这两者都是适用于具有“最优子结构”特性的问题,具体来说,就是可以把原问题拆解为1+子问题的形式。有点类似数学上的归纳法。但是这两个算法有很大的不同,
- 动态规划相比贪心算法更繁琐,能用贪心算法来解的问题,一定可以有一个繁琐的动态规划的解法。
- 动态规划是自底向上的思路,而贪心算法是自顶向下的。具体来说,在应用动态规划时,总是先解决所有的子问题,然后考虑当前问题,逐步向上推;而贪心算法则是基于一定的规则,把当前问题拆成子问题,然后去解决子问题。
- 动态规划有很大的冗余,这也是它“繁琐”的原因;而贪心算法没有,可以用递归或循环的方式解决。
我们用dp[i,j]表示起始位置在ring[i],输出字符串key[j:]时的最小操作数。可以写出如下的递推关系:
dp[i,j] = min(len(ring[i], ring[k]) + dp[k,j+1]), 满足ring[k] == key[j]
注意到相比贪心算法,动态规划用了三层循环,i,j,k分别需要循环一次,以求得全局最优解。
Solution 动态规划
class Solution {
public:
int findRotateSteps(string ring, string key) {
int rlen = ring.length();
int klen = key.length();
vector<vector<int>>pool(26, vector<int>{});
for(int i = 0; i < rlen; ++i){
pool[ring[i] - 'a'].push_back(i);
}
vector<vector<int>>dp(rlen, vector<int>(klen + 1, 0));
for(int i = klen - 1; i >= 0; --i){
for(int j = rlen - 1; j >= 0; --j){
dp[j][i] = INT_MAX;
for(int k = 0; k < pool[key[i] - 'a'].size(); ++k){
int pos = pool[key[i] - 'a'][k];
int step = (j > pos ? min(j - pos, pos - j + rlen) : min(pos - j, j - pos + rlen));
int extra = dp[pos][i + 1];
dp[j][i] = min(dp[j][i], step + extra);
}
}
}
return dp[0][0] + klen;
}
};