464-我能贏嗎-又是一道愁人的狀壓DP問題

題目

核心思路

這道題思考了挺長(zhǎng)的時(shí)間,卻始終沒有想到解決方案,主要是題目中兩位玩家游戲時(shí)都表現(xiàn)最佳感覺表示不出來,看了這位大佬的深搜之后豁然開朗。
我之前主要的疑惑是怎么只用一個(gè)搜索函數(shù)就同時(shí)表示出兩個(gè)玩家的游戲勝負(fù),因?yàn)檫@一輪是我在選數(shù),返回的結(jié)果也是我是否能取勝;而下一輪就是對(duì)手選數(shù)及能否取勝了(其實(shí)現(xiàn)在想來,傳參是傳入一個(gè)標(biāo)識(shí)位也是可行的)。在深搜代碼中,還是有部分挺值得思考的。

  public boolean dfs(int desiredTotal, boolean[] visited){
        if(desiredTotal <= 0) return false;
        for(int i = 1; i < visited.length; i++){
            if(!visited[i]){
                visited[i] = true;
                boolean tmp = dfs(desiredTotal - i, visited);
                visited[i] = false;
                if(!tmp) return true;
            }
        }
        return false;
    }

代碼中的 visited數(shù)組 表示的是1-max是否被取過了。而代碼的主要部分

        if(!visited[i]){
            visited[i] = true;
            boolean tmp = dfs(desiredTotal - i, visited);
            visited[i] = false;
            if(!tmp) return true;
        }

并不是記錄先手的人贏還是后手的人贏,而是返回當(dāng)前輪游戲中,游戲人(先手的或者后手的)是否能穩(wěn)贏,這樣本輪游戲人(以下以 代稱)又可以得到下一輪游戲人(以下以 對(duì)手 代稱)的游戲結(jié)果dfs(desiredTotal - i, visited)。這里有兩種情況, 取了 i 這個(gè)整數(shù)后,要么 對(duì)手 可以穩(wěn)贏,要么 對(duì)手 不能穩(wěn)贏。而其中 對(duì)手 不能穩(wěn)贏也就意味著在他那一輪游戲中出現(xiàn)了 可以穩(wěn)贏的情況。
這里要先回溯在返回,因?yàn)? 并不是特指先手玩家或者后手玩家, 只是本輪的游戲玩家,而且 要做出最優(yōu)的游戲選擇,所以 就要在所有剩余的取數(shù)情況中找到 對(duì)手 可以穩(wěn)輸?shù)那闆r,這樣 就穩(wěn)贏了,所以有 if(!tmp) return true; 這樣一行代碼,對(duì)手穩(wěn)輸情況返回true,而 的返回值又會(huì)交給上一輪的 對(duì)手 來考慮策略,這樣就可以保證雙方都能做出最優(yōu)的選擇,而不會(huì)某一輪游戲中一名玩家找到最優(yōu)解就直接一路return到第一輪游戲。

狀壓DP

通過上邊的深搜,其實(shí)不難發(fā)現(xiàn):如果在某次游戲中的某輪游戲中,前面取了[1,2,3]和[2,1,3]對(duì)本輪游戲是否穩(wěn)贏沒有影響,因?yàn)樗麄兊暮投紴?,也就是下一輪都要執(zhí)行 dfs(desiredTotal - 6 - i, visited),這里的 i 和上邊一樣用來遍歷沒取過的數(shù) i。也就是說對(duì)于同一個(gè)已經(jīng)取走數(shù)的集合([1,2,3]與[2,1,3]),由于剩下的desiredTotal也是相同的,故dfs方法給出的結(jié)果也是相同。這樣就有了很多重復(fù)計(jì)算的過程,考慮使用一個(gè)memo數(shù)組記錄也就理所應(yīng)當(dāng)了。
因?yàn)閷?duì)于每一個(gè)數(shù)只有取了或者沒取兩種狀態(tài),而且對(duì)于同一個(gè)狀態(tài),取的順序并不影響結(jié)果。所以可以想到使用狀態(tài)壓縮DP來記錄每一種狀態(tài)。

完整代碼

剩下的就是很典型的狀壓DP計(jì)算方法了。

class Solution {

    Boolean[] memo = new Boolean[1 << 20];

    public boolean canIWin(int maxChoosableInteger, int desiredTotal) {
        //先取穩(wěn)贏
        if(maxChoosableInteger >= desiredTotal) return true;
        //兩人都不能贏
        if(((1 + maxChoosableInteger) * maxChoosableInteger / 2) < desiredTotal) return false;
        
        return dfs(maxChoosableInteger, desiredTotal, 0);
    }

    public boolean dfs(int max, int desiredTotal, int state){
        if(memo[state] != null) return memo[state];
        
        for(int i = 0; i < max; i++){
            int tmp = 1 << i;
            if((tmp & state) == 0){
                //我這輪穩(wěn)贏或者對(duì)面下一輪穩(wěn)輸
                if(desiredTotal <= i + 1 || !dfs(max, desiredTotal - i - 1, state | tmp)){
                    memo[state] = true;
                    return true;
                }
            }
        }
        memo[state] = false;
        return false;
    }
}

對(duì)于位運(yùn)算部分:int tmp = 1 << i; temp & state,通過tmp取到當(dāng)前狀態(tài)state是否取到了i + 1這個(gè)數(shù),我遍歷從零開始主要是移位的時(shí)候舒服一點(diǎn)。state | tmp 就是在 state 的基礎(chǔ)上取 i + 1 這個(gè)數(shù)。

總結(jié)

雖然方法叫狀態(tài)壓縮DP,其實(shí)只是一種枚舉的方式,所以主要還是要看數(shù)據(jù)范圍是否能承受O(2^N)的時(shí)間復(fù)雜度,對(duì)于數(shù)據(jù)范圍小于等于30 的問題還是比較好用的,剩下最重要的就是要理解題目的意思了。
如果有寫的不正確的地方還請(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)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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