Official

D - 均等な買い物 / Equal Shopping Editorial by admin

gemini-3-flash-thinking

概要

\(N\) 軒の店から高橋君が \(P\) 軒、青木君が \(Q\) 軒選んで買い物をしたとき、二人の合計金額が一致するような計画の総数を求める問題です。クエリごとに店の買い物可能範囲 \([L_i, R_i]\) が更新されますが、制約の \(N \times M \le 2000\) および \(P, Q \le 3\)、さらに金額が最大 \(20\) と非常に小さいことを利用して、動的計画法(DP)を高速に回すことで解くことができます。

考察

1. 状態量の少なさに注目する

この問題の鍵は、各パラメータが非常に小さいことです。 - 高橋君と青木君が買い物をする店舗数 \(P, Q\) は最大 \(3\) です。 - 各店舗の金額 \(L_i, R_i\) は最大 \(20\) です。 - したがって、一人の合計金額の最大値は \(3 \times 20 = 60\) となります。

二人の合計金額が等しいという条件は、「(高橋君の合計)-(青木君の合計)= 0」 と言い換えることができます。この差の範囲は \(-60\) から \(60\) までのわずか \(121\) 通りしかありません。

2. 計算量の見積もり

クエリ回数 \(M\) と店舗数 \(N\) について、\(N \times M \le 2000\) という特殊な制約があります。これは、「クエリごとに全店舗を対象とした DP を一から解き直しても間に合う」 ことを示唆しています。 1回あたりの DP の計算量は、おおよそ \(O(N \times P \times Q \times (\text{金額の差の範囲}))\) となります。 最大ケースでも \(2000 \times 3 \times 3 \times 121 \approx 2.1 \times 10^6\) 程度の計算量であり、これを \(M\) 回ではなく全体を通して合計 \(N \times M\) のオーダーで処理できるため、十分に実行時間制限に間に合います。

アルゴリズム

動的計画法の定義

以下の状態をもつ DP を行います。 dp[p][q][diff]: - \(p\):高橋君が買い物をした店舗数 (\(0 \le p \le P\)) - \(q\):青木君が買い物をした店舗数 (\(0 \le q \le Q\)) - \(diff\):現在の合計金額の差(高橋 - 青木)。負の値を扱うため、オフセット(\(60\))を加えて \(0 \sim 120\) の範囲で管理します。

遷移

各店舗 \(i\) について、以下の 3 つの選択肢があります。

  1. 誰も買い物をしない
    • next_dp[p][q][diff] += dp[p][q][diff]
  2. 高橋君が買い物をする
    • 金額 \(v \in [L_i, R_i]\) を選ぶ。
    • next_dp[p+1][q][diff + v] += dp[p][q][diff]
  3. 青木君が買い物をする
    • 金額 \(v \in [L_i, R_i]\) を選ぶ。
    • next_dp[p][q+1][diff - v] += dp[p][q][diff]

累積和による高速化

「金額 \(v \in [L_i, R_i]\) を選ぶ」という遷移を愚直にループで回すと計算量が増えますが、dp[p][q] の diff に関する累積和を事前に計算しておくことで、範囲 \([L_i, R_i]\) の加算を \(O(1)\) で行うことができます。

例えば、高橋君が買い物をした後の next_dp[p+1][q][s] は、直前の dp[p][q] において差が s - R[i] から s - L[i] の範囲にあるものの総和となります。

計算量

  • 時間計算量: \(O(M \cdot N \cdot P \cdot Q \cdot S)\)
    • ここで \(S\) は合計金額の差の範囲(約 \(120\))です。
    • \(N \times M \le 2000\) より、全体で約 \(4 \times 10^6\) 回程度の操作となり、高速です。
  • 空間計算量: \(O(P \cdot Q \cdot S)\)
    • DP テーブルを使い回す(インライン化する)ことで、店舗数 \(N\) に依存しないメモリ量で実装可能です。

実装のポイント

  • 負のインデックス: diff が負にならないよう OFFSET = 60 を足して管理します。

  • 累積和の活用: 範囲内の金額をすべて試す代わりに累積和を使うことで、内側のループを高速化しています。

  • 初期化: 各クエリの開始時に DP テーブルを 0 で初期化し、dp[0][0][OFFSET] = 1(誰も何も買っていない状態)からスタートします。

    ソースコード

#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

using namespace std;

/**
 * Problem: Equal Shopping
 * We have N shops, each with a range [Li, Ri].
 * Takahashi shops at exactly P shops, Aoki at exactly Q shops.
 * No same shop for both.
 * Total Takahashi sum = Total Aoki sum.
 * We need to count the number of such plans after each of M updates.
 * 
 * Constraints:
 * N <= 2000, P <= 3, Q <= 3, M <= 1000, N * M <= 2000.
 * Li, Ri <= 20.
 * 
 * DP state: dp[p][q][s]
 * p: number of shops Takahashi has used (0 to P)
 * q: number of shops Aoki has used (0 to Q)
 * s: current sum difference (Takahashi - Aoki), offset by 60 to handle negative values.
 * Max sum for each is 3 * 20 = 60. Max difference is 60.
 * The difference s ranges from -60 to 60, offset to 0 to 120.
 */

int main() {
    // Fast I/O
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);

    int N, P, Q, M;
    if (!(cin >> N >> P >> Q >> M)) return 0;

    // Current ranges for each shop
    vector<int> L(N + 1), R(N + 1);
    for (int i = 1; i <= N; ++i) {
        cin >> L[i] >> R[i];
    }

    const int MOD = 998244353;
    const int OFFSET = 60;
    const int MAX_S = 120;

    // Static DP tables to avoid re-allocation and keep memory usage low
    static int dp[4][4][121];
    static int next_dp[4][4][121];
    static int S[122];

    // Process M updates
    for (int j = 0; j < M; ++j) {
        int X, A, B;
        cin >> X >> A >> B;
        L[X] = A;
        R[X] = B;

        // Reset the DP table for the current configuration of shops
        memset(dp, 0, sizeof(dp));
        dp[0][0][OFFSET] = 1;

        // Dynamic Programming over all shops
        for (int i = 1; i <= N; ++i) {
            // next_dp[p][q][s] starts as dp[p][q][s] (case where no one shops at shop i)
            memcpy(next_dp, dp, sizeof(dp));

            for (int p = 0; p <= P; ++p) {
                for (int q = 0; q <= Q; ++q) {
                    // Optimization: Skip states that have no possible plans
                    bool all_zero = true;
                    for (int s = 0; s <= MAX_S; ++s) {
                        if (dp[p][q][s] != 0) {
                            all_zero = false;
                            break;
                        }
                    }
                    if (all_zero) continue;

                    // Compute prefix sums of dp[p][q][s] for range queries on sum difference
                    S[0] = 0;
                    for (int s = 0; s <= MAX_S; ++s) {
                        S[s + 1] = (S[s] + dp[p][q][s]) % MOD;
                    }

                    // Case: Takahashi shops at shop i
                    if (p + 1 <= P) {
                        for (int s = 0; s <= MAX_S; ++s) {
                            // s_new = s_old + v  =>  s_old = s_new - v
                            // v in [L[i], R[i]]  =>  s_old in [s - R[i], s - L[i]]
                            int low = s - R[i];
                            int high = s - L[i];
                            if (high < 0) continue;
                            if (low < 0) low = 0;
                            if (low <= high) {
                                int ways = (S[high + 1] - S[low] + MOD) % MOD;
                                next_dp[p + 1][q][s] = (next_dp[p + 1][q][s] + ways) % MOD;
                            }
                        }
                    }

                    // Case: Aoki shops at shop i
                    if (q + 1 <= Q) {
                        for (int s = 0; s <= MAX_S; ++s) {
                            // s_new = s_old - v  =>  s_old = s_new + v
                            // v in [L[i], R[i]]  =>  s_old in [s + L[i], s + R[i]]
                            int low = s + L[i];
                            int high = s + R[i];
                            if (low > MAX_S) continue;
                            if (high > MAX_S) high = MAX_S;
                            if (low <= high) {
                                int ways = (S[high + 1] - S[low] + MOD) % MOD;
                                next_dp[p][q + 1][s] = (next_dp[p][q + 1][s] + ways) % MOD;
                            }
                        }
                    }
                }
            }
            // Move updated DP state to current DP
            memcpy(dp, next_dp, sizeof(dp));
        }
        // Result is the number of plans where Takahashi used P shops, Aoki used Q shops, and sums were equal (diff=0).
        cout << dp[P][Q][OFFSET] << "\n";
    }

    return 0;
}

この解説は gemini-3-flash-thinking によって生成されました。

posted:
last update: