計算量を落とすテクニック

概要

特定の区間上の複数回の操作における計算量を落とすテクニックについて考えます。累積和に近い考え方です。

問題設定

\{9, 1, 3, 8, 4, 2, 7, 6, 5\}という9つの要素を含む数列 \{a_n\}がある。数列の添字は 0から始まるものとする。
ここでは a_{0}の次の要素は a_{n-1} に戻るような循環する数列を考える。
それぞれの要素 a_i に対して、 a_{i-k} (0 \leq k \leq n-1) の最小の要素が何か求める。
ここでは要素 a_i に対して、  k 回操作したときの最小の要素を f(a_i, k)ということとする。

例えば、要素 8k=6 の場合は、\{8, 3, 1, 9, 5, 6, 7\} のうちの最小の要素は 1 であるから
答え f(a_3, 6)=1 である。(要素 8 は元の数列上で a_3 )

ナイーブな方法

各要素 a_i ごとに、それぞれ k 回ループして f(a_i, k) を求める。この場合の計算量は O(n^3) 程度である。

import java.util.Arrays;


public class Sample {

    static int INF = 1 << 30;
    public static void main(String[] args) {

        int[] a = {9, 1, 3, 8, 4, 2, 7, 6, 5};
        int n = a.length;
        int[][] f = new int[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(f[i], INF);
        }

        for (int i = 0; i < n; i++) {
            int min = a[i];
            for (int k = 0; k < n; k++) {
                for (int j = 0; j < k; j++) {
                    min = Math.min(min, a[(i-j+n-1)%n]);
                }
                f[i][k] = min;
            }
        }

    }

}

効率的な方法

 k0 から計算した場合、 k 回目 (k>0) の操作のときはすでに k-1 回のときまでの最小値は求められている。
例えば、先の例の場合、要素 8k=6 の場合は、 \{8, 3, 1, 9, 5, 6, 7\} のうちで最小の要素を求めるときは
要素 8k=5 の場合 \{8, 3, 1, 9, 5, 6\} の最小値は求められている。 f(a_3, 5)=1 である。
このとき、k=6 の場合の最小の要素は f(a_3, 6) = min\{f(a_3, 5), a_8\} で求められる。
この場合の計算量は O(n^2) である。

import java.util.Arrays;


public class Sample {

    static int INF = 1 << 30;
    public static void main(String[] args) {

        int[] a = {9, 1, 3, 8, 4, 2, 7, 6, 5};
        int n = a.length;
        int[][] f = new int[n][n];
        for (int i = 0; i < n; i++) {
            Arrays.fill(f[i], INF);
        }

        for (int i = 0; i < n; i++) {
            for (int k = 0; k < n; k++) {
                if (k == 0) {
                    f[i][k] = a[i];
                } else {
                    f[i][k] = Math.min(f[i][k-1], a[(i-k+n)%n]);
                }
            }
        }

    }

}