题解:洛谷 AT_abc470_e Concentration

【题目来源】

洛谷:AT_abc470_e [ABC470E] Concentration

【题目描述】

Takahashi 正在玩一种类似于记忆配对的单人纸牌游戏。

共有 \(2N\) 张牌。每张牌的正面都写有一个数字,背面没有写任何东西。
对于每一个满足 \(1 \leq i \leq N\)\(i\),恰好有两张牌上写着 \(A_i\)(这些 \(A_i\) 各不相同)。

Takahashi 按照以下流程玩这个游戏:

  • \(2N\) 张牌洗牌后面朝下摊开。
  • life 设为 \(L\)score 设为 \(0\)
  • 重复以下步骤,直到 life 变为 \(0\) 或桌上没有牌为止:
    • 选一张桌上面朝下的牌,将其翻开,记其数字为 \(X\)
    • 再选一张桌上面朝下的牌,将其翻开,记其数字为 \(Y\)
    • 如果 \(X=Y\),则将这两张牌从桌上移除,得分增加 \(X\)
    • 如果 \(X\neq Y\),则将这两张牌重新扣下,life 减 \(1\)

请在 Takahashi 总是采取最优策略(期望分数最大化)的情况下,求出游戏结束时分数的期望值。

下面是更形式化的描述:

  • Takahashi 知道游戏的全部规则。
  • Takahashi 知道 \(A_1,\dots,A_N\) 的值。
  • \(B\) 是通过对长度为 \(2N\) 的序列 \((A_1,A_1,A_2,A_2,\ldots,A_N,A_N)\) 进行等概率随机排列后得到的序列。
  • 起始时,Takahashi 对 \(B\) 的内容一无所知。只要他了解了 \(B_i\) 的值,就会一直记住。
  • life 初始化为 \(L\),score 初始化为 \(0\)\(S = \{1,2,3,\ldots,2N\}\)。Takahashi 永远知道这些值。
  • 不断重复以下步骤,直至 life 变为 \(0\)\(S\) 为空:
    • 根据已获得的信息,Takahashi 从 \(S\) 中选择一个元素 \(i\)
    • \(B_i\) 的值被揭示,Takahashi 记住这个信息。
    • 根据已获得的信息(包括 \(B_i\) 的值),Takahashi 从 \(S\setminus\{i\}\) 中再选一个元素 \(j\)
    • \(B_j\) 的值被揭示,Takahashi 记住这个信息。
    • \(B_i=B_j\),则从 \(S\) 中移除 \(i\)\(j\),并将 \(B_i\) 加入到得分中。
    • \(B_i\neq B_j\),则 life 减少 \(1\)
  • Takahashi 总是以最大化期望分数为目标采取最优策略。

【输入】

输入为标准输入,格式如下:

\(N\ L\ A_1\ A_2\ \dots\ A_N\)

【输出】

输出答案。
若你的输出与标准答案的绝对误差或相对误差不超过 \(10^{-5}\),则认为是正确的。

【输入样例】

3 2
1 2 3

【输出样例】

3.8666666667

【核心思想】

  1. 问题分析:给定 \(N\) 对卡片(每对数值为 \(A_i\),两两不同),共 \(2N\) 张。每次翻两张,配对成功得 \(A_i\) 分并移除,失败生命 \(-1\)。初始生命 \(L\),求最优策略下的期望得分。这是一个期望 DP 问题,核心在于状态设计需记录"完全未知的对数"和"已知单张的对数",通过概率分析四种翻牌场景进行记忆化搜索。

  2. 算法选择

    • 记忆化搜索(期望 DP)\(dp[p][l][known]\) 表示剩余 \(p\) 对完全未知、\(l\) 条生命、\(known\) 对已知单张时的最大期望得分
    • 状态压缩:由于 \(A_i\) 两两不同,只需记录对数状态,得分用平均值 \(sum = \frac{\sum A_i}{N}\) 统一计算
  3. 关键步骤

    • 初始化:读取 \(N\)\(L\)\(A[1..N]\),计算平均值 \(sum = \frac{\sum_{i=1}^{N} A_i}{N}\)
    • 记忆化数组初始化\(dp\) 全设为 \(-1\)(未计算)
    • DFS 状态转移(状态 \((p, l, known)\)):
      • 终止条件\(l = 0\) 时返回 \(0\)\(known \geq p\) 时返回 \(p \times sum\)(所有牌至少翻过一次,可全部配对)
      • 计算剩余未知牌数 \(unknown = 2p - known\)
      • 场景一(概率 \(\frac{known}{unknown}\)):第一张翻到已知单张的配对牌,必定成功配对
        • \(dp[p-1][l][known-1] + sum\)
      • 场景二(概率 \(1 - \frac{known}{unknown}\)):第一张翻到完全未知的牌
        • 2a 配对成功(概率 \(\frac{1}{unknown-1}\)):翻到同对另一张
          • \(dp[p-1][l][known] + sum\)
        • 2b 翻到已知单张的配对牌(概率 \(\frac{known}{unknown-1}\),需 \(l \geq 2\)):
          • 本轮失败但可立即消掉已知对,\(dp[p-1][l-1][known] + sum\)
        • 2c 翻到另一张完全未知的牌(概率 \(1 - \frac{1}{unknown-1} - \frac{known}{unknown-1}\)):
          • 两张都变已知单张,\(dp[p][l-1][known+2]\)
      • 按概率加权求和返回期望
    • 输出答案\(dfs(N, L, 0)\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N^2 \times L)\),状态数 \(O(N^2 \times L)\),每个状态 \(O(1)\) 转移
    • 空间复杂度:\(O(N^2 \times L)\),三维记忆化数组
  5. 期望 DP 的核心思想

    • 状态设计的关键:不记录具体哪些对已知,只记录"完全未知的对数"和"已知单张的对数",因为 \(A_i\) 两两不同且策略只依赖于信息结构而非具体数值
    • 概率分解:将每次翻牌的所有可能情况按概率分解,最优策略体现在"已知单张优先配对"的贪心选择
    • 平均值替代:由于具体哪对成功不影响后续状态结构,用 \(sum\) 统一计算期望得分
    • 边界处理\(known \geq p\) 时所有牌至少被翻过一次,可以全部配对成功,直接返回 \(p \times sum\)
    • 适用于信息不完全博弈、概率期望计算、记忆化搜索类问题

【算法标签】

提高 #期望DP

【代码详解】

#include <bits/stdc++.h>
using namespace std;
const int N = 205; // 最大数据规模
int n, L; // n:数字种类数, L:初始生命值
double sum; // 所有数字的平均值(用于计算期望得分)
double dp[N][N][N]; // dp[p][l][known]:记忆化搜索数组
                    // p:剩余完全未知的对数, l:剩余生命数, known:已知单张的对数

double dfs(int p, int l, int known)
{
    // 如果已经计算过该状态,直接返回结果
    if (dp[p][l][known]!=-1)
        return dp[p][l][known];

    // 生命为0时无法继续游戏,得分为0
    if (l==0)
        return 0;

    // unknown:剩余未知牌的总数(每对完全未知的牌贡献2张,已知单张的对贡献1张)
    int unknown = 2 * p - known;

    // 如果已知单张的对数不少于完全未知的对数,说明所有牌都至少被翻开过一张
    // 此时可以直接全部配对成功,得分为p * sum(每对期望得分sum)
    if (known>=p)
        return p * sum;

    double sum1 = 0; // 当前状态的期望得分

    // 场景一:第一张翻开的是"已知单张的配对牌"
    // 概率 = known / unknown
    // 策略:优先配对已知单张,必定成功
    double p1 = 1.0 * known / unknown;
    if (known>=1)
        // 成功配对一对(p-1)
        // 已知单张减少1,得分增加sum
        sum1 += p1 * (dfs(p-1, l, known-1) + sum);

    // 场景二:第一张翻开的是"完全未知的牌"
    // 概率 = 1 - p1 = (2p - known) / unknown
    double p2 = (1-p1);

    // 情况2a:第二张翻开的是"与第一张同对的牌"(即配对成功)
    // 概率 = 1 / (unknown - 1)(第一张的配对牌只有1张)
    double p5 = 1.0 / (unknown - 1);
    // 成功配对,完全未知对数减1,已知单张数不变,得分增加sum
    sum1 += p2 * p5 * (dfs(p-1, l, known) + sum);

    // 情况2b:第二张翻开的是"已知单张的配对牌"
    // 概率 = known / (unknown - 1)
    double p3 = 1.0 * known / (unknown - 1);
    if (l>=2)
        // 本轮配对失败(两张属于不同对),生命减1
        // 但被翻到的已知单张对现在两张位置都知道了,可以立即消掉
        // 完全未知对数减1(新翻的变成已知单张),已知单张数不变(新产生1个,消掉1个)
        // 额外获得sum分(立即消掉的那对)
        sum1 += p2 * p3 * (dfs(p-1, l-1, known) + sum);

    // 情况2c:第二张翻开的是"另一张完全未知的牌"(即不同对)
    // 概率 = 1 - p3 - p5
    double p4 = 1 - p3 - p5;
    // 配对失败,生命减1
    // 两张完全未知的牌都变成了已知单张
    // 完全未知对数不变(因为翻了两张不同对,各剩一张未知),已知单张数加2
    sum1 += p2 * p4 * dfs(p, l-1, known+2);

    // 保存结果并返回(注意:return后面的cout不会执行)
    return dp[p][l][known] = sum1;
}
int main()
{
    cin >> n >> L; // 读入数字种类数和初始生命值
    // 初始化记忆化数组为-1(表示未计算)
    for (int i=0; i<=n; i++)
        for (int j=0; j<=L; j++)
            for (int k=0; k<=n; k++)
                dp[i][j][k] = -1;

    for (int i=1; i<=n; i++)
    {
        int x; cin >> x; // 读入每种数字的值
        sum += x; // 累加所有数字值
    }
    sum /= n; // 计算平均值(每对成功配对的期望得分)
    // 输出结果:从初始状态(n对完全未知,L条生命,0对已知单张)开始搜索
    cout << fixed << setprecision(15) << dfs(n, L, 0);
    return 0;
}

【运行结果】

3 2
1 2 3
3.8666666667
posted @ 2026-08-23 21:53  团爸讲算法  阅读(4)  评论(0)    收藏  举报