Official

E - 桁の積 / Product of Digits Editorial by admin

gemini-3.5-flash-thinking

概要

この問題は、与えられた範囲 \([L, R]\) の中から、各桁の数字の積が \(K\) になるような整数の個数を求める問題です。

桁数が最大で \(18\) 桁(\(10^{18}\))と非常に大きいため、愚直に \(L\) から \(R\) までループを回して判定することはできません。このような「ある範囲内の整数で、桁に関する条件を満たすものの個数を数える」問題では、桁DP(桁動的計画法)という手法が有効です。


考察

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

\(L\) 以上 \(R\) 以下の個数」を直接求めるのは難しいため、累積和の考え方を利用します。 \(g(N)\) を「\(1\) 以上 \(N\) 以下の整数のうち、各桁の積が \(K\) になるものの個数」と定義すると、求める答えは以下の式で表せます。

\[g(R) - g(L - 1)\]

これにより、上限 \(N\) だけを考慮する問題に単純化できます。

2. \(K\) の値による場合分け

各桁の数字の積 \(f(n)\)\(K\) になる条件を考えます。\(K\) の値によってアプローチが異なります。

パターンA: \(K = 0\) の場合

積が \(0\) になるということは、「少なくとも \(1\) つの桁に \(0\) が含まれる」ということです。 これは直接数えるよりも、余事象(逆のパターン)を考える方が簡単です。 「\(1\) 以上 \(N\) 以下のすべての整数」の個数は \(N\) 個です。ここから\(0\) を一度も含まない整数の個数」を引くことで、 \(K=0\) となる個数を求められます。

パターンB: \(K > 0\) の場合

積が \(K\) になるためには、各桁に \(0\) を含んではいけません(\(0\) があると積が \(0\) になってしまうため)。よって、各桁で選べる数字は \(1\) から \(9\) のいずれかになります。 また、各桁の数字(\(1 \sim 9\))の素因数は \(2, 3, 5, 7\) のみです。したがって、\(K\)\(2, 3, 5, 7\) 以外の素因数を持つ場合、どのように桁の数字を決めても積を \(K\) にすることはできません。 この場合は即座に答えが \(0\) になります。

\(K\)\(2, 3, 5, 7\) のみで構成されている場合は、桁を上から決めていきながら、現在の目標値 \(val\)(初期値は \(K\))をその桁の数字 \(d\) で割っていくことで、残りの桁で必要な積を管理できます。


アルゴリズム

桁DPをメモ化再帰(DFS)を用いて実装します。

状態の定義

再帰関数 dfs(idx, is_less, is_started, val) を定義します。

  • idx: 現在決めている桁のインデックス(上から idx 桁目)。
  • is_less: 現在決めている数が、すでに \(N\) 未満であることが確定しているかどうか(未満フラグ)。
    • False の場合、この桁で選べる数字の上限は \(N\) の同じ桁の数字になります。
    • True の場合、この桁では \(0 \sim 9\) のどの数字でも選べます。
  • is_started: すでに \(1\) 以上の数字を置き始めているかどうか(リーディングゼロ防止フラグ)。
    • 例えば \(N=123\) に対して \(25\) という数を考えるとき、上から \(1\) 桁目を「数がない(\(0\))」として扱いたいです。この「実質的な桁数の調整のための \(0\)」を置いている間は is_started = False とし、積の計算から除外します。
  • val: 残りの桁で達成すべき積の目標値。

遷移

各桁で数字 \(d\) を選ぶときの遷移は以下の通りです。

  1. まだ数字の決定が始まっていない場合 (is_started = False)

    • \(0\) を選ぶ:数字をまだ置かないことを意味します。is_startedFalse のままで、目標値 val も変化させずに次の桁へ進みます。
    • \(1 \sim limit\) を選ぶ:ここから数字の決定が始まります。is_startedTrue にし、val\(d\) で割った値(\(val\)\(d\) で割り切れる場合のみ)を次の目標値として進みます。
  2. すでに数字の決定が始まっている場合 (is_started = True)

    • \(0\) は選べません(積が \(0\) になってしまうため)。
    • \(1 \sim limit\) のうち、現在の val を割り切れる数字 \(d\) を選び、目標値を val // d に更新して次の桁へ進みます。

最後の桁まで到達したとき、is_startedTrue かつ val == 1 であれば、条件を満たす数を \(1\) つ作れたことになるので 1 を返し、そうでなければ 0 を返します。


計算量

  • 時間計算量: \(O(D \times d(K))\)

    • \(D\)\(N\) の桁数(\(D \le 18\))です。
    • \(d(K)\)\(K\) の約数の個数です。
    • \(K \le 10^{18}\) かつ素因数が \(2, 3, 5, 7\) のみに限られるとき、約数の個数は最大でも \(5 \times 10^4\) 程度に収まります。さらに、実際に探索で到達可能な状態数はこれより遥かに少ないため、実行時間制限(通常 2.0 秒)に対して非常に高速(数十ミリ秒程度)に動作します。
  • 空間計算量: \(O(D \times d(K))\)

    • メモ化テーブル(連想配列 memo)に保存される状態数に比例します。時間計算量と同様に十分に小さく、メモリ制限を余裕でクリアできます。

実装のポイント

  1. 再帰の上限変更: Pythonでは再帰の深さ制限の初期値が小さいため、sys.setrecursionlimit(2000) で上限を増やしておきます。

  2. メモ化の工夫: is_lessFalse の状態(\(N\) と同じ数字をなぞっている状態)は、探索全体で各桁につき高々1回しか通りません。そのため、is_less = True\(N\) 未満が確定している状態)のときのみメモ化テーブルに記録・参照することで、無駄なメモリ使用と連想配列の検索コストを削減しています。

  3. 高速な素因数判定: \(K > 0\) のとき、あらかじめ \(2, 3, 5, 7\) で割れるだけ割り、残った値が \(1\) より大きければ、構成不可能として即座に 0 を返す枝刈りを行っています。

    ソースコード

import sys

# 再帰上限の変更
sys.setrecursionlimit(2000)

def solve_no_zero(N):
    if N < 1:
        return 0
    S = str(N)
    D = len(S)
    memo = {}
    
    def dfs(idx, is_less, is_started):
        if idx == D:
            return 1 if is_started else 0
        
        if is_less and (idx, is_started) in memo:
            return memo[(idx, is_started)]
        
        limit = 9 if is_less else int(S[idx])
        res = 0
        
        if not is_started:
            # 0 を選ぶ
            res += dfs(idx + 1, True, False)
            # 1 ~ limit を選ぶ
            for d in range(1, limit + 1):
                res += dfs(idx + 1, is_less or (d < limit), True)
        else:
            # 0 は選べない
            for d in range(1, limit + 1):
                res += dfs(idx + 1, is_less or (d < limit), True)
                
        if is_less:
            memo[(idx, is_started)] = res
        return res
        
    return dfs(0, False, False)

def solve(N, K):
    if N < 1:
        return 0
    if K == 0:
        return N - solve_no_zero(N)
    
    # K > 0 の場合、2, 3, 5, 7 以外の素因数を持っていれば構成不可
    temp = K
    for p in (2, 3, 5, 7):
        while temp % p == 0:
            temp //= p
    if temp > 1:
        return 0
    
    S = str(N)
    D = len(S)
    memo = {}
    
    def dfs(idx, is_less, is_started, val):
        if idx == D:
            return 1 if (is_started and val == 1) else 0
        
        if is_less and (idx, is_started, val) in memo:
            return memo[(idx, is_started, val)]
        
        limit = 9 if is_less else int(S[idx])
        res = 0
        
        if not is_started:
            # 0 を選ぶ
            res += dfs(idx + 1, True, False, val)
            # 1 ~ limit を選ぶ
            for d in range(1, limit + 1):
                if val % d == 0:
                    res += dfs(idx + 1, is_less or (d < limit), True, val // d)
        else:
            # 0 は選べない(積が 0 になってしまうため)
            for d in range(1, limit + 1):
                if val % d == 0:
                    res += dfs(idx + 1, is_less or (d < limit), True, val // d)
                    
        if is_less:
            memo[(idx, is_started, val)] = res
        return res

    return dfs(0, False, False, K)

def main():
    input = sys.stdin.read
    data = input().split()
    if not data:
        return
    L = int(data[0])
    R = int(data[1])
    K = int(data[2])
    
    ans = solve(R, K) - solve(L - 1, K)
    print(ans)

if __name__ == '__main__':
    main()

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

posted:
last update: