[LeetCode] 818. Race Car 賽車

 

Your car starts at position 0 and speed +1 on an infinite number line.  (Your car can go into negative positions.)html

Your car drives automatically according to a sequence of instructions A (accelerate) and R (reverse).git

When you get an instruction "A", your car does the following: position += speed, speed *= 2.github

When you get an instruction "R", your car does the following: if your speed is positive then speed = -1 , otherwise speed = 1.  (Your position stays the same.)算法

For example, after commands "AAR", your car goes to positions 0->1->3->3, and your speed goes to 1->2->4->-1.數組

Now for some target position, say the length of the shortest sequence of instructions to get there.函數

Example 1:
Input: 
target = 3
Output: 2
Explanation: 
The shortest instruction sequence is "AA".
Your position goes from 0->1->3.
Example 2:
Input: 
target = 6
Output: 5
Explanation: 
The shortest instruction sequence is "AAARA".
Your position goes from 0->1->3->7->7->6.

 

Note:post

  • 1 <= target <= 10000.

 

這道題是關於賽車的題(估計韓寒會比較感興趣吧,從《後會無期》,到《乘風破浪》,再到《飛馳人生》,貌似每一部都跟車有關,正所謂不會拍電影的賽車手不是好做家,哈哈~)。好,很少扯了,來作題吧,如下講解主要參考了 fun4LeetCode 大神的帖子。說是起始時有個小車在位置0,速度爲1,有個目標位置 target,是小車要到達的地方。而小車只有兩種操做,第一種是加速操做,首先當前位置加上小車速度,而後小車速度乘以2。第二種是反向操做,小車位置不變,小車速度重置爲單位長度,而且反向。問咱們最少須要多少個操做才能到達 target。咱們首先來看下若小車一直加速的話,都能通過哪些位置,從起點開始,若小車連加五次速,位置的變化爲:ui

0 -> 1 -> 3 -> 7 -> 15 -> 31url

有沒有發現這些數字很眼熟,沒有的話,就每一個數字都加上個1,那麼就應該眼熟了吧,對於信仰 1024 的程序猿來講,不眼熟不行啊,這就變成了2的指數數列啊,那麼咱們得出告終論,當小車從0開始連加n個速的話,其將會到達位置 2^n - 1。咱們能夠看出,小車越日後,位置跨度越大,那麼當 target 不在這些位置上,頗有可能一腳油門就開過了,好比,target = 6 的話,小車在3的位置上,一腳油門,就到7了,這時候就要回頭,回頭後,速度變爲 -1,此時正好就到達6了,那麼小車的操做以下:spa

Initial:    pos -> 0,    speed -> 1

A:      pos -> 1,    speed -> 2

A:      pos -> 3,    speed -> 4

A:      pos -> 7,    speed -> 8

R:      pos -> 7,    speed -> -1

A:      pos -> 6,    speed -> -2

因此,咱們只須要5步就能夠了。可是還有個問題,假如回頭了之後,一腳油門以後,又過站了怎麼辦?好比 target = 5 的時候,以前小車回頭以後到達了6的位置,此時速度已是 -2了,再加個速,就直接幹到了位置4,就得再回頭,那麼這種方式小車的操做以下:

Initial:    pos -> 0,    speed -> 1

A:      pos -> 1,    speed -> 2

A:      pos -> 3,    speed -> 4

A:      pos -> 7,    speed -> 8

R:      pos -> 7,    speed -> -1

A:      pos -> 6,    speed -> -2

A:      pos -> 4,    speed -> -4

R:      pos -> 4,    speed -> 1

A:      pos -> 5,    speed -> 2

那麼此時咱們就用了8步,但這是最優的方法麼,咱們必定要在過了目標纔回頭麼,不撞南牆不回頭麼?其實沒必要,咱們能夠在到達 target 以前提早調頭,而後往回走走,再調回來,使得以後能剛好到達 target,好比下面這種走法:

Initial:    pos -> 0,    speed -> 1

A:      pos -> 1,    speed -> 2

A:      pos -> 3,    speed -> 4

R:      pos -> 3,    speed -> -1

A:      pos -> 2,    speed -> -2

R:      pos -> 2,    speed -> 1

A:      pos -> 3,    speed -> 2

A:      pos -> 5,    speed -> 4

咱們在未到達 target 的位置3時就直接掉頭了,日後退到2,再調回來,往前走,到達5,此時總共只用了7步,是最優解。那麼咱們怎麼知道啥時候要掉頭?問得好,答案是不知道,咱們得遍歷每種狀況。可是爲了不計算一些無用的狀況,好比小車反向過了起點,或者是超過 target 好遠都不回頭,咱們須要限定一些邊界,好比小車不能去小於0的位置,以及小車在超過了 target 時,就必須回頭了,不能繼續往前開了。還有就是小車當前的位置不能超過 target x 2,不過這個限制條件博主尚未想出合理的解釋,各位看官大神們知道的話能夠給博主講講~

對於求極值的題目,根據博主多年與 LeetCode 抗爭的經驗,就是 BFS,帶剪枝的 DFS 解法,貪婪算法,或者動態規劃 Dynamic Programming 這幾種解法(帶記憶數組的 DFS 解法也能夠歸到 DP 一類中去)。通常來講,貪婪算法比較 naive,大機率會跪。BFS 有時候能夠,帶剪枝的 DFS 解法中的剪枝比較難想,而 DP 絕對是神器,基本沒有解決不了的問題,可是代價就是得抓破頭皮想狀態轉移方程,而且通常 DP 只能用來求極值,而想求極值對應的具體狀況(好比這道題若是讓求最少個數的指令是什麼),有時候可能還得用帶剪枝的 DFS 解法。不過這道題 BFS 也能夠,那麼咱們就先用 BFS 來解吧。

這裏的 BFS 解法,跟迷宮遍歷中的找最短路徑很相似,能夠想像成水波,一圈一圈的往外擴散,當碰到 target 時候,當前的半徑就是最短距離。用隊列 queue 來輔助遍歷,裏面放的是位置和速度的 pair 對兒,將初始狀態位置0速度1先放進 queue,而後須要一個 HashSet 來記錄處理過的狀態,爲了節省空間和加快速度,咱們將位置和速度變爲字符串,並在中間加逗號隔開,這樣 HashSet 中只要保存字符串便可。以後開始 while 循環,此時採用的是層序遍歷的寫法,當前 queue 中全部元素遍歷完了以後,結果 res 才自增1。在 for 循環中,首先取出隊首 pair 對兒的位置和速度,若是位置和 target 相等,直接返回結果 res。不然就要去新的地方了,首先嚐試的是加速操做,此時新的位置 newPos 爲以前的位置加速度,新的速度 newSpeed 爲以前速度的2倍,而後將 newPos 和 newSpeed 加碼成字符串,若新的狀態沒有處理過,且新位置大於0,小於 target x 2 的話,則將新狀態加入 visited,並排入隊列中。接下來就是轉向的狀況,newPos 和原位置保持不變,newSpeed 根據以前 speed 的正負變成 -1 或1,而後將 newPos 和 newSpeed 加碼成字符串,若新的狀態沒有處理過,且新位置大於0,小於 target x 2 的話,則將新狀態加入 visited,並排入隊列中。for循環結束後,結果 res 自增1便可,參見代碼以下:

 

解法一:

class Solution {
public:
    int racecar(int target) {
        int res = 0;
        queue<pair<int, int>> q{{{0, 1}}};
        unordered_set<string> visited{{"0,1"}};
        while (!q.empty()) {
            for (int i = q.size(); i > 0; --i) {
                int pos = q.front().first, speed = q.front().second; q.pop();
                if (pos == target) return res;
                int newPos = pos + speed, newSpeed = speed * 2;
                string key = to_string(newPos) + "," + to_string(newSpeed);
                if (!visited.count(key) && newPos > 0 && newPos < (target * 2)) {
                    visited.insert(key);
                    q.push({newPos, newSpeed});
                }
                newPos = pos; 
                newSpeed = (speed > 0) ? -1 : 1;
                key = to_string(newPos) + "," + to_string(newSpeed);
                if (!visited.count(key) && newPos > 0 && newPos < (target * 2)) {
                    visited.insert(key);
                    q.push({newPos, newSpeed});
                }
            }
            ++res;
        }
        return -1;
    }
};

 

好,既然說了 DP 是神器,那麼就來用用這傳說中的神器吧。首先來定義 dp 數組吧,就用一個一維的 dp 數組,長度爲 target+1,其中 dp[i] 表示到達位置i,所須要的最少的指令個數。接下來就是推導最難的狀態轉移方程了,這裏咱們不能像 BFS 解法同樣對每一個狀態都無腦嘗試加速和反向操做,由於狀態轉移方程是要跟以前的狀態創建聯繫的。根據以前的分析,對於某個位置i,咱們有兩種操做,一種是在到達該位置以前,回頭兩次,另外一種是超過該位置後再回頭,咱們就要模擬這兩種狀況。

首先來模擬位置i以前回頭兩次的狀況,那麼這裏咱們就有正向加速,和反向加速兩種可能。咱們假設正向加速能到達的位置爲j,正向加速次數爲 cnt1,反向加速能到達的位置爲k,反向加速的次數爲 cnt2。那麼正向加速位置j從1開始遍歷,不能超過i,且根據以前的規律,j每次的更新應該是 2^cnt1 - 1,而後對於每一個j位置,咱們都要反向跑一次,此時反向加速位置k從0開始遍歷,不能超過j,k每次更新應該是 2^cnt2 - 1,那麼到達此時的位置時,咱們正向走了j,反向走了k,便可表示爲正向走了 (j - k),此時的指令數爲 cnt1 + 1 + cnt2 + 1,加的2個 ‘1’ 分貝是反向操做的兩次計數,當咱們第二次反向後,此時的方向就是朝着i的方向了,此時跟i之間的距離能夠直接用差值在 dp 數組中取,爲 dp[i - (j - k)],以此來更新 dp[i]。

接下來模擬超過i位置後纔回頭的狀況,此時 cnt1 是恰好能超過或到達i位置的加速次數,咱們能夠直接使用,此時咱們比較i和j,若相等,則直接用 cnt1 來更新 dp[i],不然就反向操做一次,而後距離差爲 j-i,從 dp 數組中直接調用 dp[j-i],而後加上反向操做1次,用來更新 dp[i],最終返回 dp[target] 即爲所求,參見代碼以下:

 

解法二:

class Solution {
public:
    int racecar(int target) {
        vector<int> dp(target + 1);
        for (int i = 1; i <= target; ++i) {
            dp[i] = INT_MAX;
            int j = 1, cnt1 = 1;
            for (; j < i; j = (1 << ++cnt1) - 1) {
                for (int k = 0, cnt2 = 0; k < j; k = (1 << ++cnt2) - 1) {
                    dp[i] = min(dp[i], cnt1 + 1 + cnt2 + 1 + dp[i - (j - k)]);
                }
            }
            dp[i] = min(dp[i], cnt1 + (i == j ? 0 : 1 + dp[j - i]));
        }
        return dp[target];
    }
};

 

下面是 DP 的遞歸寫法,跟上面的迭代寫法並無太大的區別,只是將直接從 dp 數組中取值的過程變成了調用遞歸函數,參見代碼以下:

 

解法三:

class Solution {
public:
    int racecar(int target) {
        vector<int> dp(target + 1, -1);
        dp[0] = 0;
        return helper(target, dp);
    }
    int helper(int i, vector<int>& dp) {
        if (dp[i] >= 0) return dp[i];
        dp[i] = INT_MAX;
        int j = 1, cnt1 = 1;
        for (; j < i; j = (1 << ++cnt1) - 1) {
            for (int k = 0, cnt2 = 0; k < j; k = (1 << ++cnt2) - 1) {
                dp[i] = min(dp[i], cnt1 + 1 + cnt2 + 1 + helper(i - (j - k), dp));
            }
        }
        dp[i] = min(dp[i], cnt1 + (i == j ? 0 : 1 + helper(j - i, dp)));
        return dp[i];
    }
};

 

Github 同步地址:

https://github.com/grandyang/leetcode/issues/818

 

參考資料:

https://leetcode.com/problems/race-car/

https://leetcode.com/problems/race-car/discuss/123834/C%2B%2BJavaPython-DP-solution

https://leetcode.com/problems/race-car/discuss/124326/Summary-of-the-BFS-and-DP-solutions-with-intuitive-explanation

 

LeetCode All in One 題目講解彙總(持續更新中...)

相關文章
相關標籤/搜索