题解:洛谷 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
【核心思想】
-
问题分析:给定 \(N\) 对卡片(每对数值为 \(A_i\),两两不同),共 \(2N\) 张。每次翻两张,配对成功得 \(A_i\) 分并移除,失败生命 \(-1\)。初始生命 \(L\),求最优策略下的期望得分。这是一个期望 DP 问题,核心在于状态设计需记录"完全未知的对数"和"已知单张的对数",通过概率分析四种翻牌场景进行记忆化搜索。
-
算法选择:
- 记忆化搜索(期望 DP):\(dp[p][l][known]\) 表示剩余 \(p\) 对完全未知、\(l\) 条生命、\(known\) 对已知单张时的最大期望得分
- 状态压缩:由于 \(A_i\) 两两不同,只需记录对数状态,得分用平均值 \(sum = \frac{\sum A_i}{N}\) 统一计算
-
关键步骤:
- 初始化:读取 \(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]\)
- 2a 配对成功(概率 \(\frac{1}{unknown-1}\)):翻到同对另一张
- 按概率加权求和返回期望
- 输出答案:\(dfs(N, L, 0)\)
-
时间/空间复杂度:
- 时间复杂度:\(O(N^2 \times L)\),状态数 \(O(N^2 \times L)\),每个状态 \(O(1)\) 转移
- 空间复杂度:\(O(N^2 \times L)\),三维记忆化数组
-
期望 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
浙公网安备 33010602011771号