可変長ループを書こう！


## はじめに

突然ですが、あなたは多重ループは書けますか？

？？？：「書けますー！」

```test.py
cnt = 0
for i0 in range(3):
    for i1 in range(3):
        for i2 in range(3):
            for i3 in range(3):
                ##### ここに処理を書く #####
                cnt += 1
                #####
print(cnt) # 81

```

これは $4$ 重ループになってますが、これの可変なやつ（詳細は後述）をうまく処理しようというのがこの記事の趣旨です。言語は Python 、特に PyPy での実行を想定しています。

ループを書くことが目的なので「処理」のところは何でもよいのですが、ここではループした回数をカウントしています。実際の問題では、 ``ans += calc(i0, i1, i2, i3)`` のような数え上げを計算したり、 ``ans = max(ans, calc(i0, i1, i2, i3))`` のような更新をしたり、あるいは DP テーブルを更新したりするイメージです。


## 可変長ループとは

[^kahen]: 私が勝手に呼んでるだけですが
だいたいこんなのができるものを可変長ループと呼んでいます [^kahen]。


- 可変個（$K$ 個とします）の変数 $i_0,\ \cdots, i_{K-1}$ でループをする
- $i_k$ は $[l_k,\ r_k)$ の範囲の整数をループする。 $l_k,\ r_k$ は $i_0,\ \cdots,\ i_{k-1}$ に依存してもよい
- 変数間に満たすべき条件 $P(i_{k_0},\ \cdots,\ i_{k_{s-1}}) $ がいくつかある
- 非再帰かつ <b>空間</b> 計算量 $O(K)$
- $i_0,\ \cdots,\ i_{k-1}$ が満たすべき条件を満足していない場合は $i_k$ 以降のループは回さない [^edakari] 


いくつか例を挙げてみます。まずは愚直ループで書いてみるので、可変長にするにはどうすれば良いか考えながら見てみてください。
[^edakari]: これができるものを「枝刈り可変長ループ」とか呼んだりしてます（私が勝手にry


### 例1（愚直）

変数 $i_0,\ i_1,\ i_2,\ i_3$ が $0\le i_k < 2k+1$ を満たす整数を動くループを書いてください。
愚直に書くとこんな感じ。


```test.py
cnt = 0
for i0 in range(1):
    for i1 in range(3):
        for i2 in range(5):
            for i3 in range(7):
                ##### ここに処理を書く #####
                cnt += 1
                #####

print(cnt) # 105

```

[^it]: 具体的にどう書くかはあんまり考えていません
これぐらいなら Python の itertools を使うなどいろんな書き方があると思います[^it]。


### 例2（愚直）

さっきのに加えて、「隣り合う変数の合計が $3$ の倍数になるものは除く」という条件を入れたらどうなるでしょうか。

```test.py
cnt = 0
for i0 in range(1):
    for i1 in range(3):
        if (i0 + i1) % 3 == 0: continue
        for i2 in range(5):
            if (i1 + i2) % 3 == 0: continue
            for i3 in range(7):
                if (i2 + i3) % 3 == 0: continue
                ##### ここに処理を書く #####
                cnt += 1
                #####

print(cnt) # 31

```

これも itertools などでできそうですが、普通にループして全てのループ変数の組に対して条件を判定するやり方だと無駄なループができて計算量が大きくなってしまう可能性があります。具体的には、上の例だと `(i0 + i1) % 3 == 0` の時点でその後の `i2` や `i3` を見る必要はないにも関わらず、ループを回してしまうことになりそうです。
なので上の `continue` のように、条件を満たさなくなった時点でそれ以降の変数のループを回さないようにしたいです。

### 例3（愚直）

次はループ範囲がそれより前の変数に依存する場合を考えてみましょう。
例えば $i_k$ のループ範囲が $[0,\ i_0+\cdots +i_{k-1}+2)$ のように表される場合です。この例では右側だけ動かしていますが、左側が前の変数に依存することもありえます。


```test.py
cnt = 0
for i0 in range(1):
    for i1 in range(i0 + 2):
        if (i0 + i1) % 3 == 0: continue
        for i2 in range(i0 + i1 + 2):
            if (i1 + i2) % 3 == 0: continue
            for i3 in range(i0 + i1 + i2 + 2):
                if (i2 + i3) % 3 == 0: continue
                ##### ここに処理を書く #####
                cnt += 1
                #####
print(cnt) # 5

```

これのループ変数の個数を増やすと（例えば $K=10$ など）結構大変そうですね。
まあ書けなくはないんですけど。

```test.py
cnt = 0
for i0 in range(1):
    for i1 in range(i0 + 2):
        if (i0 + i1) % 3 == 0: continue
        for i2 in range(i0 + i1 + 2):
            if (i1 + i2) % 3 == 0: continue
            for i3 in range(i0 + i1 + i2 + 2):
                if (i2 + i3) % 3 == 0: continue
                for i4 in range(i0 + i1 + i2 + i3 + 2):
                    if (i3 + i4) % 3 == 0: continue
                    for i5 in range(i0 + i1 + i2 + i3 + i4 + 2):
                        if (i4 + i5) % 3 == 0: continue
                        for i6 in range(i0 + i1 + i2 + i3 + i4 + i5 + 2):
                            if (i5 + i6) % 3 == 0: continue
                            for i7 in range(i0 + i1 + i2 + i3 + i4 + i5 + i6 + 2):
                                if (i6 + i7) % 3 == 0: continue
                                for i8 in range(i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + 2):
                                    if (i7 + i8) % 3 == 0: continue
                                    for i9 in range(i0 + i1 + i2 + i3 + i4 + i5 + i6 + i7 + i8 + 2):
                                        if (i8 + i9) % 3 == 0: continue
                                        ##### ここに処理を書く #####
                                        cnt += 1
                                        #####
print(cnt) # 5964602

```

[^596]: この結果の 5964602 を覚えておいてね
これをきれいに書くのを目標にしましょう [^596]。



## 他の方法

本記事で示す方法以外にもいくつか方法があります。これらが使えることもあるでしょう。

1. 愚直にループを書く
上の例で書いたようなやつです。ただこれだとループの深さなどが可変の問題には対応しづらいですね。

1. itertools
不要なところもループしてしまうので場合によっては計算量が増えてしまう可能性があります。

1. 再帰 DFS
再帰は言語によっては遅くなるので使いたくなかったりします。

1. BFS
メモリが $O(K)$ では収まらないです。

1. 本記事の方法
可変長対応、不要なループをしない [^lp] 、非再帰、メモリは $O(K)$ 、のすべてを満足する方法です。
[^lp]: 「<a href = "https://qiita.com/Kiri8128/items/71985c934006adbe330a#%E3%81%95%E3%81%A3%E3%81%8D%E3%81%AE%E4%BE%8B2">さっきの例2</a>」セクション参照

## アイデア

アイデアというほどでもないですが、本記事の実装方法です。

- 各ループ変数に対して、「初期値」「終了値」を持つ。具体的には $i$ 番目のループ変数の初期値・終了値をそれぞれ `L[i]` および `R[i]` で表す 
- while 文をでループする

これだけです。



## 実装

### さっきの例1

さっきの例で実装してみます。
`chk` 関数で満たすべき条件を設定しています。この例では特に NG 条件がないので必ず $0$ を返すようにしています。


```test.py
def chk(i):
    if not i: return 0
    # i 番目の変数の条件（OK なら 0 を、NG なら 1 を返す）
    # i-1 番目までの変数に依存しても良い
    return 0

K = 4 # ループ変数の個数
Z = [0] * K
L = [0] * K
R = [0] * K
L[0], R[0] = 0, 1 # 半開区間 [L, R) をループする
i = 0
cnt = 0
while i >= 0:
    while i < K - 1:
        i += 1
        
        ##### 初期値・終了値の設定 #####
        L[i] = 0
        R[i] = 2 * i + 1
        #####
        
        Z[i] = L[i]
        if chk(i):
            break
    else:
        ##### ここに処理を書く #####
        cnt += 1
        #####
    
    Z[i] += 1
    while Z[i] >= R[i] or chk(i):
        if Z[i] < R[i]: Z[i] += 1
        while Z[i] >= R[i]:
            i -= 1
            if i < 0: break
            Z[i] += 1
        if i < 0: break

print(cnt) # 105

```


### さっきの例2

ループ変数間に条件がある場合です。
`chk` 関数に条件を入れています。 $i$ 番目のループ変数の条件のところでは $i$ 番目までの変数に依存して決めて良いです。言い換えると、いくつかのループ変数に関係する条件については、その中で最も大きな番号のループ変数のところで判定をして、 NG ならすぐ次に移るようにしています。


```test.py
def chk(i):
    if not i: return 0
    # i 番目の変数の条件（OK なら 0 を、NG なら 1 を返す）
    # i-1 番目までの変数に依存しても良い
    return 1 if (Z[i-1] + Z[i]) % 3 == 0 else 0

K = 4 # ループ変数の個数
Z = [0] * K
L = [0] * K
R = [0] * K
L[0], R[0] = 0, 1 # 半開区間 [L, R) をループする
i = 0
cnt = 0
while i >= 0:
    while i < K - 1:
        i += 1
        
        ##### 初期値・終了値の設定 #####
        L[i] = 0
        R[i] = 2 * i + 1
        #####
        
        Z[i] = L[i]
        if chk(i):
            break
    else:
        ##### ここに処理を書く #####
        cnt += 1
        #####
    
    Z[i] += 1
    while Z[i] >= R[i] or chk(i):
        if Z[i] < R[i]: Z[i] += 1
        while Z[i] >= R[i]:
            i -= 1
            if i < 0: break
            Z[i] += 1
        if i < 0: break

print(cnt) # 31

```


### さっきの例3

[^ri]: ここでは一回ごとに $\Theta(k)$ をかけて設定していますが、ループしながら順に計算することで、それぞれ $O(1)$ で設定することもできます

$i$ 番目のループ変数の範囲が $i-1$ 番目までのループ変数に依存する場合です。
`R[i] = sum(Z[:i]) + 2` のところで設定しています [^ri] 。


```test.py
def chk(i):
    if not i: return 0
    # i 番目の変数の条件（OK なら 0 を、NG なら 1 を返す）
    # i-1 番目までの変数に依存しても良い
    return 1 if (Z[i-1] + Z[i]) % 3 == 0 else 0

K = 4 # ループ変数の個数
Z = [0] * K
L = [0] * K
R = [0] * K
L[0], R[0] = 0, 1 # 半開区間 [L, R) をループする
i = 0
cnt = 0
while i >= 0:
    while i < K - 1:
        i += 1
        
        ##### 初期値・終了値の設定 #####
        L[i] = 0
        R[i] = sum(Z[:i]) + 2
        #####
        
        Z[i] = L[i]
        if chk(i):
            break
    else:
        ##### ここに処理を書く #####
        cnt += 1
        #####
    
    Z[i] += 1
    while Z[i] >= R[i] or chk(i):
        if Z[i] < R[i]: Z[i] += 1
        while Z[i] >= R[i]:
            i -= 1
            if i < 0: break
            Z[i] += 1
        if i < 0: break

print(cnt) # 5

```

さっきみたいに $K=10$ にしてみましょう。


```test.py
def chk(i):
    if not i: return 0
    # i 番目の変数の条件（OK なら 0 を、NG なら 1 を返す）
    # i-1 番目までの変数に依存しても良い
    return 1 if (Z[i-1] + Z[i]) % 3 == 0 else 0

K = 10 # ループ変数の個数
Z = [0] * K
L = [0] * K
R = [0] * K
L[0], R[0] = 0, 1 # 半開区間 [L, R) をループする
i = 0
cnt = 0
while i >= 0:
    while i < K - 1:
        i += 1
        
        ##### 初期値・終了値の設定 #####
        L[i] = 0
        R[i] = sum(Z[:i]) + 2
        #####
        
        Z[i] = L[i]
        if chk(i):
            break
    else:
        ##### ここに処理を書く #####
        cnt += 1
        #####
    
    Z[i] += 1
    while Z[i] >= R[i] or chk(i):
        if Z[i] < R[i]: Z[i] += 1
        while Z[i] >= R[i]:
            i -= 1
            if i < 0: break
            Z[i] += 1
        if i < 0: break

print(cnt) # 5964602
```

ちゃんと同じ結果になりました。





## 問題（ネタバレ注意）

### ABC 161 - D
<a href = "https://atcoder.jp/contests/abc161/tasks/abc161_d">問題</a>
ループするだけですね。

<a href = "https://atcoder.jp/contests/abc161/submissions/20655517">ACコード</a>
<a href = "https://atcoder.jp/contests/abc161/submissions/11552041">愚直ループ</a> でもできるけど本番では書きたくないです。


### ARC 095 - E

<a href = "https://atcoder.jp/contests/arc095/tasks/arc095_c">問題</a>
想定解じゃないけど、自然に可変長ループすると枝刈りになって通ります [^ac] [^uso]。想定解の方法でも可変長ループが使えます。
[^ac]: AC コードでは可変長ループを二重に使っています。もちろん一重に全部入れることもできます
[^uso]: 嘘解法っぽいけど、嘘かどうかはこの記事の目的とは関係ないので気にしません

<a href = "https://atcoder.jp/contests/arc095/submissions/20621162">ACコード</a>


### ARC 104 - E
<a href = "https://atcoder.jp/contests/arc104/tasks/arc104_e">問題</a>
まあこれぐらいなら可変長ループいらないけど。
<a href = "https://atcoder.jp/contests/arc104/submissions/17170329">ACコード</a> （古い書き方してたときのやつなのでこの記事の書き方とは違う）


## Q&A

Q. これ需要あるの？
[^in]: 個人の感想です
A. あんまりない気がしています。でも私は結構使ってます。直感的に書ける [^in] ので気に入っています。

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">書いても需要ない記事ばかり書く人というのが存在していてほしい</p>&mdash; えびちゃん (@rsk0315_h4x) <a href="https://twitter.com/rsk0315_h4x/status/1367129700695379969?ref_src=twsrc%5Etfw">March 3, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>


Q. 再帰じゃダメなの？
[^rec]: 再帰、ダメ、絶対
A. PyPy だと再帰遅いので [^rec] 。もちろん計算時間に十分余裕があるなら大丈夫です。


Q. BFS じゃだめなの？
A. これは全然だめじゃないです。本記事の方法だとメモリが $O(K)$ で良いというメリットがありますが、メモリがネックになることはあまりない気がします。

Q. そもそも PyPy だと JIT コンパイルでめちゃくちゃメモリ食うから、メモリ $O(K)$ って意味なくない？
A. それを言われるととてもつらい気持ちになります。

Q. もっときれいに書けない？
A. 書けそう。教えてください。

Q. Step は設定できないの？（ループ変数を $2$ ずつ増やすとか）
A. ちょっといじればできます。