状态压缩 - 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而不停地超时,且一直找不到原因= =、脑子不够用了