E - 材料を使ってロープを作る / Making Rope from Materials Editorial by admin
Qwen3-Coder-480B概要
\(N\) 種類の紐があり、それぞれ長さ \(L_i\) と在庫数 \(C_i\) が与えられます。これらの紐を何本か選び、長さの合計をちょうど \(W\) にしたいです。使用する紐の本数を最小にする問題です。
考察
この問題は典型的な「制限付き部分和問題」であり、ナップサック問題の一種です。目標は「ちょうど \(W\) の長さ」を作ることで、かつ「使用する紐の本数を最小化」することです。
素朴なDPでの問題点
最も単純な方法は、以下のようなDPを考えることです。
dp[i][w]:\(i\) 番目までの紐を使って長さ \(w\) を作るのに必要な最小の紐の本数
しかし、この方法では状態数が \(O(N \cdot W)\) となり、制約 \(N \leq 100, W \leq 50000\) のもとでは最大で \(5 \times 10^6\) の状態があり、さらに遷移で在庫数 \(C_i\) をループさせると計算量が爆発します。
多重ナップサックの高速化(バイナリテクニック)
そこで、多重ナップサックの高速化として知られる「バイナリ法(カウンティングテクニック)」を使います。
これは、あるアイテムを \(c\) 個使えるときに、以下のように分解します:
\[ c = 1 + 2 + 4 + \cdots + 2^{k-1} + r \]
ここで、\(1, 2, 4, ..., 2^{k-1}\) は2進数的な個数、\(r\) は残りです。
これにより、\(c\) 回ループする代わりに、\(\log c\) 回の処理で済みます。
例えば、ある長さ5の紐が13本ある場合、
\[ 13 = 1 + 2 + 4 + 6 \]
と分解して、長さ5×1、5×2、5×4、5×6 の「合成されたアイテム」として扱います。
これによって、効率的に遷移を行うことができます。
アルゴリズム
この問題は、多重ナップサック問題を本数最小化にして解く問題です。
DP定義
dp[w]:長さ \(w\) を作るのに必要な最小の紐の本数
初期値:dp[0] = 0, それ以外は \(\infty\)
更新方法
各紐 \(i\) に対して、在庫数 \(C_i\) をバイナリ法で分解して、合成アイテムごとにDPを更新します。
具体的には:
- add_length = L[i] * k としたとき、
- dp[j] = min(dp[j], dp[j - add_length] + k) (ただし j >= add_length)
この更新を全ての合成アイテムに対して行い、最終的に dp[W] が答えです。
計算量
- 時間計算量: \(O(N \cdot W \cdot \log(\max(C_i)))\)
- 各アイテムに対して、在庫数を \(\log C_i\) 個の合成アイテムに分解し、それぞれについてDPテーブルを逆順に更新する
- 空間計算量: \(O(W)\)
- DPテーブルはサイズ \(W+1\) の1次元配列
実装のポイント
DPテーブルの更新は逆順に行うこと(同じアイテムを複数回使わないようにするため)
在庫数のバイナリ分解は
k=1から始めて、count -= k; k *= 2で進める最終的に
dp[W]が未更新(\(\infty\))なら-1を出力ソースコード
import sys
import heapq
from collections import deque
def main():
import sys
input = sys.stdin.read
data = input().split()
idx = 0
N = int(data[idx])
idx += 1
W = int(data[idx])
idx += 1
L = []
C = []
for _ in range(N):
l = int(data[idx])
idx += 1
c = int(data[idx])
idx += 1
L.append(l)
C.append(c)
# dp[i] := ロープの長さが i になるのに必要な最小の紐の本数
# 初期値は無限大、ただし dp[0] = 0
INF = float('inf')
dp = [INF] * (W + 1)
dp[0] = 0
# 各アイテムについて処理
for i in range(N):
length = L[i]
count = C[i]
# カウンティングテクニックによる多重ナップサックの処理
# count を 1, 2, 4, ... の和に分解して処理
k = 1
while k < count:
# 長さ length*k のアイテムを1つ使うと考える
add_length = length * k
# 逆順に更新することで同じアイテムを複数回使わないようにする
for j in range(W, add_length - 1, -1):
if dp[j - add_length] != INF:
dp[j] = min(dp[j], dp[j - add_length] + k)
count -= k
k *= 2
# 残りのcount分を処理
add_length = length * count
for j in range(W, add_length - 1, -1):
if dp[j - add_length] != INF:
dp[j] = min(dp[j], dp[j - add_length] + count)
result = dp[W]
if result == INF:
print(-1)
else:
print(result)
if __name__ == "__main__":
main()
この解説は qwen3-coder-480b によって生成されました。
posted:
last update: