公式

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

gemini-3.5-flash-thinking

概要

この問題は、与えられた区間 \([L, R]\) に含まれる整数のうち、隣り合う桁の差(段差)が右に進むにつれて単調非減少(等しいか大きくなる)であるものの個数を求める問題です。 桁数が最大 \(5000\) と非常に大きいため、愚直に1つずつ判定することは不可能です。巨大な数に対する数え上げ問題の定番である桁DP(Digit DP)を用いることで、高速に解くことができます。


考察

1. 範囲の言い換え

区間 \([L, R]\) における条件を満たす整数の個数を直接求めるのは難しいため、以下の性質を利用します。 $\((\text{区間 } [L, R] \text{ の個数}) = (\text{1以上 } R \text{ 以下の個数}) - (\text{1以上 } L \text{ 以下の個数}) + (\text{L自身が条件を満たすか (0 or 1)})\)\( これにより、ある整数 \)X\( に対し、「\)1\( 以上 \)X$ 以下のスムーズな山道の個数」を求める関数 solve(X) を設計することに集中できます。

2. 桁DPの設計

上の桁(左端)から順に数字を決めていく桁DPを考えます。 「スムーズな山道」であるかを判定するためには、以下の情報が必要です。 - 直前の桁にどの数字を置いたか(現在の桁との差を計算するため) - 直前の段差はいくつだったか(現在の段差が「直前の段差以上」であるかを判定するため)

また、桁DPの一般的な状態として以下が必要です。 - 現在決めている値が、上限 \(X\) 未満であることが確定しているか(less) - すでに \(1\) 以上の数字が始まっているか(is_start)。これは、先頭の余分な 0(リーディングゼロ)を無視するために不可欠です。

これらをまとめると、DPの状態(State)は次のように定義できます。

dp[less][is_start][prev_digit][prev_diff] - less: 上限未満が確定しているか(\(0\): 未確定、 \(1\): 確定) - is_start: 数値が始まっているか(\(0\): まだ(すべて 0)、 \(1\): 開始している) - prev_digit: 直前の桁の数字(\(0 \le \text{prev\_digit} \le 9\)) - prev_diff: 直前の段差(\(0 \le \text{prev\_diff} \le 9\)。まだ段差が存在しない場合は番兵として \(10\) とする)


アルゴリズム

DPの遷移

現在の桁に配置する数字を \(d\)\(0 \le d \le 9\))とします。

  1. まだ数値が始まっていない場合 (is_start == 0)

    • \(d = 0\) のとき: 数値は始まらないままです。状態は更新されず、直前の段差も存在しない状態(10)のままです。
    • \(d > 0\) のとき: ここから数値が始まります。next_is_start = 1 となり、直前の数字は \(d\)、直前の段差はまだ存在しないため 10 となります。
  2. すでに数値が始まっている場合 (is_start == 1)

    • 直前の数字 prev_digit と現在の数字 \(d\) の差(現在の段差)を \(e = |d - \text{prev\_digit}|\) とします。
    • もし prev_diff == 10(これが最初の段差)であれば、無条件で遷移可能です。次の直前段差は \(e\) になります。
    • もし prev_diff != 10 であれば、単調非減少の条件 \(e \ge \text{prev\_diff}\) を満たす場合のみ遷移可能です。次の直前段差は \(e\) になります。

初期状態

  • dp[0][0][0][10] = 1
  • 他の状態はすべて 0

答えの集計

すべての桁について遷移を行った後、is_start == 1(実際に存在する正の整数)である状態の総和が、求める個数となります。


計算量

時間計算量

  • 桁DPの遷移: 桁数を \(N\)(最大 \(5000\))とします。 DPの状態数は \(2 \times 2 \times 10 \times 11 = 440\) です。 各状態から次の桁の数字 \(d\)\(10\) 通り)への遷移を試みます。 したがって、1桁進めるためのループ回数は最大でも \(440 \times 10 = 4400\) 回です。 全体での遷移回数は \(O(N \times 2 \times 2 \times 10 \times 11 \times 10)\) となり、定数倍を考慮しても約 \(2.2 \times 10^7\) 回の演算で済み、実行時間制限に余裕で間に合います。
  • \(L\) の判定: \(L\) がスムーズな山道であるかの判定は \(O(N)\) で行えます。

よって、全体時間計算量は \(O(N)\) です。

空間計算量

DPの遷移は直前の桁の状態のみに依存するため、インプレース(または2つの配列を交互に使い回す方法)で実装できます。 必要な配列サイズは \(2 \times 2 \times 10 \times 11\) の定数サイズです。

よって、空間計算量は \(O(1)\)(または入力を保持するメモリを考慮して \(O(N)\))です。


実装のポイント

  • 引き算における負の数の余り: solve(R) - solve(L) を行う際、引き算の結果が負になることがあります。プログラミング言語の仕様(特に C++ や Java など)に合わせて、(ans_R - ans_L + is_L_smooth + MOD) % MOD のように MOD を足してから余りを取るようにします。

  • リーディングゼロの区別: 例えば 000123 のような数を考える際、先頭の 0 同士の差を段差として計算してしまうと誤判定の原因になります。必ず is_start フラグを用いて、実際の数が始まってからの段差のみを計算するように制御します。

    ソースコード

#include <iostream>
#include <string>
#include <vector>
#include <cmath>
#include <algorithm>
#include <cstring>

using namespace std;

const int MOD = 1000000007;

int dp[2][2][10][11];
int next_dp[2][2][10][11];

int solve(const string& S) {
    int N = S.length();
    memset(dp, 0, sizeof(dp));
    dp[0][0][0][10] = 1;

    for (int i = 0; i < N; ++i) {
        memset(next_dp, 0, sizeof(next_dp));
        int limit = S[i] - '0';
        for (int less = 0; less < 2; ++less) {
            for (int is_start = 0; is_start < 2; ++is_start) {
                for (int prev_digit = 0; prev_digit < 10; ++prev_digit) {
                    for (int prev_diff = 0; prev_diff <= 10; ++prev_diff) {
                        int val = dp[less][is_start][prev_digit][prev_diff];
                        if (val == 0) continue;

                        int max_d = less ? 9 : limit;
                        for (int d = 0; d <= max_d; ++d) {
                            int next_less = less || (d < limit);
                            int next_is_start = is_start;
                            int next_prev_digit = prev_digit;
                            int next_prev_diff = prev_diff;

                            if (is_start == 0) {
                                if (d == 0) {
                                    next_is_start = 0;
                                    next_prev_digit = 0;
                                    next_prev_diff = 10;
                                } else {
                                    next_is_start = 1;
                                    next_prev_digit = d;
                                    next_prev_diff = 10;
                                }
                            } else {
                                next_is_start = 1;
                                next_prev_digit = d;
                                int diff = abs(prev_digit - d);
                                if (prev_diff == 10) {
                                    next_prev_diff = diff;
                                } else {
                                    if (diff >= prev_diff) {
                                        next_prev_diff = diff;
                                    } else {
                                        continue;
                                    }
                                }
                            }

                            next_dp[next_less][next_is_start][next_prev_digit][next_prev_diff] += val;
                            if (next_dp[next_less][next_is_start][next_prev_digit][next_prev_diff] >= MOD) {
                                next_dp[next_less][next_is_start][next_prev_digit][next_prev_diff] -= MOD;
                            }
                        }
                    }
                }
            }
        }
        memcpy(dp, next_dp, sizeof(dp));
    }

    int ans = 0;
    for (int less = 0; less < 2; ++less) {
        for (int prev_digit = 0; prev_digit < 10; ++prev_digit) {
            for (int prev_diff = 0; prev_diff <= 10; ++prev_diff) {
                ans += dp[less][1][prev_digit][prev_diff];
                if (ans >= MOD) ans -= MOD;
            }
        }
    }
    return ans;
}

bool is_smooth(const string& S) {
    if (S.length() <= 2) return true;
    int prev_diff = -1;
    for (size_t i = 0; i < S.length() - 1; ++i) {
        int diff = abs((S[i] - '0') - (S[i+1] - '0'));
        if (prev_diff != -1 && diff < prev_diff) {
            return false;
        }
        prev_diff = diff;
    }
    return true;
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    string L, R;
    if (!(cin >> L >> R)) return 0;

    int ans_R = solve(R);
    int ans_L = solve(L);
    int is_L_smooth = is_smooth(L) ? 1 : 0;

    int ans = (ans_R - ans_L + is_L_smooth + MOD) % MOD;
    cout << ans << "\n";

    return 0;
}

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

投稿日時:
最終更新: