C++で自動メモ化(競プロ用)
DP問題などを解く際、私はとりあえず再帰関数で書いてみて大丈夫そうならメ モ化再帰に書き換える、という手順を踏むことが多いのですが、この書き換え が若干面倒なので、手間を最小限に抑える方法を考えてみました。
再帰関数はlambdaで書くものとします。lambdaで再帰する方法については C++のラムダで再帰する - koturnの日記 を参照。
再帰関数をメモ化する場合、中身はだいたいこんな感じになるはずです:
int rec(int i, int j) { static bool done[N][M] {}; static int memo[N][M]; if(!done[i][j]) { int res; // res を計算 memo[i][j] = res; done[i][j] = true; } return memo[i][j]; }
このコードを自動生成することを考えます。
まず、done
, memo
は多次元配列なので、std::array
を用いて多次元配
列型を実装します:
template<typename T, size_t N, size_t... NS> struct ArrayType { using type = array<typename ArrayType<T,NS...>::type,N>; }; template<typename T, size_t N> struct ArrayType<T,N> { using type = array<T,N>; }; template<typename T, size_t... NS> using Array = typename ArrayType<T,NS...>::type; template<typename T, size_t N> T& array_at(Array<T,N>& ary, int i) { return ary[i]; } template<typename T, size_t N, size_t... NS, typename... Args> T& array_at(Array<T,N,NS...>& ary, int i, Args... args) { return array_at<T,NS...>(ary[i], args...); } template<typename T, size_t N> const T& array_at(const Array<T,N>& ary, int i) { return ary[i]; } template<typename T, size_t N, size_t... NS, typename... Args> const T& array_at(const Array<T,N,NS...>& ary, int i, Args... args) { return array_at<T,NS...>(ary[i], args...); }
多次元配列型があれば、以下のようなテンプレートを用いて先ほどのコードを 自動生成できるはずです:
template<typename F, size_t... NS> class FixPointMemo { public: explicit FixPointMemo(F&& f) : f_(forward<F>(f)) {} template<typename... Args> decltype(auto) operator()(Args... args) const { using R = decltype(f_(*this,args...)); static Array<bool,NS...> done {}; static Array<R,NS...> memo; if(!array_at<bool,NS...>(done,args...)) { array_at<R,NS...>(memo,args...) = f_(*this,args...); array_at<bool,NS...>(done,args...) = true; } return array_at<R,NS...>(memo,args...); } private: F f_; }; template<size_t... NS, typename F> decltype(auto) FIXMEMO(F&& f) { return FixPointMemo<F,NS...>(forward<F>(f)); }
以上のコードを用意しておけば、このような再帰関数があるとき:
auto f = FIX([&](auto&& self, int i, int j) -> int { /* ... */ });
以下のようにテンプレート引数を追加するだけでメモ化が完了します:
// i,j は [0,1000) の値をとるものとする auto f = FIXMEMO<1000,1000>([&](auto&& self, int i, int j) -> int { /* ... */ });
関数本体は一切変更しなくてよいためかなり楽だと思います。
難点を挙げると:
- メモ化完了フラグを
bool
配列で持っているためメモリ効率が若干悪い (手で書く場合、問題によってはメモ化配列自体に特殊な値を入れることで 代用できる) - 関数の引数は整数型に限定される
といったところですが、高難易度問題でなければあまり気にしなくていいよう に思います(どうせ工夫を要するDP問題はループで書かざるを得なかったりし ますし)。
この方法を用いてナップサック問題を解いたコードを貼っておきます: 提出 #6436875 - Educational DP Contest / DP まとめコンテスト