Zenn
📚

【競技プログラミング】ダブリングまとめ

2022/07/11に公開
1

競技プログラミングにおける典型アルゴリズムの1つ「ダブリング」の概要と類題を自分用にまとめる。

ダブリング

アルゴリズムの概要

全体の要素数がNN個あって、それぞれの要素について、その要素から1回遷移(移動)したときの移動先が定まっているとする。
このとき、「KK回遷移したときの到達点」を高速に求めるアルゴリズムの1つがダブリングである。

このような問題を愚直に求めようとするとO(K)\mathcal{O}(K)になるが、ダブリングを用いることによりO(NlogK)\mathcal{O}(N\log{K})で計算できるようになる。

アルゴリズムの詳細

一般的なダブリングでは、動的計画法で用いるDPテーブルに似た以下のようなテーブルを事前に計算する。

dp[i][j]dp[i][j]: jj番目の要素から2i2^i回遷移したときの到達地点。ただし、1jN,0ilog2K1 \leq j \leq N, 0 \leq i \leq \lceil \log_{2} {K} \rceil

このテーブルの初期条件は以下のようになる。ここで、集合S={1,2,,N}S = \{1, 2, \ldots, N\}に対して、「jj番目の要素から1回遷移したときの到達地点」を表す関数f:SSf: S \rightarrow Sf(j)f(j) (1jN)(1 \leq j \leq N)と定める。

{初期条件: dp[0][j]=f(j)(1jN)漸化式: dp[i][j]=dp[i1][dp[i1][j]](0ilog2K,1jN) \begin{cases} \text{初期条件: } &dp[0][j] = f(j) \quad (1 \leq j \leq N) \\ \text{漸化式: } &dp[i][j] = dp[i - 1][dp[i - 1][j]] \quad (0 \leq i \leq \lceil \log_2 K \rceil, 1 \leq j \leq N) \end{cases}
初期条件と漸化式について補足

初期条件(i=0)(i = 0)については、20=12 ^ 0 = 1回遷移したときの到達点はf(j)f(j)の値をそのまま入れておけばよいというだけである。

漸化式の方については、以下のように導出すればよい。

全てのjji1i - 1以下についてdp[i1][j]dp[i - 1][j]が求まっている状態で、jj番目の要素から2i2 ^ i回遷移したときの到達点を求めたい。
これを求めるには、jj番目の要素から2i12 ^ {i - 1}回遷移した後、更にもう2i12 ^ {i - 1}回遷移した結果が得られればよい。
jj番目の要素から2i12 ^ {i - 1}回遷移したときの到達点」はdp[i1][j]dp[i - 1][j]に入っているので、そこから更に2i12^{i - 1}回遷移した結果はdp[i1][dp[i1][j]]dp[i - 1][dp[i - 1][j]]に格納されている。これをdp[i][j]dp[i][j]に格納すればよい。

事前計算によってこのテーブルを計算できれば、KKを2進数とみなし、KKの各ビットを下位から見ていき、テーブルを用いて結果を更新していけばよい。
すなわち、K=2c1+2c2+2c3++2ck(0c1c2ck)K = 2^{c_1} + 2^{c_2} + 2^{c_3} + \cdots + 2^{c_k} \quad (0 \leq c_1 \leq c_2 \cdots c_k)と表したとき、c1c_1から順に、答えAAA=dp[cl][A](1lk)A = dp[c_l][A] \quad (1 \leq l \leq k)と更新していくことで解を得られる。

計算量は、事前計算でO(NlogK)\mathcal{O}(N\log{K})、解の計算でO(logK)\mathcal{O}(\log{K})となる。

類題

ダブリングを使って解くことのできる類題をいくつか挙げる。

AtCoder Beginner Contest 167 D - Teleporter

問題

AtCoderのページより引用する。

問題文

高橋王国にはNN個の町があります。町は11からNNまで番号が振られています。

それぞれの町にはテレポーターが11台ずつ設置されています。町i(1iN)i (1 \leq i \leq N)のテレポーターの転送先は町AiA_iです。

高橋王は正の整数KKが好きです。わがままな高橋王は、町11から出発してテレポーターをちょうどKK回使うと、どの町に到着するかが知りたいです。

高橋王のために、これを求めるプログラムを作成してください。

制約

  • 2N2×1052 \leq N \leq 2 \times 10^5
  • 1AiN1 \leq A_i \leq N
  • 1K10181 \leq K \leq 10^{18}

解答例

ダブリングを使って簡単に解くことができる。

テーブルを以下のように定義する。

dp[i][j]dp[i][j]: 町jjから2i2^i回テレポートして到達した町の番号 (0ilog2K,1jN)(0 \leq i \leq \log_2 K, 1 \leq j \leq N)

初期条件と漸化式は以下のようになる。

{初期条件: dp[0][j]=Aj(1jN)漸化式: dp[i][j]=dp[i1][dp[i1][j]](1ilog2K,1jN) \begin{cases} \text{初期条件: } &dp[0][j] = A_{j} \quad (1 \leq j \leq N) \\ \text{漸化式: } &dp[i][j] = dp[i - 1][dp[i - 1][j]] \quad (1 \leq i \leq \lceil \log_2 K \rceil, 1 \leq j \leq N) \end{cases}

Pythonによるコード例を以下に示す。尚、KKが最大で101810^{18}であるためlogK\log Kを60程度に取らないといけないが、NNは最大で2×1052 \times 10^5であり、計算量が10710^7オーダーになる場合がある。そのため、PythonではPyPy3で提出しないとTLEになるので注意。

d.py
from typing import List


def main():
    # 入力受け取り
    N, K = map(int, input().split())
    # 0-indexedで受け取っておく
    A: List[int] = list(map(lambda x: int(x) - 1, input().split()))

    # ダブリングのテーブル
    dp: List[List[int]] = [[0 for j in range(N)] for i in range(61)]
    # 初期条件
    for j in range(N):
        dp[0][j] = A[j]

    # 遷移
    for i in range(1, 61):
        for j in range(N):
            dp[i][j] = dp[i - 1][dp[i - 1][j]]

    # 解を求める
    answer: int = 0
    # 現在見ているビットの下位からの桁
    i: int = 0
    # Kを2進数とみなして計算する
    while K:
        # Kの下位からi桁目が1なら遷移する
        if K & 1:
            answer = dp[i][answer]
        # 1つビットシフトする
        K >>= 1
        # iを進める
        i += 1

    # 解の出力(1-indexedに戻す)
    print(answer + 1)


if __name__ == "__main__":
    main()

実際の提出はこちら

競プロ典型90問 058 - Original Calculator (★4)

問題

AtCoderのページより引用する。

問題文

あなたは奇妙な電卓を持っています。この電卓は00以上105110^5-1以下の整数を11つ表示できます。この電卓にはボタンAと呼ばれるボタンがあります。整数xxが表示されているときに ボタンA を11回押すと、次の処理が順番に行われます。

  1. xxを十進法で表したときの各桁の和を計算し、yyとする。
  2. x+yx+y10510^5で割ったあまりを計算し、zzとする。
  3. 表示されている整数をzzに変更する。

例えば、9999999999が表示されているときに ボタンA を11回押すと、99999+(9+9+9+9+9)=10004499999+(9+9+9+9+9)=100044なので、表示される整数は4444に変更されます。

今、この電卓にNNが表示されています。 ボタンA をKK回押した後に表示されている整数を求めて下さい。

制約

  • 0N10510 \leq N \leq 10^5-1
  • 1K10181 \leq K \leq 10^{18}
  • 入力はすべて整数

解答例

ダブリングテーブルを以下のように定義する。

dp[i][j]dp[i][j]: 整数jjが表示されている状態から2i2^i回ボタンを押した後に表示される整数(0ilog2K,0j<105)(0 \leq i \leq \log_2 K, 0 \leq j < 10^5)

整数xxが表示されているときにボタンを1回押した後表示される数値zzを求める関数をf(x)f(x)と表記することにする。
このとき、初期条件と漸化式は以下のようになる。

{初期条件: dp[0][j]=f(j)(0j<105)漸化式: dp[i][j]=dp[i1][dp[i1][j]](1ilog2K,0j<105) \begin{cases} \text{初期条件: } &dp[0][j] = f(j) \quad (0 \leq j < 10^5) \\ \text{漸化式: } &dp[i][j] = dp[i - 1][dp[i - 1][j]] \quad (1 \leq i \leq \lceil \log_2 K \rceil, 0 \leq j < 10^5) \end{cases}

Pythonによる実装例を以下に示す。こちらはPyPy3でなくてもTLEにはならない様子。

058.py
from typing import List
import math


def f(x: int) -> int:
    # 整数xを文字列として解釈して計算する
    y: int = sum(map(lambda s: ord(s) - ord("0"), str(x)))
    return (x + y) % 100000


def main():
    # 入力受け取り
    N, K = map(int, input().split())

    # log_2 KをMとおく。
    M: int = math.ceil(math.log2(K))

    # ダブリングテーブル
    dp: List[List[int]] = [[0 for j in range(100000)] for i in range(M + 1)]

    # 初期条件
    for j in range(100000):
        dp[0][j] = f(j)

    # 遷移
    for i in range(1, M + 1):
        for j in range(100000):
            dp[i][j] = dp[i - 1][dp[i - 1][j]]

    # 解を求める
    answer: int = N
    i: int = 0
    while K:
        if K & 1:
            answer = dp[i][answer]
        K >>= 1
        i += 1

    print(answer)


if __name__ == "__main__":
    main()

実際の提出はこちら

AtCoder Beginner Contest 241 E - Putting Candies

問題

問題文をAtCoderのページより引用する。

問題文

長さNNの数列A=(A0,A1,,AN1)A=(A_0,A_1,\ldots,A_{N-1})が与えられます。
最初の時点では空の皿があり、高橋君は次の操作をKK回繰り返します。

  • 皿の中のアメの個数をXXとする。皿にA(XmodN)A_{(X\bmod N)}個のアメを追加する。
    ただし、XmodNX\bmod NXXNNで割った余りを表す。

KK回の操作の後で、皿の中には何個のアメがあるか求めてください。

制約

  • 2N2×1052 \leq N \leq 2\times 10^5
  • 1K10121 \leq K \leq 10^{12}
  • 1Ai1061 \leq A_i\leq 10^6
  • 入力はすべて整数である。

解答例

ダブリングテーブルを以下のように定義する。

dp[i][j]dp[i][j]: 皿の上のアメの数がj(=Xmod  N)j (= X \mod N)個のとき、そこから2i2^i回操作を繰り返すことで追加されるアメの総数 (0jN,0ilog2K)(0 \leq j \leq N, 0 \leq i \leq \lceil \log_2 K \rceil)

アメの数がj(=Xmod  N)j (=X \mod N)のとき、その状態から2i2^i回操作を行ったときに追加されるアメの総数は、「皿の上のアメの数がjj個のときから2i12^{i - 1}回操作を繰り返したときに追加されるアメの個数」++「そこから更にもう2i12^{i - 1}回操作を繰り返したときに追加されるアメの個数」である。
後者の項の解釈:皿の上には元々jj個乗っていて、その状態で2i12^{i - 1}回操作を繰り返す(前者の項の操作)と、皿の上にはdp[i1][j]dp[i - 1][j]個のアメが追加される。そこから更に2i12^{i - 1}回操作を繰り返すとき、皿の上にはj+dp[i1][j]j + dp[i - 1][j]個のアメがあることになるので、dp[i1][(j+dp[i1][j])mod  N]dp[i - 1][(j + dp[i - 1][j]) \mod N]となる。

従って、初期条件と漸化式は以下のようになる。

{初期条件: dp[0][j]=Aj(1jN)漸化式: dp[i][j]=dp[i1][j]+dp[i1][dp[i1][j]](1ilog2K,0jN) \begin{cases} \text{初期条件: } &dp[0][j] = A_j \quad (1 \leq j \leq N) \\ \text{漸化式: } &dp[i][j] = dp[i - 1][j] + dp[i - 1][dp[i - 1][j]] \quad (1 \leq i \leq \lceil \log_2 K \rceil, 0 \leq j \leq N) \end{cases}

Pythonによる実装例を以下に示す。この問題もKKの最大値が101210^12なので、iiの値はせいぜい40まででよい。そうしないと全体の計算量が10710^7オーダーになり、Pythonだと時間制限が厳しくなるので注意。

e.py
from typing import List


def main():
    # 入力受け取り
    N, K = map(int, input().split())
    A: List[int] = list(map(int, input().split()))

    # ダブリングテーブル
    dp: List[List[int]] = [[0 for j in range(N)] for i in range(41)]
    # 初期条件
    for j in range(N):
        dp[0][j] = A[j]

    # 遷移
    for i in range(1, 41):
        for j in range(N):
            dp[i][j] = dp[i - 1][j] + dp[i - 1][(j + dp[i - 1][j]) % N]

    # 答えを格納する変数
    answer: int = 0
    i: int = 0
    while K:
        if K & 1:
            # dp[i][j]は「操作によって追加されるアメの数」であるので、
            # 答えの数に加算していく。
            answer += dp[i][answer % N]
        K >>= 1
        i += 1

    # 回答出力
    print(answer)


if __name__ == "__main__":
    main()


実際の提出はこちら

AtCoder Beginner Contest 258 E - Packing Potatoes

問題

問題文をAtCoderのページより引用する。

問題文

ベルトコンベアに載って1010010^{100}個のじゃがいもが11個ずつ流れてきます。流れてくるじゃがいもの重さは長さNNの数列W=(W0,,WN1)W = (W_0, \dots, W_{N-1})で表され、i(1i10100)i \, (1 \leq i \leq 10^{100})番目に流れてくるじゃがいもの重さはW(i1)modNW_{(i-1) \bmod N}です。ここで、(i1)modN(i-1) \bmod Ni1i - 1NNで割った余りを表します。

高橋君は、まず空の箱を用意し、次のルールに従ってじゃがいもを順番に箱に詰めていきます。

  • じゃがいもを箱に入れる。箱に入っているじゃがいもの重さの総和がXX以上になったら、その箱には蓋をし、新たに空の箱を用意する。

QQ個のクエリが与えられます。i(1iQ)i \, (1 \leq i \leq Q)番目のクエリでは、正整数KiK_iが与えられるので、KiK_i番目に蓋をされた箱に入っているじゃがいもの個数を求めてください。問題の制約下で、蓋をされた箱がKiK_i個以上存在することが証明できます。

制約

  • 1N,Q2×1051 \leq N, Q \leq 2 \times 10^5
  • 1X1091 \leq X \leq 10^9
  • 1Wi109(0iN1)1 \leq W_i \leq 10^9 \, (0 \leq i \leq N - 1)
  • 1Ki1012(1iQ)1 \leq K_i \leq 10^{12} \, (1 \leq i \leq Q)
  • 入力は全て整数

解答例

公式解説に倣い、各i=1,2,,10100i = 1, 2, \ldots, 10^{100}に対してj=(i1)mod  Nj = (i - 1) \mod Nをそのじゃがいもの「種類」と呼ぶこととする。
また、配列C=(C1,C2,,CN)C = (C_1, C_2, \ldots, C_N)の要素CjC_jを「種類jjのじゃがいもから始めて順番に箱詰めを行ったとき、何個入れた時点で蓋をされるか?」を表す数とする。

ダブリングテーブルを以下のように定義する。

dp[i][j]dp[i][j]: 種類jjのじゃがいもから始めて順番に箱詰めを行っていき、2i2^{i}個の箱の蓋が閉じられたときの、次に箱詰めする対象となるじゃがいもの種類。(0ilog2K,1jN)(0 \leq i \leq \lceil \log_2 K \rceil, 1 \leq j \leq N)

KKが与えられたとき、ダブリングテーブルを使うことで、K1K - 1個の箱の蓋が閉じられた後最初に入れるじゃがいもの種類がわかることになる(KK番目の箱に最初に入れるじゃがいもの種類がわかるということである)。
配列CCが既知であるなら、これらの情報より、KK番目の箱に入るじゃがいもの個数を求めることができる。

配列CCの求め方

まずは配列CCを求める。これがわからないとダブリングテーブルの漸化式が求まらない。

愚直に求めるならば、各jjからスタートしてjjを1つずつインクリメントしながらW(j1)mod  NW_{(j - 1) \mod N}を加算していき、初めて重さの和がXX以上になったところでストップする。
このときの添字をllとしたとき、Cj=lj+1C_j = l - j + 1と計算することができる。

しかし、この計算だと例えばX=109,Wj=1X = 10^9, W_j = 1のような条件では10910^9オーダーの計算が必要になってしまう。
そこで、次のように考える。W=(W1,W2,,WN)W = (W_1, W_2, \ldots, W_N)の総和をSSとおく。種類jjのじゃがいもから始めて順番に箱詰めしていったとき、S<XS < Xが成り立つならば、1周してきて種類jjに戻ってからまたいくつかのじゃがいもを入れて蓋をすることになる。
これを考えると、種類jjから始めて蓋をするまでに、XS\lfloor \frac{X}{S} \rfloor周してからもう数個じゃがいもを入れることになる。
XS\lfloor \frac{X}{S} \rfloor周したとき、残りの許容重量はXmod  SX \mod Sとなるから、種類jjから始めて重さが初めてXmod  SX \mod S以上になったときの種類をllとすればよいことがわかる。

これを踏まえると、配列CCは次のように求められる。

  • CCの各要素はXS×N\lfloor \frac{X}{S} \rfloor \times Nで初期化しておく。
  • WWを2周繋げた配列の累積和を取った配列VVを用意しておき、各jjについて以下のようにllを求める。
    • VlX+VjV_l \geq X + V_jを満たす最小の添字llを二分探索法によって見つける。
    • CjC_j(lj)(l - j)を加算する。

これで配列CiC_iを求めることができた。

ダブリングテーブルの初期条件と漸化式

ダブリングテーブルの初期条件と漸化式は以下のようになる。

{初期条件: dp[0][j]=(j+Cj)mod  N(1jN)漸化式: dp[i][j]=dp[i1][dp[i1][j]](1ilog2K,1jN) \begin{cases} \text{初期条件: } &dp[0][j] = (j + C_j) \mod N \quad (1 \leq j \leq N) \\ \text{漸化式: } &dp[i][j] = dp[i - 1][dp[i - 1][j]] \quad (1 \leq i \leq \lceil \log_2 K \rceil, 1 \leq j \leq N) \end{cases}

あとはK1K - 1回遷移したときの添字llを求め、ClC_lを出力すればよい。

Pythonによる実装例を以下に示す。

e.py
from typing import List
import sys
import itertools
import bisect


input = sys.stdin.readline


def main():
    # 入力受け取り
    N, Q, X = map(int, input().split())
    W: List[int] = list(map(int, input().split()))
    K: List[int] = [int(input()) for i in range(Q)]

    # Wの総和
    S: int = sum(W)
    # 配列C
    # 各要素は「何周するか×1周の個数」で初期化しておく
    C: List[int] = [(X // S) * N] * N

    # ダブリングテーブル
    dp: List[List[int]] = [[0 for j in range(N)] for i in range(41)]

    # 周回分はすでに加算しているので残った余りについて計算する
    X %= S
    # Wを2周分繋げて累積和を取った配列V
    V: List[int] = [0] + list(itertools.accumulate(W * 2))
    # 二分探索で配列Cの各要素の値を求める
    for j in range(N):
        l: int = bisect.bisect_left(V, X + V[j])
        C[j] += l - j

    # aダブリングテーブルの初期条件
    for j in range(N):
        dp[0][j] = (j + C[j]) % N

    # 遷移
    for i in range(1, 41):
        for j in range(N):
            dp[i][j] = dp[i - 1][dp[i - 1][j]]

    # 各クエリの解を求める
    for k in K:
        # K - 1番目の箱の蓋を締め終わったとき、
        # 次に入れるじゃがいもの種類が知りたい
        k -= 1
        index: int = 0
        i: int = 0
        while k:
            if k & 1:
                index = dp[i][index]
            k >>= 1
            i += 1

        # 解の出力
        print(C[index])


if __name__ == "__main__":
    main()

実際の提出はこちら

GitHubで編集を提案
1

Discussion

ログインするとコメントできます