読者です 読者をやめる 読者になる 読者になる

pekempeyのブログ

競技プログラミングに関する話題を書いていきます。

yukicoder No.317 辺の追加

問題文

http://yukicoder.me/problems/896

解法

連結成分の個数をkとしたとき、この問題は次の問題に置き換えられることがすぐに分かる。

  • k枚の硬貨があり、それぞれの価値はa[i]円である。このときS円を支払うために必要な最小の硬貨の枚数を求めよ。

これはDPで解けるよね。

  • dp[i][j]:=i番目のコインまでを使ってj円を支払う最小の枚数
  • dp[i+1][j]=min(dp[i][j], dp[i][j-a[i]])

でも、これだとO(kn)だから間に合わない。

実はこの問題、硬貨の種類数がO(√n)で抑えられる。

最も種類が多くなるのは1,2,3,...という額になっている場合で、その総和がnになることから種類数はO(√n)で抑えられることが分かる。

この性質を使うために硬貨の価値を多重集合で管理するように変更し、次の問題に置き換えよう。

a[i]円の硬貨がm[i]枚ある。このときS円を支払うために必要な最小の硬貨の枚数を求めよ。

DPはこんな感じ。

dp[i][j]:=i種類目のコインまでを使ってj円を支払う最小の枚数
dp[i+1][j]=min(dp[i][j],dp[i][j-a[i]]+1,dp[i][j-2a[i]]+2,...,dp[i][j-m[i]a[i]]+m[i])

minを愚直に計算してたら計算量の改善にならないから、minの計算を効率化する必要がある。

j番目のminを見る範囲と、j+1番目のminを見る範囲がほとんど変わらないことに着目すると計算量が落とせる。 今見ている範囲の値をsetに入れておけばいい。

f:id:pekempey:20151211135849p:plain
図1. 1円硬貨が3枚あるときの見る範囲

全体の計算量はO(n√n log n)でヤバそうだけど、書いてみたら1546msで通った。


他の人のコードを見て気づいたんだけど、蟻本の個数制約付きナップサックのあたりに高速化のヒントが書いてある(第2版pp.300-303)。 やり方は

  • 「スライド最小値」のようにdequeを使って最新k個の要素の最小値を管理する方法
  • 個数をm[i]=1+2+22+...+2k+aと分解する方法

の2通りがあるらしい。 dequeの方法では277ms、2の冪に分解する方法では91msで通った。

スライド最小値で書いたコード

スライド最小値をハードコーディングするとバグらせそうだったので、適当な構造体に変えてある。

yukicoder No.317 辺の追加 スライド最小値

2の冪に分解する方法で書いたコード

こっちの方が分量が少ない。

yukicoder No.317 辺の追加 2の冪

ひとこと

スライド最小値も2の冪に分解する方法もどっちも汎用性高そう。