2 条题解
-
1
满分解法(100 分)
两个离开条件要同时满足
把一次挑战的成功概率记作 。最终既要至少成功 次,又要能装下所有实际获得的残片。成功次数相同的两种结果,可能得到完全不同的背包和残片,因此只统计成功次数不够。
不过,背包容量与残片数不必分别记录。设处理完前 项挑战后的剩余容量为
$$B_i=K+\sum_{\substack{1\le t\le i\\\text{第 }t\text{ 项成功}}}a_t.$$成功得到背包时增加 ;成功得到残片时,,恰好表示占用一个容量。最终装载条件就是 。
中途的 可以为负。 例如先获得一个残片、后获得容量为 的背包,最终依然合法。题目只在全部挑战结束时检查装载,不能把暂时装不下的状态删掉。
把可能下降的容量变成非负、单调的量
直接以 为坐标,有两个麻烦:它可以为负,也可能因大背包而变得很大。观察每一步对它的影响:失败时不变,成功时增加 ,而 。也就是说,每处理一项挑战,剩余容量最多下降 。
于是用已经处理的挑战数补偿这一下降,定义
从第 项走向第 项时:
$$X_{i+1}= \begin{cases} X_i+1,&\text{第 }i+1\text{ 项失败},\\ X_i+a_{i+1}+1,&\text{第 }i+1\text{ 项成功}. \end{cases}$$两种增量都非负,所以 不会下降。初始 ,也就不再需要负下标。
在全部 项挑战结束时,装载条件等价于
这里加的是当前处理数 ,不是固定偏移量。正是它每一步都会增加 ,才使新的坐标单调不减。
超过目标阈值后,可以合并状态
我们只关心最后能否达到阈值 。由于 不会下降,一旦它已经不小于 ,以后无论哪些挑战成功,最终装载条件都会满足。
因此定义
只保留 共 种值。其中 表示“已经达到阈值”,并不表示实际 恰好等于 。
这个合并保留了后续行为。对于任意非负增量 ,都有
所以无论先截断再转移,还是先按真实值转移再截断,得到的状态都一致。成功次数仍然需要单独记录:装载已经有保证,不代表成功次数也已经达标。
概率动态规划
令 表示处理完前 项挑战,恰好成功 次,且 的概率。参数范围为 、、。
初始时还没有进行挑战,成功次数为 ,,因此
其余状态初值为 。
按 从小到大,枚举所有 。下一项挑战只有成功和失败两种互斥结果。
失败的概率是 ,成功次数不变,新的坐标增加 :
$$f\bigl(i+1,j,\min(N,s+1)\bigr) \mathrel{+}=f(i,j,s)(1-q_{i+1}).$$成功的概率是 ,成功次数增加 ,新的坐标增加 :
$$f\bigl(i+1,j+1,\min(N,s+a_{i+1}+1)\bigr) \mathrel{+}=f(i,j,s)q_{i+1}.$$注意,获得残片时 ,所以成功分支的第三维不变;获得容量为 的背包时,第三维增加 ,成功次数仍然必须增加。
最后只取成功次数不少于 且装载阈值已达到的状态:
各项挑战的结果相互独立,所以同一历史的概率乘上下一步相应的成功或失败概率,就是扩展历史的概率。不同历史互斥,落到相同状态时直接相加。两种转移完整且不重复地枚举了所有结果,而截断又保留了装载条件,故上述求和恰好得到所求概率。
用一个晚到背包的例子检查含义
取 ,两项挑战都以 的概率成功,属性依次为 。初始 。若第一项成功,先得到残片,,但是 ;这个状态仍被保留。第二项再成功时, 增加 ,最终达到阈值。
两项挑战的结果 成功次数 最终剩余容量 是否满足两个条件 失败、失败 否,成功次数不足 成功、失败 否,残片装不下 失败、成功 是 成功、成功 四种结果各有 的概率,答案为 。如果在第一项后删除负容量状态,就会错误地漏掉最后一行。
实现与复杂度
代码中的三维数组与 直接对应,保存完整处理层;每个状态只向下一层进行两次转移。输入的百分比先除以 ,概率使用
double,最后固定输出六位小数。时间复杂度为 ,因为要枚举处理数、成功次数与截断后的坐标,每次只做常数次转移。
空间复杂度为 ,用于完整三维概率表。题目上限下该状态表示满足给定空间限制,无需额外保存原始容量或残片数。
当 时,必有 。初始化直接给出 ,没有转移,答案自然为 ;当 时,初始第三维截断到 ,后续仍正常统计成功次数。
AC 代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, l, k; cin >> n >> l >> k; vector<double> p(n + 1); vector<int> a(n + 1); for (int i = 1; i <= n; i++) { cin >> p[i]; p[i] /= 100; } for (int i = 1; i <= n; i++) cin >> a[i]; // 第三维为 min(n, 当前剩余容量 + 已处理挑战数)。 vector dp(n + 1, vector(n + 1, vector<double>(n + 1))); dp[0][0][min(n, k)] = 1; for (int i = 0; i < n; i++) { for (int j = 0; j <= i; j++) { for (int s = 0; s <= n; s++) { double cur = dp[i][j][s]; dp[i + 1][j][min(n, s + 1)] += cur * (1 - p[i + 1]); dp[i + 1][j + 1][min(n, s + a[i + 1] + 1)] += cur * p[i + 1]; } } } double ans = 0; for (int j = l; j <= n; j++) ans += dp[n][j][n]; cout << fixed << setprecision(6) << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); } -
0
枚举成败结果(通过子任务 1,共 30 分)
枚举成败结果
在 的范围内,每项挑战只有成功和失败两种结果,全部结果至多有 种,可以直接搜索。
从前往后处理挑战,记录已经处理的位置、成功次数、剩余容量,以及当前这条成败历史的概率。剩余容量等于已有背包总容量减去已经获得的残片数,初始为 。
设当前处理第 项,成功概率为 。失败分支保持成功次数与剩余容量不变,并把当前概率乘以 ;成功分支把成功次数加 、剩余容量加 ,并把概率乘以 。当 时,这一步正是为新残片扣掉一个容量。
全部挑战处理完后,只有成功次数不少于 且剩余容量非负的结果才计入答案。每条完整历史只被枚举一次,不同历史互斥,因此把这些历史的概率相加即可。
哪些分支可以提前停止
若当前成功次数加上剩余挑战数仍小于 ,即使以后全部成功也无法达标,可以停止这一分支。若当前历史的概率为 ,它的任何扩展也不贡献概率,同样可以停止。
不能因为当前剩余容量小于 就剪枝。例如属性为 的两项挑战均成功时,第一项后暂时装不下残片,第二项得到的背包却可以补足容量。题目只要求最后能全部装下。
容量为 的背包挑战成功也要增加成功次数; 不代表自动满足装载条件; 时唯一的空历史合法,答案为 。
复杂度与适用范围
时间复杂度为 ,搜索树的节点数为指数级,每个节点只进行常数次更新。空间复杂度为 ,用于输入与递归调用栈。
该方法保证解决第 组的 。上述两种剪枝可能使某些更大输入也很容易,但不能改变最坏情况下的指数复杂度,因而不保证完整范围。
参考代码(C++20)
#include <bits/stdc++.h> using namespace std; using i64 = long long; void solve() { int n, l, k; cin >> n >> l >> k; vector<double> p(n); vector<int> a(n); for (double& x : p) { cin >> x; x /= 100; } for (int& x : a) cin >> x; double ans = 0; auto dfs = [&](auto&& self, int pos, int cnt, int bal, double prob) -> void { if (cnt + n - pos < l || prob == 0) return; if (pos == n) { if (bal >= 0) ans += prob; return; } self(self, pos + 1, cnt, bal, prob * (1 - p[pos])); self(self, pos + 1, cnt + 1, bal + a[pos], prob * p[pos]); }; dfs(dfs, 0, 0, k, 1); cout << fixed << setprecision(6) << ans << '\n'; } int main() { cin.tie(0)->sync_with_stdio(0); solve(); }
- 1
信息
- ID
- 2303
- 时间
- 1000ms
- 内存
- 128MiB
- 难度
- 9
- 标签
- (无)
- 递交数
- 33
- 已通过
- 11
- 上传者