狀態壓縮 - LeetCode #464 Can I Win


動態規划是一種top-down求解模式,關鍵在於分解和求解子問題,然后根據子問題的解不斷向上遞推,得出最終解

因此dp涉及到保存每個計算過的子問題的解,這樣當遇到同樣的子問題時就不用繼續向下求解而直接可以得到結果。狀態壓縮就是用來保存子問題的解的,主要思想是把所有可能的狀態(子問題)用一個數據結構(通常是整數)統一表示,再用map把每個狀態和對應結果關聯起來,這樣每次求解子問題時先find一下,如果map里面已經有該狀態的解就不用再求了;同樣每次求解完一個狀態的解后也要將其放入map中保存

狀態壓縮適用於二元狀態,即每一列的取值只有0和1,且不適合求解規模很大的問題(否則狀態太多根本保存不了)

 

LeetCode #464  Can I Win

https://leetcode.com/problems/can-i-win/

In the "100 game," two players take turns adding, to a running total, any integer from 1..10. The player who first causes the running total to reach or exceed 100 wins.

What if we change the game so that players cannot re-use integers?

For example, two players might take turns drawing from a common pool of numbers of 1..15 without replacement until they reach a total >= 100.

Given an integer maxChoosableInteger and another integer desiredTotal, determine if the first player to move can force a win, assuming both players play optimally.

You can always assume that maxChoosableInteger will not be larger than 20 and desiredTotal will not be larger than 300.

 

一開始看到這題,我想這不就是博弈樹嘛!然后也沒多思考,就開始啪啦啪啦地敲miniMax,再搞個αβ剪枝感覺時間復雜度也差不多了。一提交結果TL,整個人都不好了,還以為自己剪枝沒剪好,后來實在慫了看了discuss才意識到要用深搜,像博弈樹那樣等暴力廣搜完早就超時了=  =、(附上我超時的博弈樹代碼)

int minMaxTree( bool turn, set<int> &cho, int goal, int now, int a, int b )
{
	if(now>=goal)
	{
		if(turn)
			return -1;
		else return 1;
	}
	set<int>::iterator itr;
	if(turn)//max方
	{
		for(itr=cho.begin();itr!=cho.end();itr++)
		{
			set<int> tmp = cho;
			tmp.erase(tmp.find(*itr));
			a = minMaxTree(0,tmp,goal,now+*itr,a,b);
			if(a>=b)
				break;
		}
	}
	else
	{
		for(itr=cho.begin();itr!=cho.end();itr++)
		{
			set<int> tmp = cho;
			tmp.erase(tmp.find(*itr));
			b = minMaxTree(1,tmp,goal,now+*itr,a,b);
			if(a>=b)
				break;
		}
	}
	if(turn) return a;
	else return b; 
}

 

之后我便參考discuss上的代碼實現了dp+狀態壓縮(基本上是照着打的...),思路是暴力深搜,用狀態壓縮記錄重復的狀態降低時間消耗。這里主要說一下狀態的表示方法,該問題每個狀態(子問題)之間的不同之處在於:

1. 可以選用的數

2. 當前目標數(當前離原始目標還差多少)

即當兩個狀態上述兩項都相同時,則視為同一個狀態。狀態區分不需要考慮當前是哪一方,因為雙方實質一樣即都想贏

現在考慮如何表示狀態,可選數上限為N,那么每個狀態就會有N個布爾值表示對應的數是否已被使用,比如N=3,[true false true]表示1、3可以用,2已被用。用01表示的話就是101,可以發現其能夠轉換成對應的2進制數,因此用N位的2進制整數就可以直接表示每次的可選數情況

解決了如何表示可選用數,還需要表示當前目標,方法是直接把map放到vector里,用vector下標表示目標數,比如vector[goal][nums] = true,表示當目標數為goal,當前狀態為nums時,玩家可以贏該游戲

bool miniMax( int status, vector<unordered_map<int,bool> > &dp, int goal, int maxn )
{
	if(dp[goal-1].find(status)!=dp[goal-1].end())//該狀態已經被搜索過
		return dp[goal-1][status]; 
	for( int i=maxn-1; i>=0; i-- )
	{
		if(status & (1<<i))//遍歷每個數字,如果該數字還沒被使用 
		{
			//亦或,把該位變0表示使用該數字
			if( i+1 >= goal || !miniMax(status^(1<<i),dp,goal-i-1,maxn) ) //如果當前已經能實現目標,或對方接下來不能贏
			{
				dp[goal-1][status] = true;
				return true;
			} 
		}
	}
	dp[goal-1][status] = false;
	return false;
}

bool canIWin(int maxChoosableInteger, int desiredTotal) 
{
    if(maxChoosableInteger>=desiredTotal)
		return true;
	if((maxChoosableInteger)*(maxChoosableInteger+1)/2<desiredTotal)//可選數之和小於目標則必定不可能成功 
		return false;
	int status = (1 << maxChoosableInteger) - 1;//初始狀態為全1即全部數字都可用 
	vector<unordered_map<int,bool> > dp(desiredTotal);//記錄狀態,dp[goal][sta]表示當前可用數為sta,目標為goal時能不能贏 
	return miniMax(status,dp,desiredTotal,maxChoosableInteger);
}

這里用unordered_map代替map,搜索時速度會更快(相應空間代價更高)。有個小插曲是在miniMax中傳dp時忘記傳引用,導致時傳參因直接copy而不停地超時,且一直找不到原因=  =、腦子不夠用了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM