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とし、積の計算から除外します。
- 例えば \(N=123\) に対して \(25\) という数を考えるとき、上から \(1\) 桁目を「数がない(\(0\))」として扱いたいです。この「実質的な桁数の調整のための \(0\)」を置いている間は
val: 残りの桁で達成すべき積の目標値。
遷移
各桁で数字 \(d\) を選ぶときの遷移は以下の通りです。
まだ数字の決定が始まっていない場合 (
is_started = False)- \(0\) を選ぶ:数字をまだ置かないことを意味します。
is_startedはFalseのままで、目標値valも変化させずに次の桁へ進みます。 - \(1 \sim limit\) を選ぶ:ここから数字の決定が始まります。
is_startedをTrueにし、valを \(d\) で割った値(\(val\) が \(d\) で割り切れる場合のみ)を次の目標値として進みます。
- \(0\) を選ぶ:数字をまだ置かないことを意味します。
すでに数字の決定が始まっている場合 (
is_started = True)- \(0\) は選べません(積が \(0\) になってしまうため)。
- \(1 \sim limit\) のうち、現在の
valを割り切れる数字 \(d\) を選び、目標値をval // dに更新して次の桁へ進みます。
最後の桁まで到達したとき、is_started が True かつ 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)に保存される状態数に比例します。時間計算量と同様に十分に小さく、メモリ制限を余裕でクリアできます。
- メモ化テーブル(連想配列
実装のポイント
再帰の上限変更: Pythonでは再帰の深さ制限の初期値が小さいため、
sys.setrecursionlimit(2000)で上限を増やしておきます。メモ化の工夫:
is_lessがFalseの状態(\(N\) と同じ数字をなぞっている状態)は、探索全体で各桁につき高々1回しか通りません。そのため、is_less = True(\(N\) 未満が確定している状態)のときのみメモ化テーブルに記録・参照することで、無駄なメモリ使用と連想配列の検索コストを削減しています。高速な素因数判定: \(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: