Official

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

Claude 4.6 Opus (Thinking)

Overview

This problem asks us to count positive integers from \(1\) to \(N\) where the absolute difference between the sum of digits at odd positions and the sum of digits at even positions is at most \(D\). We solve it using Digit DP.

Analysis

Problems with the Naive Approach

Since \(N\) can be up to \(10^{15}\), checking every integer from \(1\) to \(N\) one by one takes \(O(N)\) time, which is far too slow.

Key Insight

The problem of “counting positive integers up to \(N\) that satisfy a condition related to their digits” is a classic pattern for Digit DP. We determine digits one by one from the most significant digit, managing the following information as state:

  • The current digit position being considered
  • The current value of \(S_{\mathrm{odd}} - S_{\mathrm{even}}\) (tracking the difference)
  • Whether we are still constrained by the corresponding digit of \(N\) (tight constraint)
  • Whether the number has started (to ignore leading zeros)
  • Whether the next digit to place is at an odd or even position (parity)

Tracking the Difference

Instead of managing \(S_{\mathrm{odd}}\) and \(S_{\mathrm{even}}\) separately, it suffices to track only the difference \(\mathrm{diff} = S_{\mathrm{odd}} - S_{\mathrm{even}}\). This is because we only need to check \(|\mathrm{diff}| \leq D\) at the end.

Since \(N\) has at most 16 digits, there are at most 8 odd-positioned digits and at most 8 even-positioned digits. Therefore, \(\mathrm{diff}\) is bounded within \([-72, 72]\).

Algorithm

We implement Digit DP using recursion with memoization.

  1. Convert \(N\) to a list of its decimal digits.
  2. Define a recursive function dp(pos, diff, tight, started, parity):
    • pos: The current digit position being determined (index in \(N\)’s digit sequence)
    • diff: The current value of \(S_{\mathrm{odd}} - S_{\mathrm{even}}\) determined so far
    • tight: Whether we are still bounded by \(N\)’s upper limit (if True, the next digit is restricted to at most the corresponding digit of \(N\))
    • started: Whether valid digits have started yet (for handling leading zeros)
    • parity: Whether the next digit to place is at an odd position (0) or even position (1)
  3. At each digit position, try all digits from \(0\) to the upper limit, updating diff accordingly:
    • If it’s an odd-positioned digit: diff += d
    • If it’s an even-positioned digit: diff -= d
  4. Pruning: Let \(r\) be the number of remaining digits. If \(|\mathrm{diff}| - 9r > D\), then it is impossible to satisfy the condition no matter what, so we skip.
  5. When all digits have been determined (pos == n), return 1 if started is true and \(|\mathrm{diff}| \leq D\).

Complexity

  • Time complexity: \(O(n \times W \times 2 \times 2 \times 2 \times 10)\)
    • \(n\): Number of digits (at most 16)
    • \(W\): Number of possible values for diff (approximately 145)
    • \(2 \times 2 \times 2\): Combinations of tight, started, parity
    • \(10\): Number of digits tried at each position
    • Total: approximately \(16 \times 145 \times 8 \times 10 \approx 185{,}600\) states × transitions
  • Space complexity: \(O(n \times W \times 2 \times 2 \times 2)\) (memoization table)

Implementation Notes

  • Handling leading zeros: Managed with the started flag. While there are leading zeros, we consider the number as not having started yet, and do not update diff or parity. When a non-zero digit first appears, it is always treated as an odd-positioned (1st) digit.

  • Parity management: The “which position a digit is at” in a number depends on the length of the number itself, so it must be tracked based on the count since the number actually started, not based on the overall digit position including leading zeros.

  • Pruning: Speeds up computation by early elimination of states where recovery is impossible with the remaining digits.

  • Using lru_cache: In Python, functools.lru_cache allows for a concise implementation of memoized recursion.

    Source Code

import sys
from functools import lru_cache

def solve():
    N = int(input())
    D = int(input())
    
    digits = list(map(int, str(N)))
    n = len(digits)
    
    # Digit DP
    # State: position, diff (S_odd - S_even), tight, started
    # diff can range from -9*8 = -72 to 9*8 = 72 for up to 16 digits
    # Actually for 16 digits: max 8 odd positions * 9 = 72, similarly even
    # diff range: -9*8 to 9*8 = -72 to 72
    # But we need to handle up to 16 digits (10^15 has 16 digits)
    # Max odd positions: 8, max even positions: 8 -> diff in [-72, 72]
    
    # We'll use memoization with offset for diff
    # diff = S_odd - S_even
    # At position i (0-indexed), if i is even (0,2,4,...) it's odd-numbered (1st,3rd,...) -> add to S_odd
    # if i is odd (1,3,5,...) it's even-numbered (2nd,4th,...) -> add to S_even
    # But position indexing depends on the actual number's length when started
    # 
    # Actually, we need to track the actual digit position within the number being formed.
    # When the number hasn't started yet (leading zeros), we don't count those positions.
    # So we need to track how many actual digits have been placed.
    
    # State: pos in the template, number of actual digits placed so far, diff, tight
    # number of actual digits: 0 to 16
    # diff: -72 to 72 -> offset by 72, range 0 to 144
    
    # Actually, let's think differently. We track pos, tight, started, and diff.
    # When we place a non-zero digit and haven't started, we start. The first actual digit is at odd position (1st).
    # After that, each subsequent digit alternates.
    # Instead of tracking num_digits, we can track the parity of the next digit position.
    # parity=0 means next digit goes to odd position (adds to diff), parity=1 means even position (subtracts from diff)
    
    OFFSET = 130  # enough offset
    MAX_DIFF = 260
    
    @lru_cache(maxsize=None)
    def dp(pos, diff, tight, started, parity):
        # parity: 0 = next actual digit is odd-positioned, 1 = even-positioned
        if pos == n:
            if not started:
                return 0
            return 1 if abs(diff) <= D else 0
        
        limit = digits[pos] if tight else 9
        result = 0
        
        for d in range(0, limit + 1):
            new_tight = tight and (d == limit)
            
            if not started and d == 0:
                # Still not started
                result += dp(pos + 1, 0, new_tight, False, 0)
            else:
                # Place digit d
                new_started = True
                if not started:
                    # First digit, it's at odd position (parity=0)
                    new_parity = 1  # next will be even position
                    new_diff = diff + d  # odd position: add
                else:
                    if parity == 0:
                        # odd position: add
                        new_diff = diff + d
                    else:
                        # even position: subtract
                        new_diff = diff - d
                    new_parity = 1 - parity
                
                # Pruning: remaining positions
                remaining = n - pos - 1
                # max change possible is 9 * remaining
                # if diff is already too far from 0, prune
                if abs(new_diff) - 9 * remaining > D:
                    continue
                
                result += dp(pos + 1, new_diff, new_tight, new_started, new_parity)
        
        return result
    
    print(dp(0, 0, True, False, 0))

solve()

This editorial was generated by claude4.6opus-thinking.

posted:
last update: