📊

いもす法を数式で理解する

2024/01/28に公開

いもす法の解説記事はいろいろありますが、(僕にとってしっくりくる形で)数式を使って表現したものがあまりなかったので、まとめようと思います。

考えたい問題

次のような問題を考えます。

【問題】
長さN の数列a_{1}, ..., a_{N} が与えられる。この数列に対し、次の操作をQ回行う:
k 回目の操作では、a_{l_{k}}, ..., a_{r_{k}}x_{k}を加算する。
操作後のa_{1}, ..., a_{N}を求めよ。
【制約】
1\leq N \leq 10^5
1 \leq Q \leq 10^5
1 \leq l_{k} < r_{k} \leq N \ (1\leq k \leq Q)
1 \leq x_{k} \leq 100 (1 \leq k \leq Q)

以下 0\leq k \leq Qに対して、k回目の操作を行ったあとのa_{i}a^{(k)}_{i}と書くことにします。つまり、

\begin{align} \begin{split} a^{(0)}_{i} &= a_{i} \ (1\leq i \leq N), \\ a^{(k)}_{i} &= \left\{ \begin{array}{ll} a^{(k-1)}_{i}+x_{k} & (l_{k}\leq i \leq r_{k}) \\ a^{(k-1)}_{i} & (1\leq i < l_{k} \text{ or } r_{k}<i\leq N) \end{array} \right. \ (1\leq k \leq Q) \end{split} \end{align}

とします。求めたいものはa^{(Q)}_{1}, ..., a^{(Q)}_{N}です。
N = 7, Q = 3 の場合の数列の変化の例です。太字の部分が更新された項を表します。

k l_{k} r_{k} x_{k} a^{(k)}_{1} a^{(k)}_{2} a^{(k)}_{3} a^{(k)}_{4} a^{(k)}_{5} a^{(k)}_{6} a^{(k)}_{7}
0 - - - 0 0 0 0 0 0 0
1 1 3 4 4 4 4 0 0 0 0
2 2 5 1 4 5 5 1 1 0 0
3 3 7 10 4 5 15 11 11 10 10

愚直な計算量

各操作を愚直に行うと最悪でO(N)の計算量がかかるので、全体での計算量は最悪でO(NQ)です。そのため今回の制約だとO(10^{10})程度の計算量となり、時間がかかります(AtCoderだと1秒当たり10^8回の計算が目安とされる)。

いもす法のアルゴリズム

各操作に対する計算量を落とすことを考えます。
kに対してa^{(k)}_{0}=0, \ b^{(k)}_{i} := a^{(k)}_{i} - a^{(k)}_{i-1} \ (1\leq i \leq N)とします。
すると、(1)式より

b^{(k)}_{i} = \left\{ \begin{array}{ll} b^{(k-1)}_{i}+x & (i = l_{k}) \\ b^{(k-1)}_{i}-x & (i = r_{k}+1) \\ b^{(k-1)}_{i} & (i \ne l_{k}, r_{k}+1) \end{array} \right.

となっています(r_{k} = Nのときは、最後の場合は存在しないことに注意が必要です)。これより、「a^{(k-1)}_{l_{k}}, ..., a^{(k-1)}_{r_{k}}x_{k}を足す」という操作は、

  • b^{(k-1)}_{l_{k}}x_{k}を足し、b^{(k-1)}_{r_{k}+1}-x_{k}を足す」(r_{k}=Nの場合は、「b_{l_{k}}x_{k}を足す」だけでよい)

という操作と同値だと分かります。そしてこの操作はO(1)で行えます。よって、全体として操作にかかる時間が、O(NQ)からO(Q)となりました。

このままだとb^{(Q)}_{1}, ..., b^{(Q)}_{N}を求めただけなので、最後にa^{(Q)}_{1}, ..., a^{(Q)}_{n}の情報を復元する必要があります。今、

\begin{align} \begin{split} a^{(k)}_{1} &= b^{(k)}_{1}, \\ a^{(k)}_{2} &= (a^{(k)}_{2} - a^{(k)}_{1}) + a^{(k)}_{1} = b^{(k)}_{2} + a^{(k)}_{1}, \\ &..., \\ a^{(k)}_{n} &= (a^{(k)}_{n} - a^{(k)}_{n-1}) + a^{(k)}_{n-1} = b^{(k)}_{n} + a^{(k)}_{n-1} \end{split} \end{align}

となるので、(2)式より数列a^{(Q)}_{1}, ..., a^{(Q)}_{N}b^{(Q)}_{1}, ..., b^{(Q)}_{N}の累積和として得られます。従ってa^{(Q)}_{1}, ..., a^{(Q)}_{N}を求めるのにかかる時間はO(N)です。
よって全体ではO(Q+N)で計算できるようになりました。

例の再考

最初に考えた例の場合、a^{(Q)}_{i}b^{(Q)}_{i}は次のように更新されます。太字の部分が更新された項を表します。

k l_{k} r_{k} x_{k} a^{(k)}_{1} a^{(k)}_{2} a^{(k)}_{3} a^{(k)}_{4} a^{(k)}_{5} a^{(k)}_{6} a^{(k)}_{7}
0 - - - 0 0 0 0 0 0 0
1 1 3 4 4 4 4 0 0 0 0
2 2 5 1 4 5 5 1 1 0 0
3 3 7 10 4 5 15 11 11 10 10
k l_{k} r_{k} x_{k} b^{(k)}_{1} b^{(k)}_{2} b^{(k)}_{3} b^{(k)}_{4} b^{(k)}_{5} b^{(k)}_{6} b^{(k)}_{7}
0 - - - 0 0 0 0 0 0 0
1 1 3 4 4 0 0 -4 0 0 0
2 2 5 1 4 1 0 -4 0 -1 0
3 3 7 10 4 1 10 -4 0 -1 0

2次元への拡張

いもす法のいいところは、次元を上げても適用できることです。次のような問題を考えます。
【問題】
H\times W 行列A = a_{i, j} が与えられる。次の操作をQ回行う:k回目の操作では、u_{k}\leq i \leq d_{k}かつl_{k}\leq j \leq r_{k}なる(i, j) に対して、a_{i, j}x_{k}を加算する。
操作後の行列A を求めよ。
【制約】
1\leq HW \leq 10^5
1 \leq Q \leq 10^5
1 \leq l_{i} < r_{i} \leq N \ (1\leq i \leq Q)
1 \leq x_{i} \leq 100 (1 \leq i \leq Q)

この問題も、各操作で愚直にa_{i, j}たちを更新していると最悪計算量がO(QHW)となり時間がかかります。1次元の場合と同様に、階差数列を考えて、各操作をO(1)で行うことを考えます。
この問題を考える上では、操作を一度も行っていない状態のAの各項は全て0としてよいです。0でない項がある状態から始まるときは、各項が0のH\times W 行列A'に対して操作を行い、操作終了後のA'Aに足せば、求めたい行列が求まるからです。

以下、i=0 またはj=0のとき、a_{i, j} = 0とします。k 回目の操作を行った後のa_{i, j}のことをa^{(k)}_{i, j}と置きます。つまり、

\begin{align} \begin{split} a^{(0)}_{i, j} &= a_{i, j} = 0 \ (1\leq i \leq H, \ 1\leq j \leq W), \\ a^{(k)}_{i, j} &= \left\{ \begin{array}{ll} a^{(k-1)}_{i, j} + x_{k} & (u_{k} \leq i \leq d_{k} \text{ and } l_{k} \leq j \leq r_{k}) \\ a^{(k-1)}_{i} & (\text{otherwise}) \end{array} \right. \ (1\leq k \leq Q) \end{split} \end{align}

とします。また、各0\leq k \leq Q, \ 1\leq i \leq H, 1 \leq j \leq Wについて、b^{(k)}_{i, j} := a^{(k)}_{i, j} - a^{(k)}_{i-1, j}, \ c^{(k)}_{i, j} := a^{(k)}_{i, j} - a^{(k)}_{i, j-1}と置きます。 すると(3)より、1\leq k \leq Qに対して

\begin{align} b^{(k)}_{i, j} &= \left\{ \begin{array}{ll} b^{(k-1)}_{i, j}+x_{k} & (i = u_{k} \text{ and } l_{k}\leq j \leq r_{k}) \\ b^{(k-1)}_{i, j}-x_{k} & (i = d_{k} + 1 \text{ and } l_{k}\leq j \leq r_{k}) \\ b^{(k-1)}_{i, j} & (\text{otherwise}) \end{array} \right. , \\ c^{(k)}_{i, j} &= \left\{ \begin{array}{ll} c^{(k-1)}_{i, j}+x_{k} & (u_{k} \leq i \leq d_{k} \text{ and } j = l_{k}) \\ c^{(k-1)}_{i, j}-x_{k} & (u_{k} \leq i \leq d_{k} \text{ and } j = r_{k} + 1) \\ c^{(k-1)}_{i, j} & (\text{otherwise}) \end{array} \right. \end{align}

が成り立ちます。これより、「u_{k}\leq i \leq d_{k}かつl_{k}\leq j \leq r_{k}なる(i, j) に対して、a^{(k-1)}_{i, j}x_{k}を加算する」という操作は、

  • i = u_{k} かつ l_{k}\leq j \leq r_{k} なる(i, j)について、b^{(k-1)}_{i, j}x_{k}を加算する。
  • i = d_{k}+1 かつ l_{k}\leq j \leq r_{k} なる(i, j)について、b^{(k-1)}_{i, j}-x_{k}を加算する。
  • u_{k} \leq i \leq d_{k} かつ j=l_{k} なる(i, j)について、c^{(k-1)}_{i, j}x_{k}を加算する。
  • u_{k} \leq i \leq d_{k} かつ j = r_{k}+1 なる(i, j)について、c^{(k-1)}_{i, j}-x_{k}を加算する。

という4つの操作を行うことと同値です。よって各iに対して\{b^{(k)}_{i, j}\}_{j}にいもす法を適用し、各j に対して\{c^{(k)}_{i, j}\}_{i}にいもす法を適用すれば、全体でO(Q + HW)の計算量でb^{(Q)}_{i, j}, \ c^{(Q)}_{i, j}を求められることになります。
しかし、実際はもう少し簡単にb^{(Q)}_{i, j}, \ c^{(Q)}_{i, j}を求められます。

1\leq i\leq H, \ 1\leq j \leq Wに対して、e^{(0)}_{i, j} = 0とします。また、1\leq k \leq Qに対して

e^{(k)}_{i, j} = \left\{ \begin{array}{ll} e^{(k-1)}_{i, j}+x_{k} & ((i, j) = (u_{k}, l_{k}), \ (d_{k}+1, r_{k}+1)) \\ e^{(k-1)}_{i, j}-x_{k} & ((i, j) = (u_{k}, r_{k}+1), \ (d_{k}+1, l_{k})) \\ e^{(k-1)}_{i, j} & (\text{otherwise}) \end{array} \right.

と置きます。さらに、i=0またはj=0のとき、b_{i, j} = c_{i, j} = 0 とします。このとき、b^{(k)}_{i, j} - b^{(k)}_{i, j-1} = e^{(k)}_{i, j}かつc^{(k)}_{i, j} - c^{(k)}_{i-1, j} = e^{(k)}_{i, j}が、すべての1\leq k \leq Q, \ 1\leq i \leq H, \ 1\leq j \leq Wに対して成り立ちます。

証明

kについての帰納法で示す。
k=0のとき、a^{(0)}_{i,j} = 0 \ (\forall i, j) であるので、b^{(0)}_{i, j} = c^{(0)}_{i, j} = e^{(0)}_{i, j} \ (\forall i, j)となる。k=nb^{(k)}_{i, j} - b^{(k)}_{i, j-1} = e^{(k)}_{i, j}かつc^{(k)}_{i, j} - c^{(k)}_{i-1, j} = e^{(k)}_{i, j}が成り立つとする。
(4)より、

b^{(n+1)}_{i, j} - b^{(n+1)}_{i, j-1} = \left\{ \begin{array}{ll} b^{(n)}_{i, j} - b^{(n)}_{i, j-1}+x_{n+1} & ((i, j) = (u_{n+1}, l_{n+1}), \ (d_{n+1}+1, r_{n+1}+1)) \\ b^{(n)}_{i, j} - b^{(n)}_{i, j-1} - x_{n+1} & ((i, j) = (u_{n+1}, r_{n+1}+1), \ (d_{n+1}+1, l_{n+1})) \\ b^{(n)}_{i, j} - b^{(n)}_{i, j-1} & (\text{otherwise}) \end{array} \right.

となるので、帰納法の仮定よりb^{(n+1)}_{i, j} - b^{(n)}_{i, j} = e^{(n+1)}_{i, j} \ (\forall i, j)が成り立つ。また(5)より、

c^{(n+1)}_{i, j} - c^{(n+1)}_{i-1, j} = \left\{ \begin{array}{ll} c^{(n)}_{i, j} - c^{(n)}_{i, j-1}+x_{n+1} & ((i, j) = (u_{n+1}, l_{n+1}), \ (d_{n+1}+1, r_{n+1}+1)) \\ c^{(n)}_{i, j} - c^{(n)}_{i, j-1} - x_{n+1} & ((i, j) = (u_{n+1}, r_{n+1}+1), \ (d_{n+1}+1, l_{n+1})) \\ c^{(n)}_{i, j} - c^{(n)}_{i, j-1} & (\text{otherwise}) \end{array} \right.

となるので、帰納法の仮定よりc^{(n+1)}_{i, j} - c^{(n)}_{i, j} = e^{(n+1)}_{i, j} \ (\forall i, j)が成り立つ。

従って、すべての1\leq k \leq Q, \ 1\leq i \leq H, \ 1\leq j \leq Wに対して

\begin{align} b^{(k)}_{i, j} = \sum_{l=1}^{j}\left(b^{(k)}_{i, l} - b^{(k)}_{i, l-1}\right) = \sum_{l=1}^{j}e^{(k)}_{i, l}, \\ c^{(k)}_{i, j} = \sum_{l=1}^{j}\left(c^{(k)}_{l, j} - c^{(l)}_{l-1, j}\right) = \sum_{l=1}^{j}e^{(k)}_{l, j} \end{align}

が成り立ちます。
各操作に対してe^{(k-1)}_{i, j}からe^{(k)}_{i, j}を求める部分の計算量がO(1)なので
e^{(Q)}_{i, j}を求めるのにO(Q)の計算量がかかり、またe^{(Q)}_{i, j}からb^{(Q)}_{i, j}, c^{(Q)}_{i, j} を求める部分にO(HW)の計算量がかかるので、b^{(Q)}_{i, j}, c^{(Q)}_{i, j} を求めるために必要な計算量はO(Q+HW)となります。
また、

a^{(Q)}_{i, j} = a^{(Q)}_{i, j-1} + c^{(Q)}_{i, j}, \ a^{(Q)}_{i, j} = a^{(Q)}_{i-1, j} +b^{(Q)}_{i, j}

であるので、b^{(Q)}_{i, j}, c^{(Q)}_{i, j}を用いてa^{(Q)}_{i, j}を求めるために必要な計算量はO(HW)となります。
以上よりO(Q+HW)Q回の操作後のa^{(Q)}_{i, j}を求めることができました。

なお、各iについてb^{(Q)}_{i, 1}, ..., b^{(Q)}_{i, W}を求めるごとにe^{(Q)}_{i, j}b^{(Q)}_{i, j}に順次更新し、その上で(7)の計算を行えば、得られるc^{(Q)}_{i, j}は求めたいa^{(Q)}_{i, j}と一致します。b^{(Q)}_{i, j}, c^{(Q)}_{i, j}を別々に求めるよりも、このような実装の方が楽かと思います。

実装

Ruby と Pythonでそれぞれclass を用いてimos法を実装したものです。上記での説明とは違い、0-index での実装となっていることには注意が必要です。また、値を更新する区間は半開区間としていることも注意が必要です。
pythonで実装したコード (1次元、2次元両方)
Rubyで実装したコード (1次元のみ)

応用問題

https://atcoder.jp/contests/abc338/tasks/abc338_d
この問題をコンテスト中に解けなかったことがきっかけとなって、この記事を書きました。

参考文献

より詳しい解説はこちら。特殊な座標系へのいもす法の適用なども書かれています。
https://imoz.jp/algorithms/imos_method.html
2次元のいもす法を可視化されています。
https://zenn.dev/reputeless/articles/article-2d-imos
こちらの方も数式を用いて説明されています。
https://zenn.dev/iharuoru/articles/c464535ced4b3b

Discussion