洛谷 P2473 [SCOI2008] 奖励关
题目传送门
思路
确定算法
- 首先,一个物品能不能选,还要看有没有选前提物品;
- 其次,物品种类很少,只有 \(n \leq 15\);
- 因此,我们可以确定用 状压 \(dp\) 来求期望。
状态设计
- 首先,肯定要有记录当前是第 \(i\) 轮的一维;
- 其次,由于拿物品还要看已有物品集合,所以要有记录【当前已经拿了物品的集合】的一维;
- 设 \(dp_{i, s}\) 表示在 【第 \(1\) 到 \(i - 1\) 轮】 所拿到的物品集合为 \(s\) 时,第\(i\) 到 \(k\) 轮在最优情况下,所拿物品分值总和的期望。
(为什么这么设计到状态转移会说)
状态转移
采用倒推(为什么不用正推等会说):
- 若目前状态 \(s\) 不能拿物品 \(k\),那么:$$dp_{i, s} += \frac{dp_{i + 1, s}}{n}$$
- 若目前状态 \(s\) 可以拿物品 \(k\),那么:$$dp_{i, s} += \frac{max(dp_{i + 1, s | (1 << (k - 1))} + p_k, dp_{i + 1, s})}{n}$$分别对应选 \(k\) 与不选 \(k\)。
问题一:为什么状态这么设计呢?
- 为了适应倒推的转移方程;
- 如果状态设计为【\(dp_{i, s}\) 表示到第 \(i\) 轮拿完后,且状态为 \(s\) 时,已有的分数的期望】(适应正推),就会影响后面物品的选择。因为有前驱物品集合的限制,这样 \(dp\) 就会有后效性,导致不正确。
问题二:为什么采用倒推呢 ?
- 同样也是因为正推会有后效性。
边界条件
- 对于 \(\forall s \in [0, 2^n - 1]\),\(dp_{n + 1, s}\) 均为 \(0\)。
- 含义就是:在 \(1\) 到 \(n\) 轮选完的状态 \(s\) 下,已经结束了,不能再选了,因此期望为 \(0\)。
答案
- 就是 \(dp_{1, 0}\),含义就是:还没有开始时,一个物品都没选的状态,所能得到的期望分。
复杂度
- 空间:\(O(k \times n)\);
- 时间:\(O(k \times n \times 2^n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e2 + 7;
const int maxs = (1 << 15) + 7;
int n, m;
struct Prize {int p, s;} a[maxn];
double dp[maxn][maxs];
int main () {
scanf("%d%d", &n, &m);
for (int i = 1, x; i <= m; ++i) {
scanf("%d", &a[i].p);
while (1) {
scanf("%d", &x);
if (!x) break;
a[i].s |= 1 << (x - 1);
}
}
for (int i = n; i >= 1; --i) {
for (int s = 0; s < (1 << m); ++s) {
for (int k = 1; k <= m; ++k) {
if ((s & a[k].s) != a[k].s) dp[i][s] += dp[i + 1][s];
else dp[i][s] += max(dp[i + 1][s], dp[i + 1][s | (1 << (k - 1))] + a[k].p);
}
dp[i][s] /= m;
}
}
printf("%.6lf\n", dp[1][0]);
return 0;
}
反思
- 依旧只想出了状态,没对转移方程;
- 发现对这种 最优值期望 的转移方程我总会写成 最优值 的转移方程,并没有很好地理解期望的含义及计算方法;
- 运用 倒推 的意识少;
- 那种【前面选择】影响【后面选择】的 \(dp\) 最好用倒推,因为先考虑后面,就不用管前面的影响了。

浙公网安备 33010602011771号