简单对抗搜索博弈

组合游戏概念

$1$.两个玩家
$2$.一个状态集合
$3$.游戏规则是指明玩家在一个状态下可以移动到哪些其他状态
$4$.玩家轮流进行移动
$5$.若当前处于某个状态,玩家根据规则无法移动,则游戏结束
$6$.游戏会在有限步以内结束(有向无环$DAG$)

$P$态和$N$态

$P$态:走到这个状态的玩家赢的状态
$N$态:从这个状态走的玩家赢的状态
至少能走到一个$P$态的状态是$N$态
只能走到$N$态的状态是$P$态
正常规则:终态是$P$态(无法走的人输)
反常规则:终态是$N$态(无法走的人赢)

$P$态和$N$态应用

由$P$态和$N$态的性质可以做出大部分简单的博弈题
对小数据题可以记忆化搜索所有状态
对大数据题可以先打表小数据,看看有无规律

例题:$Digital\ Deletions$

题目大意

有一个只含数字$0$到$9$的串$s$,两个人玩游戏,每次当前玩家可以把某一位改为比当前位更小的数字,或者删除一个$0$以及它的右边全部的数字。删除最后一个数字的人赢。求最优策略下,先手必胜还是后手必胜
串$s$的长度不超过$6$位

解题思路

当一个串的开头是$0$的时候,先手一步获胜,将串变成空串
已知游戏终止状态,即递归终点
那么根据至少能走到一个$P$态的状态是$N$态,只能走到$N$态的状态是$P$态
通过递归遍历所有决策去记忆化搜索即可

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <bits/stdc++.h>
const int N = 1e6 + 1;
int dp[N];
int real_num(std::string t)
{
int res = 0;
for (int i = 0; i < t.size(); ++i)
res = res * 10 + t[i] - '0';
return res;
}
int dfs(std::string u)
{
if (u[0] == '0')
return 1;
int num = real_num(u);
if (dp[num] != -1)
return dp[num];
for (int i = 0; i < u.size(); ++i)
{
char backup = u[i];
for (char j = backup - 1; j >= '0'; --j)
{
u[i] = j;
if (dfs(u) == 0)
return dp[num] = 1;
}
u[i] = backup;
}
for (int i = 0; i < u.size(); ++i)
{
if (u[i] == '0')
{
std::string t = std::string(u.begin(), u.begin() + i);
if(dfs(t) == 0)
return dp[num] = 1;
}
}
return dp[num] = 0;
}
void solve()
{
std::string str;
std::cin >> str;
if (dfs(str) == 1)
std::cout << "Yes\n";
else
std::cout << "No\n";
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
int T = 1;
std::cin >> T;
memset(dp, -1, sizeof dp);
while (T--)
solve();
return 0;
}

例题:$Bash$游戏

题目大意

一堆石子共$n$个,$A$和$B$两人轮流拿,每次最少拿一颗,最多拿$k$颗,最后无法行动的人输
$A$先手,问谁必胜

解题思路

游戏终止状态是$0$颗石子,此时先手必输,即$P$态
由此打表$/$手玩找规律,非常容易发现规律

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <bits/stdc++.h>
void solve()
{
int n, k;
std::cin >> n >> k;
if (n % (k + 1))
std::cout << 'A' << '\n';
else
std::cout << 'B' << '\n';
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
int T = 1;
std::cin >> T;
while (T--)
solve();
return 0;
}

例题:$CF1537D$

题目大意

$Alice$和$Bob$玩游戏,开始时有一个整数,两者轮流行动
每次行动可以在当前的$n$上减去其一个非$1$也非$n$的因子
无法行动者输,$Alice$先手,问谁必胜

解题思路

注释内容为打表
通过打表能很快发现结论为
若$n$为奇数,$Bob$必胜
若$n$为偶数,当$n$为$2$的奇数次幂时,$Bob$必胜,否则$Alice$必胜

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#include <bits/stdc++.h>
// const int N = 1e5 + 10;
// int dp[N];
// int n;
// int dfs(int u)
// {
// if (dp[u] != 0)
// return dp[u];
// for (int i = 2; i <= u - 1; ++i)
// if (u % i == 0)
// if (dfs(u - i) == -1)
// return dp[u] = 1;
// return dp[u] = -1;
// }
// void solve()
// {
// std::cin >> n;
// for (int i = 1; i <= n; ++i)
// if (dfs(i) == 1)
// std::cout << i << '\n';
// }
void solve()
{
std::set<int> two_pow;
int t = 2;
while (t <= 1e9)
{
two_pow.insert(t);
t *= 2;
}
int n;
std::cin >> n;
if (n & 1)
std::cout << "Bob" << '\n';
else
{
if (two_pow.count(n))
{
if ((int)std::log2(n) & 1)
std::cout << "Bob" << '\n';
else
std::cout << "Alice" << '\n';
}
else
std::cout << "Alice" << '\n';
}
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
int T = 1;
std::cin >> T;
while (T--)
solve();
return 0;
}

例题:$CF1527B2$

题目大意

$Alice$和$Bob$对一个$01$串做游戏
$Alice$先手,每轮可以做两种操作中的一种
$1.$任选一个位置的$0$,将其变为$1$,花费代价$1$
$2.$若当前串不为回文串,且上一个操作不为操作$2$,那么可以$std::reverse$这个$01$串
$01$串变为全$1$时游戏结束,此时花费代价少的人获胜,否则平局
问谁必胜

解题思路

串中对称的两个$1$不再有用
所以考虑对称的$00$,对称的$01$,长度为奇数的串中间是否为$0$,上一次操作是不是$reverse$这四个维度
做记忆化搜索即可

具体代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
#include <bits/stdc++.h>
const int N = 510;
const int M = 2;
const int INF = 0x7f7f7f7f;
int dp[N][N][M][M];
int n;
std::string s;
int dfs(int cnt00, int cnt01, int mz, int rev)
{
if (cnt00 == 0 && cnt01 == 0 && mz == 0)
return dp[cnt00][cnt01][mz][rev] = 0;
if (dp[cnt00][cnt01][mz][rev] != INF)
return dp[cnt00][cnt01][mz][rev];
int res = INF;
if (cnt00 > 0)
res = std::min(res, -dfs(cnt00 - 1, cnt01 + 1, mz, 0) + 1);
if (cnt01 > 0)
res = std::min(res, -dfs(cnt00, cnt01 - 1, mz, 0) + 1);
if (mz == 1)
res = std::min(res, -dfs(cnt00, cnt01, 0, 0) + 1);
if (rev == 0 && cnt01 > 0)
res = std::min(res, -dfs(cnt00, cnt01, mz, 1));
return dp[cnt00][cnt01][mz][rev] = res;
}
void solve()
{
std::cin >> n;
std::cin >> s;
s = "?" + s;
int cnt00 = 0, cnt01 = 0, mz = 0;
for (int i = 1; i <= n / 2; ++i)
{
if (s[i] == '0' && s[n - i + 1] == '0')
++cnt00;
if (s[i] != s[n - i + 1])
++cnt01;
}
if ((n & 1) && s[(n + 1) / 2] == '0')
mz = 1;
int ans = dfs(cnt00, cnt01, mz, 0);
if (ans > 0)
std::cout << "BOB" << '\n';
else if (ans < 0)
std::cout << "ALICE" << '\n';
else
std::cout << "DRAW" << '\n';
}
signed main()
{
std::ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
int T = 1;
std::cin >> T;
memset(dp, 0x7f, sizeof dp);
while (T--)
solve();
return 0;
}