D - 均等な買い物 / Equal Shopping 解説 by admin
gpt-5.5-xhigh概要
各お店について「買わない」「高橋君が買う」「青木君が買う」のどれかを選び、高橋君が \(P\) 軒、青木君が \(Q\) 軒で買い物し、合計金額が等しくなる計画数を求めます。
更新後ごとに、お店を先頭から順に見る動的計画法で答えを再計算します。
考察
均等な買い物計画では、重要なのは次の \(3\) つです。
- 高橋君が買ったお店の数
- 青木君が買ったお店の数
- 高橋君の合計金額と青木君の合計金額の差
そこで、
\[ \text{差} = \text{高橋君の合計金額} - \text{青木君の合計金額} \]
として管理します。
最終的に求めたいのは、
- 高橋君が買ったお店数が \(P\)
- 青木君が買ったお店数が \(Q\)
- 差が \(0\)
となる場合の数です。
素朴に全ての買い物計画を列挙すると、各お店について複数の選択肢があるため非常に多くなり、間に合いません。
一方で、この問題では以下の制約が小さいです。
- \(P \leq 3\)
- \(Q \leq 3\)
- 購入金額は最大 \(20\)
したがって、それぞれの合計金額は最大でも
\[ 3 \times 20 = 60 \]
です。
よって、差は \(-60\) 以上 \(60\) 以下だけを考えれば十分です。
また、更新があるため高速な更新処理が必要に見えますが、制約に
\[ N \times M \leq 2000 \]
があります。
そのため、各更新後に最初から DP をやり直しても十分間に合います。
アルゴリズム
DP を次のように定義します。
\[ dp[p][q][d] \]
を、現在まで見たお店について、
- 高橋君が \(p\) 軒で買い物した
- 青木君が \(q\) 軒で買い物した
- 金額差が \(d - \text{OFF}\)
であるような計画数とします。
ここで、差は \(-60\) から \(60\) までなので、配列の添字として扱いやすくするために
\[ \text{OFF} = 60 \]
を足して管理します。
つまり、差 \(0\) は添字 \(60\) に対応します。
初期状態では、まだどのお店も見ておらず、誰も買い物していないので、
\[ dp[0][0][\text{OFF}] = 1 \]
です。
各お店 \(i\) について、範囲を \([L_i, R_i]\) とします。
現在の状態 \(dp[p][q][d]\) から、次の \(3\) 種類の遷移を行います。
1. 誰も買い物しない
\[ ndp[p][q][d] += dp[p][q][d] \]
2. 高橋君が金額 \(a\) で買い物する
\(p < P\) のとき、各 \(a \in [L_i, R_i]\) について、
\[ ndp[p+1][q][d+a] += dp[p][q][d] \]
高橋君の合計金額が増えるので、差も \(a\) 増えます。
3. 青木君が金額 \(a\) で買い物する
\(q < Q\) のとき、各 \(a \in [L_i, R_i]\) について、
\[ ndp[p][q+1][d-a] += dp[p][q][d] \]
青木君の合計金額が増えるので、差は \(a\) 減ります。
全てのお店を処理したあと、答えは
\[ dp[P][Q][\text{OFF}] \]
です。
これは、高橋君が \(P\) 軒、青木君が \(Q\) 軒で買い物し、差が \(0\)、つまり合計金額が等しい計画数を表します。
各更新では、指定されたお店の区間 \([L_X, R_X]\) を新しい \([A, B]\) に置き換えたあと、この DP を最初から計算し直します。
計算量
購入金額の候補数を \(V = 20\)、差の状態数を
\[ D = 121 \]
とします。
- 時間計算量: \(O(M \times N \times (P+1)(Q+1) \times D \times V)\)
- 空間計算量: \(O((P+1)(Q+1) \times D)\)
\(P,Q,V,D\) はすべて小さい定数であり、さらに \(N \times M \leq 2000\) なので十分高速です。
実装のポイント
差は負になる可能性があるため、添字として使うために
OFF = 60を足して管理します。- 差 \(0\) は
OFF - 差 \(+a\) は
OFF + a - 差 \(-a\) は
OFF - a
- 差 \(0\) は
各お店について
ndpを新しく作り、そこへ遷移します。- これにより、同じお店で高橋君と青木君が同時に買うような遷移を防げます。
答えは非常に大きくなる可能性があるため、常に \(998244353\) で割った余りを取ります。
更新は累積的なので、配列
L,Rを直接書き換えてから DP を実行します。ソースコード
#include <bits/stdc++.h>
using namespace std;
static const int MOD = 998244353;
static const int OFF = 60;
static const int W = OFF * 2 + 1;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, P, Q, M;
cin >> N >> P >> Q >> M;
vector<int> L(N), R(N);
for (int i = 0; i < N; i++) cin >> L[i] >> R[i];
auto solve = [&]() -> int {
int dp[4][4][W] = {};
int ndp[4][4][W] = {};
dp[0][0][OFF] = 1;
for (int i = 0; i < N; i++) {
memset(ndp, 0, sizeof(ndp));
for (int p = 0; p <= P; p++) {
for (int q = 0; q <= Q; q++) {
for (int d = 0; d < W; d++) {
int val = dp[p][q][d];
if (!val) continue;
int &none = ndp[p][q][d];
none += val;
if (none >= MOD) none -= MOD;
if (p < P) {
for (int a = L[i]; a <= R[i]; a++) {
if (d + a < W) {
int &to = ndp[p + 1][q][d + a];
to += val;
if (to >= MOD) to -= MOD;
}
}
}
if (q < Q) {
for (int a = L[i]; a <= R[i]; a++) {
if (d - a >= 0) {
int &to = ndp[p][q + 1][d - a];
to += val;
if (to >= MOD) to -= MOD;
}
}
}
}
}
}
memcpy(dp, ndp, sizeof(dp));
}
return dp[P][Q][OFF];
};
for (int j = 0; j < M; j++) {
int X, A, B;
cin >> X >> A >> B;
--X;
L[X] = A;
R[X] = B;
cout << solve() << '\n';
}
return 0;
}
この解説は gpt-5.5-xhigh によって生成されました。
投稿日時:
最終更新: