375:Guess Number Higher or Lower II
首先找规律,以为是使左右数字和的差值最小。结果发现不是,例如7的情况,首先选择的数是4而不是5,最终代价是10。
然后回溯+剪枝暴力找每一段区间需要的最小代价,结果超时。
再一想发现回溯的过程中有重复子问题,所以加一个备忘录。Runtime 6ms,比dp要快,原因是剪枝较多。
int getMoneyAmount(int begin, int end, int** amount) {
if (begin >= end)
return 0;
else if (amount[begin][end] > 0) {
return amount[begin][end];
}
int minMoney = INT_MAX, lastMoney = INT_MAX;
for (int index = begin + (end - begin) / 2; index < end; index++) {
int m_money = index + max(getMoneyAmount(begin, index - 1, amount), getMoneyAmount(index + 1, end, amount));
if (m_money > lastMoney)
break;
if (m_money < minMoney)
minMoney = m_money;
}
amount[begin][end] = minMoney;
return minMoney;
}
int getMoneyAmount(int n) {
int** amount = new int*[n + 1];
for (int i = 0; i <= n; i++) {
amount[i] = new int[n + 1]();
}
return getMoneyAmount(1, n, amount);
}
当然dp也可以做的,递推公式如下:
其中,M[i][j]表示[i, j]区间内完成猜数字所需最小费用。
首先回溯,结果超时。考虑到里面又大量重复自问题,例如甲选1,乙选2,剩下[3, n]个数 和 甲选2,乙选1,剩下[3, n]个数 这两种情况是一样的,因为只考虑两人选择的数的综合超过target。但是怎么去记录子问题是个难点,因为有的数选了,有的数没选,还有剩下的数要完成的target是多少(其实这个不是问题,因为确定了选择的数后,剩下的target唯一确定了)。
看了discussion之后,找到了方案,由于最多只有20个数可选,倒数第i个数用二进制的倒数第i位表示,可选为1,已选为0,这样总共有2^20次方种可能情况,用数字[0, 2^20)来表示状态即可。在存储时使用int数组,每个元素0代表未知,1代表该状态下先手能赢,0代表该状态下先手必输。
2^20等于1024*1024,int大小为4Byte,所以空间需求为4MB。
状态递推,遍历每个可能选择的数,如果超过target或者选后对方必输则获胜。每个数都不能赢,则必输。
class Solution {
public:
bool canIWin(int maxChoosableInteger, int desiredTotal, int* win, int state) {
if (win[state]) {
return win[state] > 0;
}
for (int i = 0; i < maxChoosableInteger; i++) {
if (state & 1 << i) {
if (i + 1 >= desiredTotal || !canIWin(maxChoosableInteger, desiredTotal - i - 1, win, state ^ 1 << i)) {
win[state] = 1;
return true;
}
}
}
win[state] = -1;
return false;
}
bool canIWin(int maxChoosableInteger, int desiredTotal) {
if (maxChoosableInteger * (maxChoosableInteger + 1) / 2 < desiredTotal)
return false;
int* win = new int[1 << maxChoosableInteger]();
return canIWin(maxChoosableInteger, desiredTotal, win, (1 << maxChoosableInteger) - 1);
}
};
用动态规划的话比较麻烦,例如用win[i]表示状态i时能不能赢,还是用上面的递推公式,从win[0]开始推(所有数字都选完)。但是初始条件处理比较麻烦,因为随着可选数字增多,首先回到target之内的状态是赢。
这道题和Nim Game的区别在于,Nim先拿完就赢,而这道题胜利要求不一样,胜利条件是所得的数比对方大。
因此记录每个状态的胜利与否是不够的,要记录所得数的和能胜出多少。
由于每次玩家都只能从最左或最右,可得递推式:
win[i][j] = max(nums[i] - win[i + 1][j], nums[j] - win[i][j - 1])
其中win[i][j]表示在数组下标位于[i, j]区间时,先手能胜出多少。选定一个值后,减去对方胜出的数即可,首尾两种选法取最大值。
bool PredictTheWinner(vector<int>& nums) {
int len = nums.size();
int** win = new int*[len];
for (int i = 0; i < len; i++) {
win[i] = new int[len];
win[i][i] = nums[i];
}
for (int cnt = 1; cnt < len; cnt++) {
for (int i = 0; i < len - cnt; i++) {
int j = i + cnt;
win[i][j] = max(nums[i] - win[i + 1][j], nums[j] - win[i][j - 1]);
}
}
return win[0][len - 1] >= 0;
}
总结:Minimax这类题就是可能用到了min或max函数的动态规划/记忆化搜索?