NeFut Logo NeFut
EN 管理员登录

解密公平组合游戏:博弈论中的P与N状态分析

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#C++ #Tutorial

核心逻辑与数学原理

博弈论(Game Theory)是离散数学中研究具有对抗性质决策交互的理论。在 NOIP 竞赛中,考察的边界高度集中于公平组合游戏(Impartial Combinatorial Games, ICG)

1. 公平组合游戏(ICG)的特征

一个游戏被称为公平组合游戏,必须满足以下硬性约束:

2. 状态空间划分:P-position 与 N-position

在 ICG 的状态空间拓扑图中,所有的游戏状态可以被严格且唯一地划分为两类:

状态判定的三大公理

根据倒推归纳法(Backward Induction),状态的本质由以下三条底层逻辑锁定:

  1. 终结状态必为 P 状态:所有无法做出任何合法移动的终止状态,均为 P-position(根据无法移动者输的规则)。
  2. 可达 P 必为 N:如果从当前状态出发,存在至少一种合法的移动能转移到某个 P-position,则当前状态为 N-position(当前玩家只需果断将这个必败态抛给对手,自己即可获胜)。
  3. 后继皆 N 必为 P:如果从当前状态出发,无论做出何种合法移动,得到的后继状态全部都是 N-position,则当前状态为 P-position(当前玩家无论怎么挣扎,对手面对的都是必胜态)。

算法推导与状态设计

状态图的拓扑记忆化搜索

对于局势相对简单、状态可以被数值量化的博弈游戏,我们可以将游戏的所有可能状态抽象为有向无环图(DAG)上的节点,合法移动抽象为有向边。

$$f[u] = \bigvee_{(u, v) \in E} \big( f[v] == 0 \big)$$

代数含义:对节点 $u$ 的所有后继节点 $v$ 进行逻辑或(OR)运算。只要后继中有一个是必败态($f[v]==0$),当前就是必胜态($f[u]=1$);如果所有后继均为必胜态,则当前为必败态。


C++ 标准源码

以下源码展示了如何通过记忆化搜索,对一个典型的博弈状态图进行 P/N 状态判定。

#include <iostream>
#include <vector>
#include <cstring>

const int MAXS = 10000;
int f[MAXS + 5]; // 状态记忆化数组:-1 表示未计算,0 表示 P-position,1 表示 N-position

// 获取当前状态的所有合法后继状态(示例:每次可以减去 1, 3, 或 4)
std::vector<int> get_next_states(int state) {
    std::vector<int> next_states;
    if (state >= 1) next_states.push_back(state - 1);
    if (state >= 3) next_states.push_back(state - 3);
    if (state >= 4) next_states.push_back(state - 4);
    return next_states;
}

// 记忆化搜索判定 P/N 状态
int judge_position(int state) {
    if (f[state] != -1) return f[state]; // 拦截已计算状态

    std::vector<int> next_moves = get_next_states(state);

    // 边界公理 1:无法移动的终止状态必为 P-position
    if (next_moves.empty()) {
        f[state] = 0;
        return 0;
    }

    // 遍历所有可能的后继移动
    for (int next_state : next_moves) {
        // 公理 2:只要后继状态中存在一个必败态(P),当前状态就转化为必胜态(N)
        if (judge_position(next_state) == 0) {
            f[state] = 1;
            return 1;
        }
    }

    // 公理 3:所有后继移动全部走向必胜态,当前玩家无路可逃,当前状态为必败态(P)
    f[state] = 0;
    return 0;
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    std::memset(f, -1, sizeof(f)); // 初始化状态表为 -1

    int t;
    if (std::cin >> t) {
        while (t--) {
            int initial_state;
            std::cin >> initial_state;

            if (judge_position(initial_state) == 1) {
                std::cout << "First player wins (N-position)\n";
            } else {
                std::cout << "Second player wins (P-position)\n";
            }
        }
    }
    return 0;
}

NOIP 实战避坑指南

  1. 混淆 P 状态(必败)与 N 状态(必胜)的代数归宿 在编写博弈论搜索时,经常有选手在判断条件上写反。务必死记:当前是 N,当且仅当后继有 P。很多选手误写成“后继全为 P 才是 N”,直接导致整个博弈树的胜负逻辑发生完全对调。
  2. 多组询问下忘记重置或错误重置记忆化数组 如果博弈游戏的规则(如每次允许取走的石子集合)在不同的输入数据中是会发生改变的,那么上一次询问得到的 f 数组局势绝对不能沿用到下一次询问中。必须在每组测试数据输入时使用 std::memset(f, -1, sizeof(f)) 进行彻底洗牌,否则会引发严重的跨数据状态污染。

经典 NOIP/洛谷 真题

1. 洛谷 P1290 黄金分割数游戏

2. 洛谷 P2148 [SDOI2009] E&D

原文链接: local://22.1

[h] 返回首页