【Light OJ-1344】Aladdin and the Game of Bracelets DP(记忆化搜索) + SG函数 博弈

Light OJ-1344 Aladdin and the Game of Bracelets

题目描述

It’s said that Aladdin had to solve seven mysteries before getting the Magical Lamp which summons a powerful Genie. Here we are concerned about the fourth mystery.

In the cave, Aladdin was moving forward after passing the pathway of magical stones. He found a door and he was about to open the door, but suddenly a Genie appeared and he addressed himself as the guardian of the door. He challenged Aladdin to play a game and promised that he would let Aladdin go through the door, if Aladdin can defeat the Genie. Aladdin defeated the Genie and continued his journey. However, let’s concentrate on the game.

The game was called ‘Game of Bracelets’. A bracelet is a linear chain of some pearls of various weights. The rules of the game are:

  1. There are n bracelets.
  2. Players alternate turns.
  3. In each turn, a player has to choose a pearl from any bracelet. Then all the pearls from that bracelet, that were not lighter than the pearl, will be removed. It may create some smaller bracelets or the bracelet will be removed if no pearl is left in the bracelet.
  4. The player, who cannot take a pearl in his turn, loses.

For example, two bracelets are: 5-1-7-2-4-5-3 and 2-1-5-3, here the integers denote the weights of the pearls. Suppose a player has chosen the first pearl (weight 5) from the first bracelet. Then all the pearls that are not lighter than 5, will be removed (from first bracelet). So, 5-1-7-2-4-5-3, the red ones will be removed and thus from this bracelet, three new bracelets will be formed, 1, 2-4 and 3. So, in the next turn the other player will have four bracelets: 1, 2-4, 3 and 2-1-5-3. Now if a player chooses the only pearl (weight 3) from the third bracelet, then the bracelet will be removed.

Now you are given the information of the bracelets. Assume that Aladdin plays first, and Aladdin and the Genie both play optimally. Your task is to find the winner.

样例输入

4
2
7 5 1 7 2 4 5 3
4 2 1 5 4
2
2 5 2
2 5 2
1
5 5 2 5 2 5
3
5 5 2 5 2 5
5 7 2 7 3 2
4 5 1 5 4

样例输出

Case 1: Aladdin
(2 5)
Case 2: Genie
Case 3: Aladdin
(1 2)(1 5)
Case 4: Aladdin
(2 7)(3 1)(3 5)

题意

有 n 个手镯,每个手镯上都有很多带有权值的珍珠。Aladdin 和 Genie 轮流操作,在其中一个手镯上选择一颗珍珠,然后被选择的这颗珍珠和这个手镯上权值大于或等于他的珍珠都会被删除。这样子一次操作后,这个手镯就会变成很多小手镯,或者如果手镯上没有珍珠了,手镯就会被删除。如果最后谁不能操作,谁就输了。最后输出第一步可以必胜的操作 “(选择的手镯,选择的权值)”。

题解

解题主要分为两部分,一是解决输赢,二是解决必胜的取珍珠问题。

第一部分

对于第一部分,由于每个手镯都是独立互不干扰的,所以我们可以考虑求出每个手镯的 SG 值,然后异或起来,如果最后异或的结果不为 0,则先手必胜,否则后手胜。
如此一来,难点变成了求 SG 值。由于每次输入的权值不同,会导致分割节点的不同,所以我们如果在输入权值之前就枚举所有分割情况,复杂度太高。所以我们选择在输入权值后,枚举手镯上的权值,进行分割,然后记忆化搜索。最后如果手镯上的珍珠有 0 个,必输,此时 SG = 0。如果有一个,必胜 SG = 1;
我们考虑用 $SG[i][j][k]$ 来存储第 $i$ 个手镯,区间为 $[j, k]$ 的 SG 值进行记忆化搜索,也就是DP,如果不了解记忆化搜索,可以去学习一下。
记忆化搜索 SG 值框架:

1
2
3
4
5
6
7
8
9
memset(SG, -1, sizeof SG);
int find(int num, int l, int r) {//对第num条项链,区间为 [l,r] 进行搜索
if (SG[num][l][r] != -1) return SG[num][l][r]; //如果已经被搜索过,直接返回
if (l == r) return SG[num][l][r] = 1; //如果手镯只有一个珍珠,SG赋值为1,并返回
if (r < l) return SG[num][l][r] = 0; //如果手镯没有珍珠,必输,返回0
int now_SG = 0;
//如果不满足以上条件,则寻找这种情况的SG值。
return SG[num][l][r] = now_SG;
}

现在的问题只剩下,如果不符合以上情况,应该怎么去找 SG 值,我们可以去枚举这个手镯上所有珍珠,假设去掉这个珍珠,他的SG值是多少。然后获得一个mex_SG的集合,那他的SG就是不属于这个集合的最小非负整数。
我们知道如果一个手镯 5-1-7-2-4-5-3,如果我们选择了 5,那就会删去 5,7,5,然后变成了 1,2-4,3 三个手镯,那此时去掉 5 的情况下的 SG 值就会 1,2-4,3,这三个手镯的异或值。
记忆化搜索SG值:

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
memset(SG, -1, sizeof SG);
int find(int num, int l, int r) {//对第num条项链,区间为 [l,r] 进行搜索
if (SG[num][l][r] != -1) return SG[num][l][r]; //如果已经被搜索过,直接返回
if (l == r) return SG[num][l][r] = 1; //如果手镯只有一个珍珠,SG赋值为1,并返回
if (r < l) return SG[num][l][r] = 0; //如果手镯没有珍珠,必输,返回0
int now_SG = 0;
//定义vis记录mex_SG集合
bool vis[maxn]; memset(vis, false, sizeof vis);//vis只能在函数体内定义。
//枚举每一颗珍珠
for (int i = l; i <= r; i++) {
int tmp = 0;// 记录当前的情况下的SG值
int t = 现在这个珍珠的权值;
for (int k = 0; k < 分成的区间; k++) {
//遍历所有分割的区间
tmp ^= find(num, k_l, k_r);//找第Num条手镯,区间为[k_l, k_r]的 SG 值
}
vis[tmp] = true;//标记SG值
}
//对应mex_SG找到这个区间的SG值
for (int i = 0; i < maxn; i++) {
if (vis[i] == false) {
now_SG = i; break;
}
}
return SG[num][l][r] = now_SG;
}

第二部分

解决第二部分问题的前提是,知道输赢的结果,如果是Genie赢,则跳过。Aladdin赢,则判断。
这个判断我们也是枚举所有的珍珠,看选择这颗珍珠是否必胜,如果必胜,则记录下来。

最后排序,去重,然后输出。

那我们假设我们已经的得到了所有手镯的异或值为 ans (ans > 0)。
当我们枚举第 num 条手镯时,我们就先用 ans = ans ^ SG[num][1][len_num],这个结果是消除了第 num 条手镯的影响,也就是除了第 num 条手镯之外的其他手镯的异或值。
然后我们得到了第 num 条手镯选择了某颗珍珠的 SG 值为 now_SG。
那么我们就将后来的 ans ^ now_SG,如果结果为 0,则说明选择这颗珍珠必胜。
总的来说就是第一部分的 ans ^ SG[num][1][len_num] ^ now_SG。
我们就可以枚举所有的珍珠,如果可以使得最后的异或结果为 0 则加入到答案中,后来排序,去重再输出。

问题: 为什么异或结果为 0 必胜?
因为最后计算出来的结果是选择了这颗珍珠之后的输赢状态。Aladdin 选择了这颗珍珠,则到了 Genie 先手,此时 ans = 0,先手必败,则Genie 必败,因此 Aladdin 必胜。

代码

在代码中,我使用了 a[i][j] 来表示第 i 条手镯第 j 个珍珠的权值,用 a[i][0] 存储第 i 条手镯的长度。
用 node 结构体来存储最后的答案,然后重载了 < 和 == ,用于答案的排序和去重。

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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int maxn = 55;
int sg[maxn][maxn][maxn], a[maxn][maxn], n, T, ca;

struct node {
int a, b;
node(int c = 0, int d = 0) {a = c, b = d;}
bool operator < (node &t) const {
if (a == t.a) return b < t.b;
return a < t.a;
}
bool operator == (node &t) const {
return (a == t.a && b == t.b);
}
} res[maxn * maxn];

int findSG(int num, int l, int r) {
if (sg[num][l][r] != -1) return sg[num][l][r];
if (r == l) {
return sg[num][l][r] = 1;
}
if (r < l) {
return sg[num][l][r] = 0;
}

bool vis[maxn];
memset(vis, false, sizeof vis);
for (int i = l; i <= r; i++) {
int t = a[num][i], tmp[maxn], cnt = 0, ans = 0;
//记录比 t 大的 所有分割点
tmp[cnt++] = l - 1;
for (int p = l; p <= r; p++) {
if (a[num][p] >= t) tmp[cnt++] = p;
}
tmp[cnt++] = r + 1;

for (int p = 1; p < cnt; p++) {
ans ^= findSG(num, tmp[p - 1] + 1, tmp[p] - 1);
}
vis[ans] = true;
}
for (int i = 0; i < maxn; i++) {
if (vis[i] == false) {
sg[num][l][r] = i; break;
}
}
return sg[num][l][r];
}

int main()
{
scanf("%d", &T);
while (T--) {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", &a[i][0]);
for (int j = 1; j <= a[i][0]; j++) {
scanf("%d", &a[i][j]);
}
}
memset(sg, -1, sizeof sg);
int ans = 0;
for (int i = 0; i < n; i++) {
ans ^= findSG(i, 1, a[i][0]);
}

printf("Case %d: ", ++ca);
if (ans) printf("Aladdin\n");
else printf("Genie\n");

if (ans) {
int cnt = 0;
for (int i = 0; i < n; i++) {
// 枚举手镯
for (int j = 1; j <= a[i][0]; j++) {
// 枚举选中的珍珠
int ret = ans ^ sg[i][1][a[i][0]];
int t = a[i][j], tmp[maxn], k = 0;
//记录比 t 大的 所有分割点
tmp[k++] = 0;
for (int p = 1; p <= a[i][0]; p++) {
if (a[i][p] >= t) tmp[k++] = p;
}
tmp[k++] = a[i][0] + 1;
for (int p = 1; p < k; p++) {
ret ^= findSG(i, tmp[p - 1] + 1, tmp[p] - 1);
}
//与其他相连进行Nim和
if (!ret) {
res[cnt++] = node(i, t);
}
}
}
sort(res, res + cnt);
cnt = unique(res, res + cnt) - res;
for (int i = 0; i < cnt; i++) {
printf("(%d %d)", res[i].a + 1, res[i].b);
}
printf("\n");
}
}

return 0;
}
/*
4
2
7 5 1 7 2 4 5 3
4 2 1 5 4
2
2 5 2
2 5 2
1
5 5 2 5 2 5
3
5 5 2 5 2 5
5 7 2 7 3 2
4 5 1 5 4
*/