当前位置: 首页 > article >正文

[蓝桥杯]取球博弈

取球博弈

题目描述

两个人玩取球的游戏。

一共有 NN 个球,每人轮流取球,每次可取集合 n1,n2,n3n1​,n2​,n3​中的任何一个数目。

如果无法继续取球,则游戏结束。

此时,持有奇数个球的一方获胜。

如果两人都是奇数,则为平局。

假设双方都采用最聪明的取法,

第一个取球的人一定能赢吗?

试编程解决这个问题。

输入描述

输入格式:

第一行 3 个正整数 n1,n2,n3 (0<n1,n2,n3<100)n1​,n2​,n3​ (0<n1​,n2​,n3​<100),空格分开,表示每次可取的数目。

第二行 5 个正整数 x1,x2,⋯,x5 (0<xi<1000)x1​,x2​,⋯,x5​ (0<xi​<1000),空格分开,表示 5 局的初始球数。

输出描述

输出一行 5 个字符,空格分开。分别表示每局先取球的人能否获胜。

能获胜则输出 +,次之,如有办法逼平对手,输出 0,无论如何都会输,则输出 -。

输入输出样例

示例

输入

1 2 3
1 2 3 4 5

输出

+ 0 + 0 -

运行限制

  • 最大运行时间:3s
  • 最大运行内存: 256M

总通过次数: 1061  |  总提交次数: 1355  |  通过率: 78.3%

难度: 困难   标签: 2016, 省赛, 动态规划, 博弈

算法思路:动态规划与状态压缩

本问题是一个博弈论问题,需要模拟双方轮流取球的过程,并根据最终球数的奇偶性判断胜负。核心思路是​​状态压缩+动态规划​​,将问题分解为四个维度的状态:剩余球数、先手奇偶性、后手奇偶性、当前玩家。通过自底向上的DP计算所有可能状态的最优策略结果。

算法步骤

  1. ​状态定义​​:

    • dp[s][a][b][turn]:剩余球数s,先手奇偶性a(0偶1奇),后手奇偶性b,当前玩家turn(0先手/1后手)
    • 值:1(先手赢)/0(平)/-1(先手输)
  2. ​状态转移​​:

    • ​先手操作​​(turn=0):尝试所有合法取球数k
      • 新先手奇偶性:new_a = a ^ (k & 1)
      • 转移到:dp[s-k][new_a][b][1]
      • 优先选赢(1),次选平(0),最后输(-1)
    • ​后手操作​​(turn=1):尝试所有合法取球数k
      • 新后手奇偶性:new_b = b ^ (k & 1)
      • 转移到:dp[s-k][a][new_b][0]
      • 优先选先手输(-1),次选平(0),最后赢(1)
  3. ​边界处理​​:

    • s=0:根据奇偶性直接返回结果
    • s<min_take:无法取球,直接结算
  4. ​结果输出​​:

    • 对每局初始球数x,查询dp[x][0][0][0]
    • 1→'+'0→'0'-1→'-'

代码实现(C++)

#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;int main() {// 读取取球规则vector<int> takes(3);cin >> takes[0] >> takes[1] >> takes[2];int min_take = *min_element(takes.begin(), takes.end());// 初始化DP数组 [s][a][b][turn]int dp[1001][2][2][2];const int UNCALC = INT_MIN;// 初始化s=0的边界状态for (int a = 0; a < 2; a++) {for (int b = 0; b < 2; b++) {for (int turn = 0; turn < 2; turn++) {if (a == 1 && b == 0) dp[0][a][b][turn] = 1;else if (a == 0 && b == 1) dp[0][a][b][turn] = -1;else dp[0][a][b][turn] = 0;}}}// DP计算:s从1到1000for (int s = 1; s <= 1000; s++) {for (int a = 0; a < 2; a++) {for (int b = 0; b < 2; b++) {for (int turn = 0; turn < 2; turn++) {// 默认无法取球时直接结算if (s < min_take) {if (a == 1 && b == 0) dp[s][a][b][turn] = 1;else if (a == 0 && b == 1) dp[s][a][b][turn] = -1;else dp[s][a][b][turn] = 0;continue;}if (turn == 0) {  // 先手操作int best = UNCALC;for (int k : takes) {if (k > s) continue;int new_a = a ^ (k & 1);  // 更新奇偶性int res = dp[s - k][new_a][b][1];  // 转后手if (res == 1) {  // 找到必胜策略best = 1;break;} else if (res == 0) {best = 0;  // 标记平局可能} else if (best == UNCALC) {best = -1;  // 无更优选择}}dp[s][a][b][turn] = (best == UNCALC) ? ((a == 1 && b == 0) ? 1 : (a == 0 && b == 1) ? -1 : 0) : best;} else {  // 后手操作int best = UNCALC;for (int k : takes) {if (k > s) continue;int new_b = b ^ (k & 1);  // 更新奇偶性int res = dp[s - k][a][new_b][0];  // 转先手if (res == -1) {  // 找到必杀策略best = -1;break;} else if (res == 0) {best = 0;  // 标记平局可能} else if (best == UNCALC) {best = 1;  // 无更优选择}}dp[s][a][b][turn] = (best == UNCALC) ? ((a == 1 && b == 0) ? 1 : (a == 0 && b == 1) ? -1 : 0) : best;}}}}}// 处理5局游戏vector<int> x(5);for (int i = 0; i < 5; i++) cin >> x[i];for (int i = 0; i < 5; i++) {int res = dp[x[i]][0][0][0];if (res == 1) cout << '+';else if (res == 0) cout << '0';else cout << '-';if (i < 4) cout << ' ';}return 0;
}

