题目链接
题目描述
在一个一维的格子条上,Alice 和 Bob 轮流填入字母 'L' 或 'O'。第一个成功构成连续子串 "LOL" 的玩家立即获胜。如果所有格子都填满仍未出现 "LOL",则为平局。
给定一个包含 'L', 'O', '*' (空格) 的初始局面,你需要判断在 Alice 先手且双方都采取最优策略的情况下,最终的游戏结果。
解题思路
这是一个典型的组合游戏,其核心是找到最优策略。我们可以通过递归搜索所有可能的游戏进程,并用记忆化来优化,这本质上是一种动态规划(记忆化搜索)。
1. 状态定义
游戏中的任何一个局面都可以用一个字符串来唯一表示。我们的目标是判断在某个局面下,当前先手的玩家是必胜、必败还是只能平局。
2. 递归函数设计 (Minimax)
我们可以设计一个递归函数 dfs(state)
,它的功能是判断在当前是 state
局面时,轮到当前玩家走,他能得到的最好结果是什么。我们定义返回值如下:
1
: 当前玩家有必胜策略。0
: 当前玩家最好也只能逼平。-1
: 当前玩家必败。
3. 递归逻辑
- 终止条件:
- 对手已获胜: 检查当前
state
是否已经包含了 "LOL"。如果包含,说明上一个玩家(即当前玩家的对手)已经获胜,所以当前玩家必败。返回-1
。 - 平局: 检查当前
state
是否已无空格*
。如果填满了且无人获胜,则为平局。返回0
。
- 对手已获胜: 检查当前
- 递归探索:
- 遍历当前
state
中所有的空格*
。 - 对于每个空格,尝试填入 'L' 和 'O',形成一个新的局面
new_state
。 - 对
new_state
递归调用dfs(new_state)
。这个调用的返回值,是从对手的视角看的结果。
- 遍历当前
- 结果判定:
- 如果
dfs(new_state)
返回-1
,这意味着对手在new_state
局面下必败。那么,这对于当前玩家来说就是一个必胜的走法。我们立即可以确定当前state
是必胜的,返回1
。 - 如果
dfs(new_state)
返回0
,这意味着对手可以逼平。这对于当前玩家来说,意味着他至少可以保证不输。我们记录下存在一个可以走向平局的策略。 - 如果
dfs(new_state)
返回1
,这意味着对手必胜。这是一个对当前玩家不利的走法。
- 如果
- 最终返回:
- 如果在遍历所有可能的走法后,我们找到了任何一个能让对手必败的走法,那么当前玩家必胜,返回
1
。 - 如果没有必胜走法,但至少有一个能逼平的走法,那么当前玩家的最好结果就是平局,返回
0
。 - 如果所有走法都会导向对手必胜的局面,那么当前玩家必败,返回
-1
。
- 如果在遍历所有可能的走法后,我们找到了任何一个能让对手必败的走法,那么当前玩家必胜,返回
4. 记忆化
游戏中的同一局面可能通过不同路径到达。为了避免重复计算,我们使用一个 map
(C++) / HashMap
(Java) / dict
(Python) 来存储已经计算过的 state
的结果。在 dfs
函数的入口处,首先检查当前 state
是否已经有结果,如果有则直接返回,这大大提高了算法效率。
代码
#include <iostream>
#include <string>
#include <vector>
#include <map>
using namespace std;
map<string, int> memo;
int dfs(string s) {
if (memo.count(s)) {
return memo[s];
}
if (s.find("LOL") != string::npos) {
return memo[s] = -1;
}
int star_count = 0;
for (char c : s) {
if (c == '*') star_count++;
}
if (star_count == 0) {
return memo[s] = 0;
}
bool can_tie = false;
for (int i = 0; i < s.length(); ++i) {
if (s[i] == '*') {
s[i] = 'L';
if (dfs(s) == -1) {
s[i] = '*';
return memo[s] = 1;
}
if (dfs(s) == 0) {
can_tie = true;
}
s[i] = '*';
s[i] = 'O';
if (dfs(s) == -1) {
s[i] = '*';
return memo[s] = 1;
}
if (dfs(s) == 0) {
can_tie = true;
}
s[i] = '*';
}
}
if (can_tie) {
return memo[s] = 0;
} else {
return memo[s] = -1;
}
}
void solve() {
string s;
cin >> s;
memo.clear();
int result = dfs(s);
if (result == 1) {
cout << "Alice\n";
} else if (result == 0) {
cout << "Tie\n";
} else {
cout << "Bob\n";
}
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int t;
cin >> t;
while (t--) {
solve();
}
return 0;
}
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
public class Main {
static Map<String, Integer> memo;
static int dfs(String s) {
if (memo.containsKey(s)) {
return memo.get(s);
}
if (s.contains("LOL")) {
memo.put(s, -1);
return -1;
}
if (!s.contains("*")) {
memo.put(s, 0);
return 0;
}
boolean canTie = false;
char[] chars = s.toCharArray();
for (int i = 0; i < chars.length; i++) {
if (chars[i] == '*') {
chars[i] = 'L';
int resL = dfs(new String(chars));
if (resL == -1) {
chars[i] = '*';
memo.put(s, 1);
return 1;
}
if (resL == 0) {
canTie = true;
}
chars[i] = 'O';
int resO = dfs(new String(chars));
if (resO == -1) {
chars[i] = '*';
memo.put(s, 1);
return 1;
}
if (resO == 0) {
canTie = true;
}
chars[i] = '*'; // Backtrack
}
}
int result = canTie ? 0 : -1;
memo.put(s, result);
return result;
}
public static void solve(Scanner sc) {
String s = sc.next();
memo = new HashMap<>();
int result = dfs(s);
if (result == 1) {
System.out.println("Alice");
} else if (result == 0) {
System.out.println("Tie");
} else {
System.out.println("Bob");
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int t = sc.nextInt();
while (t-- > 0) {
solve(sc);
}
}
}
import sys
memo = {}
def dfs(s):
if s in memo:
return memo[s]
if "LOL" in s:
memo[s] = -1
return -1
if '*' not in s:
memo[s] = 0
return 0
can_tie = False
for i in range(len(s)):
if s[i] == '*':
# Try placing 'L'
s_list = list(s)
s_list[i] = 'L'
new_s_l = "".join(s_list)
res_l = dfs(new_s_l)
if res_l == -1:
memo[s] = 1
return 1
if res_l == 0:
can_tie = True
# Try placing 'O'
s_list[i] = 'O'
new_s_o = "".join(s_list)
res_o = dfs(new_s_o)
if res_o == -1:
memo[s] = 1
return 1
if res_o == 0:
can_tie = True
result = 0 if can_tie else -1
memo[s] = result
return result
def solve():
s = sys.stdin.readline().strip()
global memo
memo = {}
# Python recursion limit can be an issue
# The number of empty cells is max depth
empty_cells = s.count('*')
if sys.getrecursionlimit() < empty_cells + 5:
sys.setrecursionlimit(empty_cells + 5)
result = dfs(s)
if result == 1:
print("Alice")
elif result == 0:
print("Tie")
else:
print("Bob")
def main():
t_str = sys.stdin.readline()
if not t_str.strip():
return
t = int(t_str)
for _ in range(t):
solve()
if __name__ == "__main__":
main()
算法及复杂度
- 算法:记忆化搜索 (Minimax 动态规划)
- 时间复杂度:
,其中
是字符串的长度,
是从初始状态可达到的所有不同游戏局面的数量。由于记忆化的存在,每个局面只会被计算一次。在每次计算中,我们需要遍历字符串来寻找可落子的位置,成本为
。
- 空间复杂度:
,主要用于存储记忆化表的键(字符串)和值。递归的深度最多为棋盘上的空格数。