問題
奇数の法 $M$ と $Q$ 個のクエリが与えられる。各クエリは整数 $A,E$($0\le A
法$M$を「2のべき乗$R$」を使った特殊な形(モンゴメリ形式)$\tilde a=a\cdot R\bmod M$に変換しておくことで、以降の乗算をすべて「乗算とビットシフト」だけで行える。$R$を法とする剰余計算がシフトで済むため、除算命令を一切使わずに$a\times b\bmod M$を計算できる。低レベル言語での高速な多倍長mod演算や暗号ライブラリの内部実装で広く使われる実用的技法。
入力形式
M Q
A_1 E_1
:
A_Q E_Q
制約
$3 \le M \le 10^{18}$($M$は奇数)
$1 \le Q \le 10^5$
$0 \le A_i < M$
$0 \le E_i \le 10^{18}$
入出力例
入力例1
1000000007 2
3 5
2 100
出力例1
243
976371285
$3^5=243$。$2^{100}\bmod 1000000007=976371285$。
概念図
ヒント(段階的開示)
ヒント1: 方向性
$a\times b\bmod m$はPythonでは`(a*b)%m`で一瞬だが、内部的には大きな数の除算を行っている。法$M$を「2のべき乗」に関連づけて考えると、シフト演算だけで済む場面が作れることに気づけるかがカギ。
ヒント2: アプローチ
$M$より大きい2のべき乗$R=2^k$を選ぶ($M$が奇数なので$\gcd(R,M)=1$は自動的に成立)。整数$a$を「モンゴメリ形式」$\tilde a=a\cdot R\bmod M$に変換しておくと、2つのモンゴメリ形式の積$\tilde a\cdot\tilde b$を「$R$で割って$\bmod M$する」処理(REDC)だけで正しく$\widetilde{a\cdot b}$が得られる。この「$R$で割る」操作がビットシフトのみで実現できる点が核心。$M'=-M^{-1}\bmod R$を前計算しておく。
ヒント3: 誘導(コード骨格)
r_bits = M.bit_length() + 1
R = 1 << r_bits
mask = R - 1
M_inv = pow(M, -1, R)
M_prime = (-M_inv) & mask
R2 = (R * R) % M
def redc(t):
s = ((t & mask) * M_prime) & mask
u = (t + s * M) >> r_bits
if u >= M:
u -= M
return u
def mont_mul(aR, bR):
return redc(aR * bR)
繰り返し二乗法と組み合わせれば`pow(a,e,m)`と同じ結果を除算なしで計算できる。
模範解答 (Python)
import sys
def solve():
data = sys.stdin.buffer.read().split()
idx = 0
m = int(data[idx]); idx += 1
q = int(data[idx]); idx += 1
r_bits = m.bit_length() + 1
r = 1 << r_bits
mask = r - 1
shift = r_bits
m_inv = pow(m, -1, r) # m * m_inv ≡ 1 (mod R)
m_prime = (-m_inv) & mask # m * m_prime ≡ -1 (mod R)
r2 = (r * r) % m # R^2 mod M(モンゴメリ形式への変換用)
def redc(t):
# 0 <= t < R*M を仮定し、t*R^{-1} mod M を除算なしで計算する
s = ((t & mask) * m_prime) & mask
u = (t + s * m) >> shift
if u >= m:
u -= m
return u
def mont_mul(ar, br):
return redc(ar * br)
out = []
for _ in range(q):
a = int(data[idx]); idx += 1
e = int(data[idx]); idx += 1
ar = redc((a % m) * r2) # aをモンゴメリ形式に変換
result_r = redc(r2) # 1のモンゴメリ形式 = R mod M = redc(R^2)
while e > 0:
if e & 1:
result_r = mont_mul(result_r, ar)
ar = mont_mul(ar, ar)
e >>= 1
out.append(str(redc(result_r))) # モンゴメリ形式から通常表現に戻す
print("\n".join(out))
solve()
計算量: $O(Q\log E)$(各クエリで$O(\log E)$回のモンゴメリ乗算、各乗算は$O(1)$の多倍長演算)。
Step-by-Step 解説
1パラメータの前計算
$R=2^k$($M$より大きい)を選び、$M'=-M^{-1}\bmod R$を`pow(m,-1,r)`で求める($M$に依存し使い回せる)。
$R=2^k$($M$より大きい)を選び、$M'=-M^{-1}\bmod R$を`pow(m,-1,r)`で求める($M$に依存し使い回せる)。
2REDC関数
`redc(t)`は$0\le t
`redc(t)`は$0\le t
3変換と逆変換
$a$のモンゴメリ形式化は`redc(a*R2)`、逆変換は`redc(ar)`を1回呼ぶだけでよい。
$a$のモンゴメリ形式化は`redc(a*R2)`、逆変換は`redc(ar)`を1回呼ぶだけでよい。
4モンゴメリ形式での繰り返し二乗法
`mont_mul`を使って通常の繰り返し二乗法を回し、最後に結果を通常表現へ戻す。
`mont_mul`を使って通常の繰り返し二乗法を回し、最後に結果を通常表現へ戻す。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| $R$を$M$より小さい2のべき乗にしてしまう | $R>M$かつ$\gcd(R,M)=1$がREDCの前提条件 | $R$のビット数は`M.bit_length()+1`程度、確実に$M$を超えるようにする |
| $M$が偶数のケースを想定してしまう | $R$(2のべき乗)と$M$が互いに素である必要がある | 本問では$M$は奇数と制約されているが、一般化時は奇数性を確認する |
| モンゴメリ形式への変換を忘れて生の値のまま演算する | 非モンゴメリ形式同士の積は正しい結果にならない | 冪乗の前に必ず$a$と$1$をモンゴメリ形式に変換する |
| 最終結果をモンゴメリ形式のまま出力してしまう | `redc`による逆変換をし忘れる | ループ終了後、必ず`redc(result_r)`で通常表現に戻す |
次のステップ
- 発展: 固定長整数レジスタ上(C/Rust等)でのモンゴメリ乗算実装によるさらなる高速化
- 発展: バレット還元(Barrett Reduction)との比較実装・使い分け($M$が毎回変わる場合はバレット還元が有利なことが多い)
- 次回予告: 最大空き長方形(Largest Empty Rectangle・座標圧縮 + スイープライン)