2 条题解

  • 1
    @ 2026-9-26 11:59:31

    满分解法(100 分)

    两个离开条件要同时满足

    把一次挑战的成功概率记作 qi=pi/100q_i=p_i/100。最终既要至少成功 LL 次,又要能装下所有实际获得的残片。成功次数相同的两种结果,可能得到完全不同的背包和残片,因此只统计成功次数不够。

    不过,背包容量与残片数不必分别记录。设处理完前 ii 项挑战后的剩余容量为

    $$B_i=K+\sum_{\substack{1\le t\le i\\\text{第 }t\text{ 项成功}}}a_t.$$

    成功得到背包时增加 aia_i;成功得到残片时,ai=−1a_i=-1,恰好表示占用一个容量。最终装载条件就是 BN≥0B_N\ge 0。

    中途的 BiB_i 可以为负。 例如先获得一个残片、后获得容量为 11 的背包,最终依然合法。题目只在全部挑战结束时检查装载,不能把暂时装不下的状态删掉。

    把可能下降的容量变成非负、单调的量

    直接以 BiB_i 为坐标,有两个麻烦:它可以为负,也可能因大背包而变得很大。观察每一步对它的影响:失败时不变,成功时增加 aia_i,而 ai≥−1a_i\ge -1。也就是说,每处理一项挑战,剩余容量最多下降 11。

    于是用已经处理的挑战数补偿这一下降,定义

    Xi=Bi+i.X_i=B_i+i.

    从第 ii 项走向第 i+1i+1 项时:

    $$X_{i+1}= \begin{cases} X_i+1,&\text{第 }i+1\text{ 项失败},\\ X_i+a_{i+1}+1,&\text{第 }i+1\text{ 项成功}. \end{cases}$$

    两种增量都非负,所以 XiX_i 不会下降。初始 X0=K≥0X_0=K\ge 0,也就不再需要负下标。

    在全部 NN 项挑战结束时,装载条件等价于

    BN≥0⟺XN≥N.B_N\ge 0\quad\Longleftrightarrow\quad X_N\ge N.

    这里加的是当前处理数 ii,不是固定偏移量。正是它每一步都会增加 11,才使新的坐标单调不减。

    超过目标阈值后,可以合并状态

    我们只关心最后能否达到阈值 NN。由于 XiX_i 不会下降,一旦它已经不小于 NN,以后无论哪些挑战成功,最终装载条件都会满足。

    因此定义

    Si=min⁡(N,Xi),S_i=\min(N,X_i),

    只保留 0,1,…,N0,1,\ldots,N 共 N+1N+1 种值。其中 Si=NS_i=N 表示“已经达到阈值”,并不表示实际 XiX_i 恰好等于 NN。

    这个合并保留了后续行为。对于任意非负增量 dd,都有

    min⁡(N,min⁡(N,x)+d)=min⁡(N,x+d).\min\bigl(N,\min(N,x)+d\bigr)=\min(N,x+d).

    所以无论先截断再转移,还是先按真实值转移再截断,得到的状态都一致。成功次数仍然需要单独记录:装载已经有保证,不代表成功次数也已经达标。

    概率动态规划

    令 f(i,j,s)f(i,j,s) 表示处理完前 ii 项挑战,恰好成功 jj 次,且 Si=sS_i=s 的概率。参数范围为 0≤i≤N0\le i\le N、0≤j≤i0\le j\le i、0≤s≤N0\le s\le N。

    初始时还没有进行挑战,成功次数为 00,X0=KX_0=K,因此

    f(0,0,min⁡(N,K))=1,f\bigl(0,0,\min(N,K)\bigr)=1,

    其余状态初值为 00。

    按 ii 从小到大,枚举所有 j,sj,s。下一项挑战只有成功和失败两种互斥结果。

    失败的概率是 1−qi+11-q_{i+1},成功次数不变,新的坐标增加 11:

    $$f\bigl(i+1,j,\min(N,s+1)\bigr) \mathrel{+}=f(i,j,s)(1-q_{i+1}).$$

    成功的概率是 qi+1q_{i+1},成功次数增加 11,新的坐标增加 ai+1+1a_{i+1}+1:

    $$f\bigl(i+1,j+1,\min(N,s+a_{i+1}+1)\bigr) \mathrel{+}=f(i,j,s)q_{i+1}.$$

    注意,获得残片时 ai+1=−1a_{i+1}=-1,所以成功分支的第三维不变;获得容量为 00 的背包时,第三维增加 11,成功次数仍然必须增加。

    最后只取成功次数不少于 LL 且装载阈值已达到的状态:

    答案=∑j=LNf(N,j,N).\text{答案}=\sum_{j=L}^{N}f(N,j,N).

    各项挑战的结果相互独立,所以同一历史的概率乘上下一步相应的成功或失败概率,就是扩展历史的概率。不同历史互斥,落到相同状态时直接相加。两种转移完整且不重复地枚举了所有结果,而截断又保留了装载条件,故上述求和恰好得到所求概率。

    用一个晚到背包的例子检查含义

    取 N=2,L=1,K=0N=2,L=1,K=0,两项挑战都以 50%50\% 的概率成功,属性依次为 −1,1-1,1。初始 X0=0X_0=0。若第一项成功,先得到残片,B1=−1B_1=-1,但是 X1=B1+1=0X_1=B_1+1=0;这个状态仍被保留。第二项再成功时,X2X_2 增加 1+1=21+1=2,最终达到阈值。

    两项挑战的结果 成功次数 最终剩余容量 是否满足两个条件
    失败、失败 00 否,成功次数不足
    成功、失败 11 −1-1 否,残片装不下
    失败、成功 11 是
    成功、成功 22 00

    四种结果各有 1/41/4 的概率,答案为 1/21/2。如果在第一项后删除负容量状态,就会错误地漏掉最后一行。

    实现与复杂度

    代码中的三维数组与 f(i,j,s)f(i,j,s) 直接对应,保存完整处理层;每个状态只向下一层进行两次转移。输入的百分比先除以 100100,概率使用 double,最后固定输出六位小数。

    时间复杂度为 O(N3)O(N^3),因为要枚举处理数、成功次数与截断后的坐标,每次只做常数次转移。

    空间复杂度为 O(N3)O(N^3),用于完整三维概率表。题目上限下该状态表示满足给定空间限制,无需额外保存原始容量或残片数。

    当 N=0N=0 时,必有 L=0L=0。初始化直接给出 f(0,0,0)=1f(0,0,0)=1,没有转移,答案自然为 11;当 K>NK>N 时,初始第三维截断到 NN,后续仍正常统计成功次数。

    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
      @ 2026-9-26 11:59:31

      枚举成败结果(通过子任务 1,共 30 分)

      枚举成败结果

      在 N≤10N\le 10 的范围内,每项挑战只有成功和失败两种结果,全部结果至多有 2102^{10} 种,可以直接搜索。

      从前往后处理挑战,记录已经处理的位置、成功次数、剩余容量,以及当前这条成败历史的概率。剩余容量等于已有背包总容量减去已经获得的残片数,初始为 KK。

      设当前处理第 ii 项,成功概率为 qi=pi/100q_i=p_i/100。失败分支保持成功次数与剩余容量不变,并把当前概率乘以 1−qi1-q_i;成功分支把成功次数加 11、剩余容量加 aia_i,并把概率乘以 qiq_i。当 ai=−1a_i=-1 时,这一步正是为新残片扣掉一个容量。

      全部挑战处理完后,只有成功次数不少于 LL 且剩余容量非负的结果才计入答案。每条完整历史只被枚举一次,不同历史互斥,因此把这些历史的概率相加即可。

      哪些分支可以提前停止

      若当前成功次数加上剩余挑战数仍小于 LL,即使以后全部成功也无法达标,可以停止这一分支。若当前历史的概率为 00,它的任何扩展也不贡献概率,同样可以停止。

      不能因为当前剩余容量小于 00 就剪枝。例如属性为 −1,1-1,1 的两项挑战均成功时,第一项后暂时装不下残片,第二项得到的背包却可以补足容量。题目只要求最后能全部装下。

      容量为 00 的背包挑战成功也要增加成功次数;L=0L=0 不代表自动满足装载条件;N=0N=0 时唯一的空历史合法,答案为 11。

      复杂度与适用范围

      时间复杂度为 O(2N)O(2^N),搜索树的节点数为指数级,每个节点只进行常数次更新。空间复杂度为 O(N)O(N),用于输入与递归调用栈。

      该方法保证解决第 11 组的 N≤10N\le 10。上述两种剪枝可能使某些更大输入也很容易,但不能改变最坏情况下的指数复杂度,因而不保证完整范围。

      参考代码(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
      上传者