公式

E - バランスチェック / Balance Check 解説 by admin

gemini-3.5-flash-thinking

概要

この問題は、\(1\) 以上 \(N\) 以下の整数のうち、奇数番目の桁の数字の合計 \(S_{\mathrm{odd}}\) と、偶数番目の桁の数字の合計 \(S_{\mathrm{even}}\) の差の絶対値が \(D\) 以下であるものの個数を求める問題です。

\(N\) が最大で \(10^{15}\) と非常に大きいため、1つずつ条件を満たすか判定する素朴な方法では間に合いません。桁の上限 \(N\) を意識しながら、上の桁から順に数字を決めていく桁DP(動的計画法)を用いることで、高速に解くことができます。


考察

1. なぜ素朴な探索ではダメなのか?

\(1\) から \(N\) までのすべての整数をループで走査し、各整数の桁を調べて判定する場合、最悪で \(10^{15}\) 回のループが必要になります。一般的なコンピュータが1秒間に実行できる計算回数は \(10^8\) 回程度であるため、この方法(全探索)では実行時間制限(TLE)になってしまいます。

2. 桁DPによる解決

\(N\) 以下の整数」という条件で数を数え上げる問題では、「上の桁から順番に数字を決定していく」というアプローチが非常に有効です。これが桁DPです。

しかし、この問題では以下の2点に注意する必要があります。

① リーディングゼロ(先頭の 0)の扱い

例えば、全体の桁数が \(5\) 桁のとき、数値 12300123 として表現したとします。 もし先頭の 0 をそのままカウントしてしまうと、0(1番目・奇数)、0(2番目・偶数)、1(3番目・奇数)、2(4番目・偶数)、3(5番目・奇数)となり、1 が奇数番目の桁になってしまいます。 本来、123 の奇数番目の桁は 13、偶数番目は 2 です。 このように、「実際に数字が始まってから何番目の桁か」を正しく判定するために、「まだ数字が始まっていない(すべて 0)」状態と「すでに数字が始まっている」状態を区別する必要があります。

② 奇数番目か偶数番目かの状態

数字が始まっている場合、現在決めている桁が「奇数番目」か「偶数番目」かによって、その桁の数字を差に足す(\(S_{\mathrm{odd}}\) に加える)か、引く(\(S_{\mathrm{even}}\) に加える、すなわち差を引く)かが変わります。したがって、現在の桁の奇偶(パリティ)を状態として持つ必要があります。

③ 差の管理とオフセット

奇数番目の桁の和と偶数番目の桁の和の差 \(S_{\mathrm{odd}} - S_{\mathrm{even}}\) を DP の状態として持ちます。 \(N \le 10^{15}\) より、最大桁数は \(15\) 桁です。各桁の数字は最大で \(9\) なので、差の最大値は \(15 \times 9 = 135\)、最小値は \(-135\) となります。 配列のインデックスに負の数は使えないため、基準値(オフセット)として \(150\) 程度を足すことで、差を \([0, 300]\) の範囲の正の整数として安全に管理できます。


アルゴリズム

DP の状態定義

各桁を上から決めていく際、以下の状態を保持します。

  • is_less: 現在決めている値が、\(N\) 未満であることが確定しているか(0: 未確定, 1: 確定)
  • is_started: すでに \(1\) 以上の数字が現れて、数の表記が始まっているか(0: 始まっていない, 1: 始まっている)
  • parity: 次に決める桁が、開始から数えて何番目か(0: 奇数番目, 1: 偶数番目)
  • diff: 現在の「奇数番目の和 - 偶数番目の和」にオフセット(\(150\))を加えた値

これらをまとめた状態 dp[is_less][is_started][parity][diff] を更新していきます。

状態遷移

上の桁から順に、次の桁の数字 \(d\)\(0 \le d \le 9\))を決定していきます。

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

    • \(d = 0\) のとき: まだ数字は始まりません。状態は変化せず、diff もそのままです。
    • \(d > 0\) のとき: ここから数字が始まります。これが「1番目(奇数番目)」の桁になるので、diff\(d\) を加算し、次の桁は「偶数番目 (parity = 1)」になります。また is_started = 1 に変化します。
  2. すでに数字が始まっている場合 (is_started == 1)

    • parity == 0(次の桁が奇数番目)のとき: diff\(d\) を加算し、次の桁は「偶数番目 (parity = 1)」になります。
    • parity == 1(次の桁が偶数番目)のとき: diff から \(d\) を減算し、次の桁は「奇数番目 (parity = 0)」になります。

最終的な集計

すべての桁を決定し終えたあと、以下の条件を満たす状態の総和が答えになります。 - is_started == 1(正の整数である) - diff(オフセット適用後)が、許容される差 \(D\) の範囲内にある(すなわち、\(\mathrm{OFFSET} - D \le \mathrm{diff} \le \mathrm{OFFSET} + D\)


計算量

時間計算量

  • \(N\) の桁数を \(L\) とします(\(1 \le L \le 15\))。
  • DP の状態数は \(L \times 2 \times 2 \times 2 \times \mathrm{MAX\_DIFF}\) です。 ここで、\(\mathrm{MAX\_DIFF} = 301\) とします。
  • 各状態からの遷移(次の桁の数字 \(d\) の選択肢)は最大 \(10\) 通りです。
  • したがって、全体の計算量は \(O(L \times \mathrm{MAX\_DIFF} \times 10)\) となります。 \(L = 15\) のとき、計算回数は \(15 \times 8 \times 301 \times 10 \approx 3.6 \times 10^5\) 回となり、実行時間制限(通常2.0秒)に対して十分に高速(数ミリ秒)で動作します。

空間計算量

  • DP テーブルは次の桁の計算をする際に「1つ前の桁の状態」だけが必要となるため、テーブルを使い回すことができます。
  • したがって、必要な空間計算量は \(O(\mathrm{MAX\_DIFF})\) となり、メモリをほとんど消費しません。

実装のポイント

  1. 1次元配列へのエンコードによる高速化 Pythonなどの言語では、多次元リスト(多重の list)へのアクセスはオーバーヘッドが大きくなります。正解コードでは、多次元の状態 (is_less, is_started, parity, diff) を1つの整数にエンコードし、サイズが STATE_SIZE の1次元配列として管理することで、劇的な高速化を図っています。
   # 状態を1つのインデックスにマッピングする
   state = ((is_less * 2 + is_started) * 2 + parity) * MAX_DIFF + diff
  1. オフセットの適切な設定 差の最小値は \(-135\)、最大値は \(135\) です。余裕を持って OFFSET = 150 と設定し、MAX_DIFF = 301 とすることで、すべての差の値を \(0\) 以上のインデックス(\([15, 285]\))に収めています。

    ソースコード

import sys

def solve():
    # Read all inputs from standard input
    input_data = sys.stdin.read().split()
    if not input_data:
        return
    N_str = input_data[0]
    D = int(input_data[1])
    
    L = len(N_str)
    OFFSET = 150
    MAX_DIFF = 301
    STATE_SIZE = 2 * 2 * 2 * MAX_DIFF  # 2408 states
    
    # dp[state] where state is encoded as:
    # ((is_less * 2 + is_started) * 2 + parity) * MAX_DIFF + diff
    dp = [0] * STATE_SIZE
    
    # Initial state: is_less=0, is_started=0, parity=0, diff=OFFSET
    initial_state = ((0 * 2 + 0) * 2 + 0) * MAX_DIFF + OFFSET
    dp[initial_state] = 1
    
    for char in N_str:
        limit = int(char)
        next_dp = [0] * STATE_SIZE
        
        for is_less in range(2):
            for is_started in range(2):
                for parity in range(2):
                    base_idx = ((is_less * 2 + is_started) * 2 + parity) * MAX_DIFF
                    for diff in range(MAX_DIFF):
                        count = dp[base_idx + diff]
                        if count == 0:
                            continue
                        
                        max_d = 9 if is_less else limit
                        for d in range(max_d + 1):
                            next_is_less = is_less or (d < limit)
                            
                            if is_started == 0:
                                if d == 0:
                                    next_is_started = 0
                                    next_parity = 0
                                    next_diff = diff
                                else:
                                    next_is_started = 1
                                    next_parity = 1  # Next digit is the 2nd digit (even index)
                                    next_diff = diff + d
                            else:
                                next_is_started = 1
                                if parity == 0:  # Odd-positioned digit
                                    next_diff = diff + d
                                    next_parity = 1
                                else:  # Even-positioned digit
                                    next_diff = diff - d
                                    next_parity = 0
                            
                            next_state = ((next_is_less * 2 + next_is_started) * 2 + next_parity) * MAX_DIFF + next_diff
                            next_dp[next_state] += count
        dp = next_dp
        
    ans = 0
    # Sum up valid states where is_started = 1 and |diff - OFFSET| <= D
    for is_less in range(2):
        for parity in range(2):
            base_idx = ((is_less * 2 + 1) * 2 + parity) * MAX_DIFF
            for diff in range(OFFSET - D, OFFSET + D + 1):
                if 0 <= diff < MAX_DIFF:
                    ans += dp[base_idx + diff]
                    
    print(ans)

if __name__ == '__main__':
    solve()

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

投稿日時:
最終更新: