連載
» 2010年03月06日 00時00分 UPDATE

最強最速アルゴリズマー養成講座:アルゴリズマーの登竜門、「動的計画法・メモ化再帰」はこんなに簡単だった (5/5)

[高橋直大,ITmedia]
前のページへ 1|2|3|4|5       

連想配列のわな

 プログラミングに慣れている読者であれば、JavaのHashMapやC#のDictionary、C++のmapといった連想配列などを利用して解けばよいのではないか、と考えた人も多いかと思います。

 データ構造に関してはまだこの連載で取り上げていませんので、ここでは詳しく説明しませんが、適当な連想配列dicに対して、dic[i] = j;とすれば、dic[i]の中にjが入っていて、iの領域を気にすることがない便利なもの、程度にとらえていただければ構いません(ただし、今すぐ使用したい場合は、検索などで調べてからの使用を推奨します)。

 C#の連想配列であるDictionaryを用いて1つ前のソースコードを書き換えると、以下のようになります。


using System;
using System.Collections;
using System.Collections.Generic;
public class InfiniteSequence2 {
    Dictionary dic;
    public long calc(long n, int p, int q, int x, int y)
    {
        dic = new Dictionary();
        return saiki(n, p, q, x, y);
    }
    long saiki(long n, int p, int q, int x, int y)
    {
        if (n <= 0) return 1;
        if (dic.ContainsKey(n)) return dic[n];
        return (dic[n] = saiki(n / p - x, p, q, x, y) + saiki(n / q - y, p, q, x, y));
    }
}

連想配列を利用したメモ化再帰

 ほんの少しの書き換えで連想配列を利用したメモ化再帰に変更できました。こうすることで、一度も訪れない数字に対してメモリを割く必要もなくなり、メモリの使用量を大幅に減らせます。このような方法は、記憶範囲が飛び飛びになった場合によく使われる方法なので、覚えておくと戦術の幅が広がります。

 さて、確かにこの方法を利用すれば、メモリの使用量を直感的にもかなり減らすことが可能です。では、メモリの使用量はいったいどれくらいになるのでしょうか? ほとんどの方はここまで計算しないと思いますし、計算も難しいのですが、O(√n)程度までは減らせます。ただし、連想配列では通常より多くメモリを確保してしまうことを考慮すると、O(√n)でもメモリ使用量を少しオーバーしてしまう可能性があります。

 こういった場合は、実際に組んでみて、本当に大丈夫であるかをテストしてみるのがよいでしょう。メモリ使用量を最大とするには、小さい数字が大量に発生をすることを考えれば、大きい数字をばらつかせてたくさん発生させることが重要なので、

  • p、qは、大きい数字への割り算の影響が大きいので、両方最小の2とする
  • x、yは、大きい数字への引き算となるため、最初の影響は小さい。ここはばらつかせるために差をつける

といったことを考慮し、例えば、n=10^13、p=2、q=2、x=0、y=123456などのテストケースを考え、TopCoder上で実行させてみると、メモリ使用量がオーバーしてしまうことが確認できます。つまり、TopCoderではこの方法でも、残念ながら不正解というわけです。

初心者でも書ける意外な解決策

 さて、それではどうすればよいのでしょうか。動的計画法やメモ化再帰で、なぜメモリ上に記憶をさせるかというと、「同じところを何度も訪れることで無駄が生じるから」です。それなら、通った状態をすべて記憶できないなら、よく訪れるものだけ記憶すればよいのです。分かってしまえば非常に簡単ですね。

 この問題では、図を見れば分かるとおり、大きな数字より小さな数字が明らかに大量に発生します。それならば、大きな数字のときは普通に探索をし、ある程度小さい数字になったところでメモ化再帰に切り替えればよいでしょう。

 小さい数字を適当に10^6と設定し、ソースコードを書くと以下のようになります。


using System;
using System.Collections;
using System.Collections.Generic;
public class InfiniteSequence2 {
    long[] dp;
    public long calc(long n, int p, int q, int x, int y)
    {
        dp = new long[1000001];
        return saiki(n, p, q, x, y);
    }
    long saiki(long n, int p, int q, int x, int y)
    {
        if (n <= 0) return 1;
        if (n <= 1000000 && dp[n] != 0) return dp[n];
        long res = saiki(n / p - x, p, q, x, y) + saiki(n / q - y, p, q, x, y);
        if (n <= 1000000) dp[n] = res;
        return res;
    }
}

 さて、本当にこの方法で正しいのでしょうか?

 メモリ使用量は、long型の配列を10^6個なので、8バイト*10^6 = 約8Mバイトとなり、条件を満たします。

 一方、実行時間についても、10^6未満とそれ以上に分けて考えると、問題がないことが分かります。10^6未満に関しては、すべて求めてもO(10^6)となり、0.1秒も掛からず終わります。1000000(10^6)より大きな数字に関しては、nから1000000までに変化するまでに発生する頂点の個数を考えれば分かります。この式において、p、qは2以上、x、yは0以上であることを考えれば、子の頂点の数字の和が、親の頂点の和を超えることは有り得ません。よって、発生する頂点の個数はO(n/10^6)=O(10^7)となり、こちらも2秒の実行時間の制限に引っかかることはありません。よって、このコードでどんなケースにおいても対応できることが分かります。

 なお、再帰専用の関数を削除し、1関数にすると、以下のようにさらにシンプルなものになります。このコードをチャレンジ用のソースにそのまま流用すると、恐らく不正解が返されると思いますが、これは初期化の問題です。TopCoderでは、各々のテストケースを実行する度に初期化を行っていますが、そうではない場合、配列に前の計算結果が残ってしまいますので、注意してください。


using System;
using System.Collections;
using System.Collections.Generic;
public class InfiniteSequence2 {
    long[] dp = new long[1000001];
    public long calc(long n, int p, int q, int x, int y)
    {
        if (n <= 0) return 1;
        if (n <= 1000000 && dp[n] != 0) return dp[n];
        long res = calc(n / p - x, p, q, x, y) + calc(n / q - y, p, q, x, y);
        if (n <= 1000000) dp[n] = res;
        return res;
    }
}

 最終的な回答となったソースコードは非常に短いですが、この回答にたどり着くのはなかなか難しいと思います。使用できるメモリと実行時間を意識して適切なソースコードを書くというのは、TopCoderにおいてはほかの人との差となって現れてきますし、実務においても重要なポイントです。

 今回のポイントは、「メモ化再帰・動的計画法は同じ計算をする無駄を省く」ということに尽きます。それをしっかりと意識できるようになれば、今回のような問題にも立ち向かえるようになるでしょう。

 今回紹介したものよりも美しい実装方針を思いついたり、きれいなコードが書けたら、ぜひご意見ください。ソーシャルブックマークのコメントでも、筆者のブログに書き込んでいただいても構いません。可能な限り反応したいと思っていますので、どんどんご意見をお寄せください。それでは、次回もお楽しみに!

著者プロフィール:高橋直大(たかはし なおひろ)

tnapro.jpg

Microsoftが主催する技術コンテスト「Imagine Cup」の2008年アルゴリズム部門で世界第3位に入賞した若きアルゴリズマー。TopCoderでは誰もが恐れるRedcoderとして活躍中。2009年に開催された「天下一プログラマーコンテスト」では特別賞を受賞した。現在、慶應義塾大学環境情報学部2年。口癖は「みょんみょん」。


世界でも有数のRedcoder、高橋直大がアルゴリズムの魅力と極意を伝授する「最強最速アルゴリズマー養成講座」はこちらから


前のページへ 1|2|3|4|5       

Copyright© 2012 ITmedia, Inc. All Rights Reserved.

オンラインムック Special

- PR -

Special

- PR -

Special

- PR -

新着記事

節電お役立ち情報(スマートジャパン)

news079.jpg

住宅で消費する電力量のデータを蓄積、分析し、グラフなどの形でユーザーに見せるHEMS(...

news061.jpg

ファミリーマートが最新の節電技術を取り入れたコンビニエンスストアを6月に北九州市でオ...

news046.jpg

再生可能エネルギーの活用や節電対策によってエネルギーの消費量を実質的にゼロにする「...