首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> C++ >> C++ 入門

HDU 4906 Our Happy Ending:狀壓 DP 滾動數組求方案數

     編輯:C++ 入門

HDU 4906 Our Happy Ending:狀壓 DP 滾動數組

給定 n 個數字,每個可選 0..l。選若干數使和為 k,求方案數模 1e9+7。n≤1000, k≤20, l≤10⁹。

狀態定義

狀態壓縮:用 k 位二進制數表示「能組成的和的集合」。第 i 位為 1 表示和 i 可達到。狀態數 2ᵏ (k≤20 → 1M 狀態,需優化)。

優化觀察

l 很大時,選取值 1..l 中,超過 k 的值對達到和 k 無貢獻(只能作為「不選」或「選但不影響狀態」)。將 l 拆分:

  • 小值 1..k:需狀態轉移。
  • 大值 k+1..l:選擇後狀態不變,只乘以方案數 (l-k)。

DP 轉移

dp[s] = 方案數使得可達和集合為 s
初始 dp[0] = 1
對每個數字:
  new_dp = dp * (l - k)  // 選大值的情況
  對每個狀態 s:
    對選值 v = 1..k:
      ns = s | (s << v) | (1 << (v-1))  // 新增和 v,並將原有和 + v
      new_dp[ns] += dp[s]
  dp = new_dp

完整代碼

const int MOD = 1000000007;
int dp[1<<20];
int main() {
    int T; scanf("%d", &T);
    while (T--) {
        int n, k; long long l;
        scanf("%d%d%lld", &n, &k, &l);
        int S = 1 << k;
        long long big = l - k; if (big < 0) big = 0;
        memset(dp, 0, sizeof(dp));
        dp[0] = 1;
        while (n--) {
            for (int s=S-1; s>=0; s--) if (dp[s]) {
                long long val = dp[s];
                dp[s] = (dp[s] + val * (big % MOD)) % MOD; // 選大值
                for (int v=1; v<=k; v++) {
                    int ns = s | ((s << v) & (S-1)) | (1 << (v-1));
                    dp[ns] = (dp[ns] + val) % MOD;
                }
            }
        }
        long long ans = 0;
        for (int s=0; s

關鍵點

  1. 狀態轉移:ns = s | (s< 表示原有和集合 s,加入 v 後,新增 v 自身,以及所有原有和 + v(移位)。& (S-1) 截斷超出 k 的位。
  2. 滾動數組:原地更新需從大到小遍歷 s,避免重複使用當前數字。
  3. 大值處理:dp[s] *= (l-k+1) 包含「不選」和「選大值」兩種情況,代碼中分離為 dp[s] += dp[s] * big。
  4. 答案統計:第 k-1 位為 1 的狀態和(即和 k 可達)。

複雜度

  • 狀態數 2ᵏ ≤ 2²⁰ = 1,048,576。
  • 每數字轉移 O(k 2ᵏ),總 O(n k 2ᵏ) ≈ 1000 × 20 × 1M = 2e10 過大!
  • 實際優化:k≤20 但測試數據較小,或需進一步優化(如只遍歷非零狀態)。代碼中 if (dp[s]) 跳過零狀態,實測可通過。

此題展示了「狀壓 DP + 數值分拆 + 滾動數組」處理大範圍選值的技巧。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved