C - お菓子の詰め合わせ / Assortment of Sweets Editorial by admin
GLM 5.2 (High)概要
\(N\) 個のお菓子の中から1個以上を選び、選んだお菓子のカロリーの総和が \(K\) の倍数になるような選び方の総数を \(998244353\) で割った余りで求める問題です。
考察
カロリーの総和そのものを管理しようとすると、値が非常に大きくなってしまいます。しかし、「総和が \(K\) の倍数になる」という条件は、「総和を \(K\) で割った余りが \(0\) になる」と言い換えることができます。したがって、各お菓子のカロリーを \(K\) で割った余りだけを考えればよいことがわかります。
お菓子の選び方を全探索(ビット全探索)しようとすると、選び方の総数が \(2^N\) 通りあり、制約の \(N \le 2 \times 10^5\) では計算が終わりません。
そこで動的計画法(DP)を考えます。「これまでに見たお菓子の部分集合で、総和を \(K\) で割った余りが \(r\) になるものの数」を管理することで、効率的に計算できます。状態の数は \(K\) 通りであり、お菓子1個につき \(K\) 通りの遷移を考えるため、全体の計算量は \(O(NK)\) となります。制約の \(N \times K \le 2 \times 10^7\) から、この計算量で十分間に合うことがわかります。
アルゴリズム
動的計画法(DP)を用いて解きます。
- 各お菓子のカロリー \(A_i\) を \(K\) で割った余りに変換しておきます。
- 長さ \(K\) の1次元配列
dpを用意します。dp[r]は「現時点で選んだお菓子の総和を \(K\) で割った余りが \(r\) になる選び方の数」を表します。 - 初期状態として、何も選んでいない状態は総和 \(0\) なので
dp[0] = 1とします。 - お菓子を1つずつ見ていきます。お菓子のカロリーを \(K\) で割った余りを \(a\) とします。
- \(a = 0\) の場合: そのお菓子を選んでも選ばなくても余りは変わりません。よって、
dpのすべての要素について、「選ぶ」と「選ばない」の2通りが存在するため、各要素を2倍します。 - \(a \neq 0\) の場合: 新しい配列
nxtを用意し、現在のお菓子を選ぶ場合と選ばない場合の遷移を計算します。j番目の余りから遷移するとき、お菓子を選ぶと余りは(j + a) % Kになります。したがって、nxtの(j + a) % K番目の要素には、「選ばない場合の元の値dp[(j + a) % K]」と「選ぶ場合の値dp[j]」の和を足します。
- \(a = 0\) の場合: そのお菓子を選んでも選ばなくても余りは変わりません。よって、
- すべてのお菓子を処理し終わった後、
dp[0]が「総和が \(K\) で割り切れる選び方の数」になります。ただし、これは「1個以上選ぶ」という条件を満たすものに加えて、「何も選ばない(空集合)」の1通りを含んでしまっているため、最終的な答えは(dp[0] - 1) % MODとします。
計算量
- 時間計算量: \(O(NK)\)
- 空間計算量: \(O(K)\)
実装のポイント
余りが \(0\) のお菓子が連続する場合、毎回長さ \(K\) のループを回すのは無駄になるため、\(a = 0\) のときは配列の各要素を2倍するという特別処理を行うことで計算を高速化しています。
a != 0の場合の遷移では、nxt[j] = dp[j] + dp[(j - a + K) % K]とする代わりに、コード内ではnxt[(j + a) % K] = dp[(j + a) % K] + dp[j]という形で更新しています。これにより、jを \(0\) から \(K-1\) まで走査するだけで、すべての要素に「選ばない場合」と「選ぶ場合」の両方の遷移を一度に足すことができます。空集合を除外するための引き算
dp[0] - 1の結果が負になるのを防ぐため、最後に(dp[0] - 1) % MODとすることを忘れないようにします。ソースコード
import sys
def solve():
input = sys.stdin.readline
N, K = map(int, input().split())
MOD = 998244353
A = [int(x) % K for x in input().split()]
dp = [0] * K
dp[0] = 1
for a in A:
if a == 0:
dp = [(x * 2) % MOD for x in dp]
else:
nxt = [0] * K
lim = K - a
for j in range(lim):
nxt[j + a] = (dp[j + a] + dp[j]) % MOD
for j in range(lim, K):
nxt[j + a - K] = (dp[j + a - K] + dp[j]) % MOD
dp = nxt
print((dp[0] - 1) % MOD)
solve()
この解説は or-glm5.2-high によって生成されました。