1872-石子游戲Ⅷ-優(yōu)化DP

寫(xiě)在前面

這周周賽的最后一題,經(jīng)典遞推博弈論,但是沒(méi)想出來(lái),通過(guò)學(xué)習(xí)看懂了推理過(guò)程,還順便學(xué)會(huì)了這種通過(guò)前綴的方式優(yōu)化DP,收獲良多。

題目

核心思路

通過(guò)理解題意,不難發(fā)現(xiàn),當(dāng)取走左邊若干個(gè)石子后,對(duì)右邊石子原來(lái)的分?jǐn)?shù)是沒(méi)有影響的,仍是前綴和,所以預(yù)處理一個(gè)前綴和是很顯然的。

int[] sum = new int[n + 1];
for(int i = 0; i < n; i++) { 
  sum[i + 1] = sum[i] + stones[i];
}

游戲過(guò)程我們不妨先不考慮時(shí)間的要求,直接通過(guò)暴力模擬來(lái)解決。

暴力法

暴力法直接模擬游戲過(guò)程,需要注意每一輪得到的結(jié)果都是這一輪的玩家期望得分差值的最大值。如果當(dāng)前已經(jīng)取到第i (1 <= i <= n)塊石子,那么這一輪可以取到的結(jié)果solve(i)就是從in中選擇一個(gè)位置j,使得sum[j] - (下一輪對(duì)手的得分)最大,這里的sum[j]就是這一輪的得分,由于要保證雙方均采用最優(yōu)策略,下一輪對(duì)手也會(huì)選擇最大的得分差值,所以相當(dāng)于求解sum[j] - solve(j + 1)的最大值。

暴力法代碼
class Solution {

    int n;
    int[] stones;
    int[] sum;

    public int stoneGameVIII(int[] stones) {
        n = stones.length;
        this.stones = stones;
        sum = new int[n + 1];
        
        for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];

        return solve(2);
    }

    public int solve(int idx){
        if(idx == n) return sum[idx];

        int res = sum[n];
        for(int i = idx; i < n; i++){
            res = Math.max(res, sum[i] - solve(i + 1));
        }
        return res;
    }
}

記憶化遞歸O(N ^ 2)

完全模擬達(dá)到指數(shù)級(jí)別的時(shí)間復(fù)雜度,肯定需要進(jìn)行優(yōu)化,遞歸加優(yōu)化最常見(jiàn)的就是加一個(gè)備忘錄,寫(xiě)成記憶化遞歸。

O(N ^ 2)遞歸代碼
class Solution {

    int n;
    int[] stones;
    int[] sum;
    Integer[] memo;

    public int stoneGameVIII(int[] stones) {
        n = stones.length;
        this.stones = stones;
        
        memo = new Integer[n + 1];
        sum = new int[n + 1];

        for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];
        memo[n] = sum[n];
        return solve(2);
    }

    public int solve(int idx){
        if(memo[idx] != null) return memo[idx];

        int res = sum[n];
        for(int i = idx; i < n; i++){
            res = Math.max(res, sum[i] - solve(i + 1));
        }
        return memo[idx] = res;
    }
}

記憶化過(guò)程還是很簡(jiǎn)單的,直接加個(gè)備忘錄就可以了,不過(guò)這樣還是O(N ^ 2)的時(shí)間復(fù)雜度,還是會(huì)超時(shí)的。

前綴優(yōu)化DP

在記憶化中,每次遞歸都要從當(dāng)前位置向后遍歷找到最大的滿足條件的值,時(shí)間消耗較大,而每個(gè)位置都只與他后邊的值有關(guān),我們不妨來(lái)看一下solve(x)的值到底等于什么。

solve(x) = max(sum[x] - solve(x + 1), sum[x + 1] - solve(x + 2), ... , sum[n - 1] - solve(n), sum[n] - solve(n + 1))

而后邊這一段sum[x + 1] - solve(x + 2), ... , sum[n - 1] - solve(n), sum[n] - solve(n + 1),恰好是solve(x + 1)的值,帶入也就得到

solve(x) = Math.max(solve(x + 1), sum[x] - solve(x + 1))
這樣我們就可以得到優(yōu)化到O(N)時(shí)間復(fù)雜度的代碼了

O(N)遞歸代碼
class Solution {

    int n;
    int[] stones;
    int[] sum;
    Integer[] memo;

    public int stoneGameVIII(int[] stones) {
        n = stones.length;
        this.stones = stones;
        
        memo = new Integer[n + 1];
        sum = new int[n + 1];

        for(int i = 0; i < n; i++) sum[i + 1] = sum[i] + stones[i];
        memo[n] = sum[n];
        return solve(2);
    }

    public int solve(int idx){
        if(memo[idx] != null) return memo[idx];

        int res = Math.max(solve(idx + 1), sum[idx] - solve(idx + 1));
        return memo[idx] = res;
    }
}

當(dāng)然遞歸可以完成,迭代也同樣可以,不過(guò)迭代DP是自底向上求解,在這道題里也就是從dp[n]開(kāi)始一直求到dp[2],逆序遞推即可

O(N)動(dòng)態(tài)規(guī)劃代碼
class Solution {
    public int stoneGameVIII(int[] stones) {
        int n = stones.length;
        int[] sum = new int[n + 1];
        for(int i = 0; i < n; i++){
            sum[i + 1] = sum[i] + stones[i];
        }

        int[] dp = new int[n + 1];
        dp[n] = sum[n];

        for(int i = n - 1; i >= 2; i--){
            dp[i] = Math.max(dp[i + 1], sum[i] - dp[i + 1]);
        }
        return dp[2];
    }
}

可以發(fā)現(xiàn)dp[i]只與dp[i + 1]有關(guān),經(jīng)典的空間優(yōu)化,用一個(gè)變量代替dp數(shù)組即可

O(N)動(dòng)態(tài)規(guī)劃優(yōu)化空間代碼
class Solution {
    public int stoneGameVIII(int[] stones) {
        int n = stones.length;
        int[] sum = new int[n + 1];
        for(int i = 0; i < n; i++){
            sum[i + 1] = sum[i] + stones[i];
        }

        int res = sum[n];

        for(int i = n - 1; i >= 2; i--){
            res = Math.max(res, sum[i] - res);
        }
        return res;
    }
}

總結(jié)

博弈論的問(wèn)題也做過(guò)幾道了,還是不太能抓得住要領(lǐng),不過(guò)這種優(yōu)化DP的方法還是很值得學(xué)習(xí)的,希望可以越來(lái)越強(qiáng)。
如果文章有寫(xiě)的不對(duì)的地方,還請(qǐng)指出,感謝相遇~~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容