代码解析

  1. ​状态压缩​​:

    • 使用a/b的0/1表示奇偶性,将球数状态压缩为4个维度
    • dp[s][a][b][turn]存储当前状态的最优结果
  2. ​核心转移逻辑​​:

    • ​先手策略​​(L42-55):优先选择使后手必输的操作(返回1),次选平局
    • ​后手策略​​(L56-69):优先选择使先手必输的操作(返回-1),次选平局
    • ​奇偶更新​​:new_a = a ^ (k & 1)(异或等效模2加法)
  3. ​边界处理​​:

    • s=0时直接根据奇偶性判定(L24-28)
    • s<min_take时无法操作,直接结算(L35-39)
  4. ​结果查询​​:

    • 初始状态:dp[x][0][0][0](双方0球,先手操作)
    • 结果映射:1→+/0→0/-1→-

实例验证

输入:1 2 3 + 1 2 3 4 5

​输出​​:+ 0 + 0 -

​状态分析​​:

  1. ​球数1​​:先手取1→剩0球,先手奇数(1)后手偶数(0)→先手赢+
  2. ​球数2​​:
    • 取1:转状态(1,1,0,1)→后手取1→(0,1,1,0)→平局
    • 取2:转状态(0,0,0,1)→平局
    • 最优平局0
  3. ​球数3​​:取3→(0,1,0,1)→先手赢+
  4. ​球数4​​:
    • 取1:转(3,1,0,1)→后手取3→(0,1,1,0)→平局
    • 取2/3:后手可迫使平局
    • 最优平局0
  5. ​球数5​​:
    • 所有走法后手可迫使先手输-
输入:1 4 5 + 10 11 12 13 15

​输出​​:0 - 0 + +(符合样例)

注意事项

  1. ​奇偶性更新​​:

    • 使用异或运算a^(k&1)等效模2加法
    • 例:奇(1)+奇(1)→偶(0):1^1=0
  2. ​不可操作状态​​:

    • s<min_take时直接结算
    • 需单独处理避免越界
  3. ​DP初始化​​:

    • s=0时turn维度无意义,但需完整初始化
    • 使用UNCALC标记未计算状态
  4. ​策略优先级​​:

    • 先手:赢→平→输(找到赢立即break)
    • 后手:输→平→赢(找到输立即break)

多方位测试点

​测试类型​​测试数据​​预期结果​​验证要点​
最小球数1 2 3 + 1+边界处理
不可操作2 3 4 + 10直接奇偶结算
全奇取胜1 3 5 + 3+奇偶组合逻辑
强制平局2 2 2 + 40无最优解的平局处理
后手必杀1 4 5 + 11-后手策略优先级
大球数性能1 2 3 + 1000<1sDP效率验证
重复取法3 3 3 + 60操作去重逻辑

优化建议

  1. ​循环优化​​:

    // 预处理合法操作(去重排序)
    sort(takes.begin(), takes.end());
    takes.erase(unique(takes.begin(), takes.end()), takes.end());
  2. ​维度压缩​​:

    • 奇偶状态仅需2位,可用位运算压缩为单整数
    int state = (a << 2) | (b << 1) | turn;
  3. ​记忆化搜索​​:

    // 替代迭代DP,减少无效计算
    int dfs(int s, int a, int b, int turn) {if (dp[s][a][b][turn] != UNCALC) return dp[s][a][b][turn];// ...递归计算
    }
  4. ​剪枝策略​​:

    • 当找到必胜/必杀策略时立即跳出循环
    • 倒序排序取法,优先尝试大数加速命中
http://www.lryc.cn/news/2401803.html

相关文章:

  • Spring Security入门:创建第一个安全REST端点项目
  • [Java 基础]数组
  • fastadmin fildList 动态下拉框默认选中
  • java学习笔记——数组和二维数组
  • ‘pnpm‘ 不是内部或外部命令,也不是可运行的程序
  • Android Test2 获取系统android id
  • webpack打包学习
  • 基于Java(Jsp+servelet+Javabean)+MySQL实现图书管理系统
  • 服务器CPU被WMI Provider Host系统进程占用过高,导致系统偶尔卡顿的排查处理方案
  • JavaSwing之--JMenuBar
  • vue3+elementplus表格表头加图标及文字提示
  • 【物联网-S7Comm协议】
  • NLP中的input_ids是什么?
  • LeetCode Hot100刷题——划分字母区间
  • c++ 基于OpenSSL的EVP接口进行SHA3-512和SM3哈希计算
  • Vue3实现拖拽改变元素大小
  • Spring IoC 详解:原理、实现与实战
  • 深入Java NIO:构建高性能网络应用
  • 数据分析后台设计指南:实战案例解析与5大设计要点总结
  • 深度学习之模型压缩三驾马车:基于ResNet18的模型剪枝实战(1)
  • SSH/RDP无法远程连接?腾讯云CVM及通用服务器连接失败原因与超全排查指南
  • 网络测试实战:金融数据传输的生死时速
  • 数据库系统概论(十四)详细讲解SQL中空值的处理
  • 【信创-k8s】海光/兆芯+银河麒麟V10离线部署k8s1.31.8+kubesphere4.1.3
  • [蓝桥杯]三体攻击
  • 深入解析支撑向量机(SVM):原理、推导与实现
  • 一台电脑联网如何共享另一台电脑?网线方式
  • 面试题:SQL 中如何将 多行合并为一行(合并行数据为列)?
  • MacroDroid安卓版:自动化操作,让生活更智能
  • 力提示(force prompting)的新方法