LeetCode 514. Freedom Trail && 动态规划与贪心算法的对比分析

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]:
  1. 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].
  2. 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:


Input: ring = "godding", key = "gd"
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:
  1. Length of both ring and key will be in range 1 to 100.
  2. There are only lowercase letters in both strings and might be some duplcate characters in both strings.
  3. 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+子问题的形式。有点类似数学上的归纳法。但是这两个算法有很大的不同,
  1. 动态规划相比贪心算法更繁琐,能用贪心算法来解的问题,一定可以有一个繁琐的动态规划的解法。
  2. 动态规划是自底向上的思路,而贪心算法是自顶向下的。具体来说,在应用动态规划时,总是先解决所有的子问题,然后考虑当前问题,逐步向上推;而贪心算法则是基于一定的规则,把当前问题拆成子问题,然后去解决子问题。
  3. 动态规划有很大的冗余,这也是它“繁琐”的原因;而贪心算法没有,可以用递归或循环的方式解决。
    在实际应用中,判断一个问题是否具有“最优子结构”特性并不难,关键是判断什么时候可以用贪心算法,什么时候只能用动态规划。这个判断并没有一定之法,需要针对不同的情形分析当做出一个“贪心选择”,把问题拆解成子问题后,是否当前问题的最优解等价于子问题的最优解。比如这个题目中,不能简单的选取离当前字母最近的下一个字母,反例可以参考前面的例子。这时,动态规划的冗余量就显得非常有必要了。
    我们用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;
    }
};

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值