Official

E - スムーズな山道 / Smooth Mountain Path Editorial by admin

gemini-3.5-flash-thinking(計算量の説明が誤り)

概要

この問題は、隣り合う桁の差(段差)が右に進むにつれて単調非減少(等しいか大きくなる)であるような、指定された範囲 \([L, R]\) 内の整数の個数を求める問題です。

制約として \(L, R\) の桁数が最大で \(5000\) 桁と非常に大きいため、通常の整数型で扱うことはできません。数え上げの典型手法である桁DP(Digit DP)を用いることで、高速かつ正確に解くことができます。


考察

1. 範囲 \([L, R]\) の言い換え

まずは、範囲 \([L, R]\) 内の条件を満たす整数の個数を直接求めるのではなく、定番の累積和の考え方を適用します。 \(f(X)\) を「\(1\) 以上 \(X\) 以下のスムーズな山道の個数」と定義すると、求める答えは以下のように表せます。

\[\text{Ans} = f(R) - f(L) + (L \text{ がスムーズな山道なら } 1, \text{ そうでなければ } 0)\]

これにより、任意の正の整数 \(N\) に対して \(f(N)\) を高速に求める問題へと帰着されます。

2. スムーズな山道の判定に必要な情報

ある数がスムーズな山道であるかを左から順に(上の桁から順に)決めていくことを考えます。 第 \(i\) 桁目の数字を \(d_i\) としたとき、次の第 \(i+1\) 桁目の数字 \(d_{i+1}\) を決める際に必要な情報は以下の2点のみです。

  1. 直前の桁の数字: \(d_i\) (次の段差 \(|d_i - d_{i+1}|\) を計算するため)
  2. 直前の段差: \(diff = |d_{i-1} - d_i|\) (次の段差がこれ以上である必要があるため)

※ただし、まだ段差が1つも存在しない状態(1桁目までしか決まっていない状態)では、次の段差に対する制限はありません。この「制限なし」の状態を \(diff = 10\) という特別な値で表すことにします。

このように、状態を「(現在の数字, 直前の段差)」のペアで管理すれば、それ以前の具体的な数字の並び方は気にする必要がありません。


アルゴリズム

1. 後ろからの動的計画法(Suffix DP)の事前計算

\(f(N)\) を求める際、「ある桁まで \(N\) と一致し、次の桁で \(N\) より小さい数字を選び、それ以降の桁は自由に決める」という遷移を行います。 この「残り \(rem\) 桁を自由に決める方法の数」は、上限 \(N\) の値に依存せず、「残り桁数」「現在の数字」「直前の段差」のみで決まります。これを事前に DP で計算しておきます。

DPテーブルの定義

  • suf_all[rem][d * 11 + diff] : 残り \(rem\) 桁で、現在の数字が \(d\)、課されている最小の段差が \(diff\) であるとき、それ以降の桁を適切に埋めてスムーズな山道を作る方法の数。
    • 状態数: \(5000 \times 10 \times 11 \approx 5.5 \times 10^5\)

遷移

残り \(rem\) 桁から残り \(rem+1\) 桁への遷移を考えます。次に置く数字を \(nx \in \{0, \ldots, 9\}\) とすると、新たな段差は \(nd = |d - nx|\) となります。 スムーズな山道の条件を満たすためには \(nd \geq diff\) である必要があります。

\[\text{suf\_all}[rem][d \times 11 + diff] = \sum_{\substack{0 \leq nx \leq 9 \\ |d - nx| \geq diff}} \text{suf\_all}[rem-1][nx \times 11 + |d - nx|] \pmod{10^9+7}\]

初期値は \(rem = 0\) のとき、これ以上桁を埋める必要がないため、すべての状態に対して 1 とします。


2. \(f(N)\) の数え上げ

事前計算した suf_all を用いて、 \(N\) 以下のスムーズな山道の個数を以下の3つのグループに分けて数え上げます。

① 桁数が \(N\) の桁数 \(M\) 未満の数

例えば \(N = 352\) (3桁)のとき、1桁および2桁の数をすべて数え上げます。 先頭の桁 \(d \in \{1, \ldots, 9\}\) を決めると、残りの桁数は自由に決められます(最初の段差の制約はないので \(diff = 10\))。

\[\sum_{len=1}^{M-1} \sum_{d=1}^{9} \text{suf\_all}[len-1][d \times 11 + 10]\]

② 桁数が \(M\) で、 \(N\) 未満の数

左から順に \(N\) と一致する桁を伸ばしていき、途中の第 \(i\) 桁目(0-indexed)で \(N[i]\) 未満の数字 \(d\) を選ぶ場合を考えます。 - \(i = 0\)(先頭桁)のとき: \(d \in \{1, \ldots, N[0]-1\}\) を選びます。残りの桁数は \(M-1\) で、制約は \(diff = 10\)。 - \(i = 1\) のとき: \(d \in \{0, \ldots, N[1]-1\}\) を選びます。直前の段差は \(diff = |N[0] - d|\) となり、残りの桁数は \(M-2\)。 - \(i \geq 2\) のとき: これまでの接頭辞 \(N[0 \ldots i-1]\) がすでにスムーズな山道の条件を満たしている場合のみ遷移可能です。 \(d \in \{0, \ldots, N[i]-1\}\) のうち、新しい段差 \(diff = |N[i-1] - d|\) が直前の段差 \(|N[i-1] - N[i-2]|\) 以上になるものを選びます。残りの桁数は \(M - i - 1\)

条件を満たす各ケースについて、対応する suf_all の値を足し合わせます。

\(N\) 自体

\(N\) 自体がスムーズな山道の条件を満たしている場合、答えに \(1\) を加算します。


計算量

  • 時間計算量: \(O(M \cdot D^2)\)
    • ここで \(M\) は最大桁数(\(5000\))、\(D\) は基数(\(10\))です。
    • 事前計算の DP の遷移は、各状態(\(M \times 10 \times 11\) 通り)に対して次の数字 \(nx\)\(10\) 通りを試すため、全体で \(5000 \times 110 \times 10 \approx 5.5 \times 10^6\) 回のループとなり、制限時間内に余裕で間に合います。
    • \(f(N)\) の計算は、各桁 \(i\) について高々 \(10\) 回の遷移を見るため \(O(M \cdot D)\) であり、非常に高速です。
  • 空間計算量: \(O(M \cdot D^2)\)
    • DPテーブル suf_all のサイズは \(5000 \times 110\) 個の整数であり、メモリ制限に対しても極めて軽量です。

実装のポイント

  1. 状態のエンコード: 2次元の情報を1次元に圧縮するため、d * 11 + diff というインデックスを用いています。これにより、多次元配列へのアクセスを高速化し、Pythonにおけるオーバーヘッドを削減しています。

  2. 遷移の事前計算: valid_nx[d][diff] に「数字 \(d\) の次に置くことができ、かつ差が \(diff\) 以上になるような次の数字 \(nx\) とその段差のリスト」をあらかじめ用意しておくことで、DPの二重ループ内での無駄な条件分岐を省き、定数倍の高速化を図っています。

    ソースコード

import sys

def main():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    L_str = data[0]
    R_str = data[1]
    
    MOD = 10**9 + 7
    M = len(R_str)
    
    # valid_nx[d][diff] = list of (nx, nd) where nd = abs(d - nx) and nd >= diff
    # diff = 10 represents -1 (no constraint)
    valid_nx = [[[] for _ in range(11)] for _ in range(10)]
    for d in range(10):
        for diff in range(11):
            if diff == 10:
                valid_nx[d][diff] = [(nx, abs(d - nx)) for nx in range(10)]
            else:
                valid_nx[d][diff] = [(nx, abs(d - nx)) for nx in range(10) if abs(d - nx) >= diff]
                
    # DP suffix
    # suf_all[rem][d * 11 + diff]
    suf_all = []
    dp = [1] * 110
    suf_all.append(dp)
    for rem in range(1, M + 1):
        new_dp = [0] * 110
        for d in range(10):
            for diff in range(11):
                val = 0
                for nx, nd in valid_nx[d][diff]:
                    val += dp[nx * 11 + nd]
                new_dp[d * 11 + diff] = val % MOD
        dp = new_dp
        suf_all.append(dp)
        
    def solve_f(N_str):
        if not N_str or N_str == "0":
            return 0
        M_local = len(N_str)
        N = [int(c) for c in N_str]
        
        ans = 0
        
        # 1. Less than M_local digits
        for len_val in range(1, M_local):
            for d in range(1, 10):
                ans += suf_all[len_val - 1][d * 11 + 10]
        ans %= MOD
        
        # 2. Exactly M_local digits, strictly less than N
        is_smooth = [True] * M_local
        D = [0] * M_local
        for i in range(1, M_local):
            D[i] = abs(N[i] - N[i-1])
            if i >= 2:
                is_smooth[i] = is_smooth[i-1] and (D[i] >= D[i-1])
            else:
                is_smooth[i] = is_smooth[i-1]
                
        for i in range(M_local):
            if i > 0 and not is_smooth[i-1]:
                break
                
            limit = N[i]
            start = 1 if i == 0 else 0
            for d in range(start, limit):
                if i == 0:
                    ans += suf_all[M_local - 1][d * 11 + 10]
                elif i == 1:
                    diff = abs(N[0] - d)
                    ans += suf_all[M_local - 2][d * 11 + diff]
                else:
                    diff = abs(N[i-1] - d)
                    if diff >= D[i-1]:
                        ans += suf_all[M_local - i - 1][d * 11 + diff]
            ans %= MOD
            
        # 3. N itself
        if is_smooth[M_local-1]:
            ans += 1
            ans %= MOD
            
        return ans

    def is_smooth_num(S):
        n = len(S)
        if n <= 2:
            return True
        D = [abs(int(S[i]) - int(S[i-1])) for i in range(1, n)]
        for i in range(1, len(D)):
            if D[i] < D[i-1]:
                return False
        return True

    ans = (solve_f(R_str) - solve_f(L_str) + (1 if is_smooth_num(L_str) else 0)) % MOD
    print(ans)

if __name__ == '__main__':
    main()

この解説は gemini-3.5-flash-thinking によって生成されました。

posted:
last update: