博弈树-极大极小搜索算法
跟博弈的必败必胜一样的分析,后手存在必败则先手必胜,先手全为必胜则先手必败。
DFS时对后手的返回值做上述两种判断就行。
LC 913. 猫和老鼠
方法一:必胜态分析+DFS
思路:每次走一步,鼠走的时候,如果之后存在鼠必胜(即猫必败),则当前鼠必胜(相当于沿着必胜的方式一直走);如果之后都是猫必胜,则当前鼠必败;如果不是这两种情况,说明是平局。
DFS递归的出口:(鼠,猫,步数),关键:走2n步没结果说明是平局
class Solution {
public:
int dp[55][55][105];
// (鼠,猫,步数)
int dfs(vector<vector<int>>& graph,int i, int j, int k) {
// cout << i << " " << j << " " << k << endl;
int& ret = dp[i][j][k];
if(ret != -1) return ret;
if(i == j) return ret = 2;
if(i == 0) return ret = 1;
if(k > 2*graph.size()) return ret = 0;
bool all_one = true, all_two = true;
if(k%2 == 0) { // 鼠走
for(int u : graph[i]) {
int tmp = dfs(graph, u, j, k+1);
if(tmp == 1) return ret=1;
if(tmp == 0) all_two = false;
}
if(all_two) return ret = 2;
return ret = 0;
} else { // 猫走
for(int v : graph[j]) {
if(v == 0) continue;
int tmp = dfs(graph, i, v, k+1);
if(tmp == 2) return ret=2;
if(tmp == 0) all_one = false;
}
if(all_one) return ret = 1;
return ret = 0;
}
}
int catMouseGame(vector<vector<int>>& graph) {
int n = graph.size();
memset(dp, -1, sizeof(dp));
return dfs(graph, 1, 2, 0);
}
};
方法二:极大极小博弈+DFS
最单纯的极大极小算法
局面估价函数:我们给每个局面(state)规定一个估价函数值 f,评价它对于己方的有利程度。胜利的局面的估价函数值为 +oo,而失败的局面的估价函数值为–oo。
- Max 局面:假设这个局面轮到己方走,有多种决策可以选择,其中每种决策都导致一种子局面(sub-state)。由于决策权在我们手中,当然是选择估价函数值 f 最大的子局面,因此该局面的估价函数值等于子局面 f 值的最大值,把这样的局面称为 max 局面。
- Min 局面:假设这个局面轮到对方走,它也有多种决策可以选择,其中每种决策都导致一种子局面(sub-state)。但由于决策权在对方手中,在最坏的情况下,对方当然是选择估价函数值 f 最小的子局面,因此该局面的估价函数值等于子局面 f 值的最小值,把这样的局面称为 max 局面。
- 终结局面:胜负已分(假设没有和局)
大概是这样的博弈树,一层取最小,一层取最大。
我们假设老鼠赢为 11 分,猫赢为 33 分,和局为 22 分,题目就可以转化为最大最小博弈。对于老鼠来说,得分要尽可能小,而对于猫来说,得分要尽可能高。【参考题解喜帖-极大极小搜索】
class Solution {
public:
int dp[55][55][105];
// (鼠,猫,步数)
// 鼠1 猫3 平局2
int dfs(vector<vector<int>>& graph,int i, int j, int k) {
// cout << i << " " << j << " " << k << endl;
int& ret = dp[i][j][k];
if(ret != -1) return ret;
if(i == j) return ret = 3;
if(i == 0) return ret = 1;
if(k > 2*graph.size()) return ret = 2;
if(k%2 == 0) { // 鼠走
ret = INT_MAX;
for(int u : graph[i]) {
ret = min(ret, dfs(graph, u, j, k+1));
if(ret == 1) return ret; // 必胜,可以不管后面的了,优化
}
return ret;
} else { // 猫走
ret = -INT_MAX;
for(int v : graph[j]) {
if(v == 0) continue;
ret = max(ret, dfs(graph, i, v, k+1));
if(ret == 3) return 3; // 优化
}
return ret;
}
}
int catMouseGame(vector<vector<int>>& graph) {
int n = graph.size();
memset(dp, -1, sizeof(dp));
int res = dfs(graph, 1, 2, 0);
if(res == 1) return 1;
else if(res == 2) return 0;
return 2;
}
};
1406. 石子游戏 III
方法一:极大极小+DFS
方法:同样也可以看成一个极大极小博弈问题,将后手的转成负数,先手越大越好,后手越小越好。【参考 题解Netcan-极小化极大算法】
dfs的含义是对turn=0返回最大值,对turn=1返回最小值
class Solution {
public:
int dp[50005][2];
const int INF = 0x3f3f3f3f;
// 拿到了第i堆,轮到了turn
int dfs(vector<int>& stoneValue, int i, int turn) {
int n = stoneValue.size();
if(i >= n) return 0;
int& ret = dp[i][turn];
if(ret != INF) return ret;
int cur = 0;
if(turn == 0) { // turn=0, Alice拿
ret = -INF;
for(int k = 0;k < 3 && i+k< n;k++) {
cur += stoneValue[i+k];
ret = max(ret, cur + dfs(stoneValue, i+k+1, !turn));
}
} else {
ret = INF;
for(int k = 0;k < 3 && i+k< n;k++) {
cur -= stoneValue[i+k];
ret = min(ret, cur + dfs(stoneValue, i+k+1, !turn));
}
}
return ret;
}
string stoneGameIII(vector<int>& stoneValue) {
memset(dp, INF, sizeof(dp)); // -0x3f3f3f3f不能这样填充
int res = dfs(stoneValue, 0, 0);
cout << "res: " << res << endl;
if(res > 0) return "Alice";
if(res < 0) return "Bob";
return "Tie";
}
};
方法二:极大极小简化版
由于turn每次都是反转过来,上面的代码进一步简化(记忆化不能丢,不让会超时)
当前值减去后手的-每种情况最大值,这里DFS的含义就是(i, turn)的最大值
class Solution {
public:
int dp[50005][2];
const int INF = 0x3f3f3f3f;
// 拿到了第i堆,轮到了turn
int dfs(vector<int>& stoneValue, int i, int turn) {
int n = stoneValue.size();
if(i >= n) return 0;
int& ret = dp[i][turn];
if(ret != INF) return ret;
int cur = 0;
ret = -INF;
for(int k = 0;k < 3 && i+k< n;k++) {
cur += stoneValue[i+k];
ret = max(ret, cur - dfs(stoneValue, i+k+1, !turn));
}
return ret;
}
string stoneGameIII(vector<int>& stoneValue) {
memset(dp, INF, sizeof(dp)); // -0x3f3f3f3f不能这样填充
int res = dfs(stoneValue, 0, 0);
cout << "res: " << res << endl;
if(res > 0) return "Alice";
if(res < 0) return "Bob";
return "Tie";
}
};
方法三:线性递推,改成DP
很容易发现,上面的状态可以倒推得到,而且是线性递推(不像猫捉老鼠是图结构),所以可以改成dp。
class Solution {
public:
string stoneGameIII(vector<int>& stoneValue) {
int n = stoneValue.size();
vector<vector<int>>dp(n+1, vector<int>(2, INT_MIN));
dp[n][0] = dp[n][1] = 0;
for(int i = n-1;i>=0;i--) {
for(int turn = 0;turn < 2;turn++) {
int sum = 0;
for(int k = 0;k < 3 && i+k < n;k++) {
sum += stoneValue[i+k];
dp[i][turn] = max(dp[i][turn], sum - dp[i+k+1][!turn]);
}
}
}
int res = dp[0][0];
// cout << "res: " << res << endl;
if(res > 0) return "Alice";
if(res < 0) return "Bob";
return "Tie";
}
};
LC 1510. 石子游戏 IV
方法:博弈最简单的例题了,线性的,没有平局。这里采用必胜必败分析,也可以用极大极小(必胜必败算是其特例),也可以递推。
class Solution {
public:
int dp[100005];
int dfs(int n) {
// cout << "n: " << n << endl;
if(n == 0) return 0;
int& ret = dp[n];
if(ret != -1) return ret;
int gen = sqrt(n);
for(int i = 1;i <= gen;i++) {
if(dfs(n-i*i) == 0) return ret=1; // 后手存在必败,则先手必胜
}
return ret=0; // 由于没有平局,一定为必败
}
bool winnerSquareGame(int n) {
memset(dp, -1, sizeof(dp));
return dfs(n);
}
};
个性签名:时间会解决一切