競プロ(Python)で巨大な数を扱うときは注意しよう(自戒)

普段競技プログラミングPythonを使っているのですが、想定解(?)でもTLEになる場合があります。 その時は定数倍で高速化するらしいのですが、ちょっと特殊(?)なパターンに遭遇したので備忘録。

結論

  • Pythonのintはデフォルトで28byteだよ
  • これを超えるとメモリを再確保するよ
  • 毎回やってるとおっそいよ

詳細

やらかした問題はTypical DP ContestのE-数です。 典型的な桁DPで、

dp[i][smaller][mod] := ( i 番目まで選んだ時の答え(Dの倍数) )
ただし、smallerNより小さいかどうかのフラグ、modはDで割ったときの余りを示す。

とすれば解けます。

自分が提出したソースコードはこんな感じ。

MOD = 1000000007  
D = int(input())
N = [int(x) for x in list(input())]
dp = [[[ 0 for _ in range(D) ] for _ in range(2) ] for _ in range(len(N)+1)]

dp[0][0][0] = 1
for i in range(len(N)):
    max_lim = int(N[i])
    for l in range(2):
        for m in range(D):
            lim = 9 if l else N[i]
            lim = 9 if l else max_lim
            for d in range(lim+1):
                if l:
                    dp[i+1][l][(m+d) % D] += dp[i][l][m]
                elif d < lim:
                    dp[i+1][1][(m+d) % D] += dp[i][l][m]
                else:
                    dp[i+1][l][(m+d) % D] += dp[i][l][m]

print((dp[len(N)][0][0] + dp[len(N)][1][0] -1) % MOD)

オーダー的にはO(nD)(nはNの桁数)になるので、問題ないはずです。 が、制約上限の値を入力するとPyPyで提出してもTLEになってしまいます。 しょうがなくC++で再実装を行っているときに、オーバーフローを考慮した実装を行ったことでようやく気づきました…。 巨大な数値をそのまま扱っているせいだ、と。

Pythonのintはデフォルトで28Byteですが、メモリが許す限り拡張し続けます。 演算するたびに新しくメモリを確保…を繰り返していたらそりゃ遅いです。 なので、dpを更新するたびにmodを取ることにしました。 修正後のコードはこんな感じ(一部抜粋)

(~~略~~)
for d in range(lim+1):
if l:
    dp[i+1][l][(m+d) % D] += dp[i][l][m]
        dp[i+1][l][(m+d) % D] %= MOD
elif d < lim:
    dp[i+1][1][(m+d) % D] += dp[i][l][m]
        dp[i+1][l][(m+d) % D] %= MOD
else:
    dp[i+1][l][(m+d) % D] += dp[i][l][m]
        dp[i+1][l][(m+d) % D] %= MOD
(~~略~~)

TLEだったのが最大300ms強になりAC。ちなみにローカルで実行したら半分くらいの時間になりました。

研究でも競プロでも個人的な開発でもPythonを多用しているので、制限なしintに甘えてましたね……今後気を付けたいと思います。