少女祈祷中...

DeMen Blog #3: Lũy thừa nhanh và hơn thế nữa!

Tác giả: Võ Khắc Triệu (DeMen100ns)

Kiến thức cần biết

Đặt vấn đề

Xét bài toán sau:

Bài toán

Tính: (ab) modulo m(a^b)\ modulo\ m (0a<m109,0b1018)(0 \le a < m \le 10^9, 0 \le b \le 10^{18})

Thuật toán ngây thơ

Đơn giản, ta sẽ vét cạn, khá dễ dàng nhưng cực kỳ chậm.

Độ phức tạp: O(b)O(b).

Code:

int ans = 1;
for(int i = 1; i <= b; ++i){
ans = (ans * 1ll * a) % m; //nhớ chuyển thành long long để không bị tràn số.
}
cout << ans;

Thuật toán lũy thừa nhanh 1 (Dùng chia để trị)

Nhận xét:

  • Với nn chẵn: ab=(ab2)2a^b = (a^{\lfloor \frac{b}{2} \rfloor})^2
  • Với nn lẻ: ab=(ab2)2×aa^b = (a^{\lfloor \frac{b}{2} \rfloor})^2 \times a

Như vậy ta chỉ cần tính aba^b bằng ab2a^{\lfloor \frac{b}{2} \rfloor}, và bb sẽ giảm một nửa liên tục cho đến khi còn b=1b = 1a1=aa^1 = a. Dễ thấy bb chỉ giảm log2b\log_2{b} lần.

Độ phức tạp: O(log2b)O(\log_2{b})

Code:

int f(int a, int b, int m){
if (b == 0) return 1;
if (b == 1) return a;
int f2 = f(a, b / 2, m);
int ans = (f2 * 1ll * f2) % m;
if (b % 2 == 1){
ans = (ans * 1ll * a) % m;
}
return ans;
}
int ans = f(a, b, m);
cout << ans;

Thuật toán lũy thừa nhanh 2 (Dùng biểu diễn nhị phân)

Nhận xét: Ta có thể tính a1,a2,a4,a8,,a(2k)a^1, a^2, a^4, a^8, \dots, a^{(2^k)} trong O(k)O(k), vì: a(2k)=(a(2k1))2a^{(2^k)} = (a^{(2^{k-1})})^2. (1)(1)

Ta cũng biết rằng, mỗi số tự nhiên nn có thể được phân tách dưới dạng: 2p1+2p2++2pk(p1<p2<<pk)2^{p_1} + 2^{p_2} + \dots + 2^{p_k} (p_1 < p_2 < \dots < p_k), với klog2nk \le \log_2{n}. Ta dễ dàng xác định các biến pip_i thông qua biểu diễn nhị phân của nn.

Như vậy, ta có thể tính aba^b nhanh bằng cách tách bb thành các pip_i theo biểu diễn nhị phân như trên rồi nhân các apia^{p_i} vào với nhau. Các apia^{p_i} đã được tính ở bước (1)(1) trong O(k)O(k).

Ví dụ: Tính aba^b với b=13b = 13. Xét biểu diễn nhị phân của b=13:1101b = 13: 1101. b=13=23+22+20=8+4+1\rightarrow b = 13 = 2^3 + 2^2 + 2^0 = 8 + 4 + 1. ab=a13=a8×a4×a1\rightarrow a^b = a^{13} = a^8 \times a^4 \times a^1.

Độ phức tạp: O(log2b)O(\log_2{b})

int ans = 1, pw = a;
for(int i = 0; i < 30; ++i){
if (b >> i & 1){ //bit i cua b bat
ans = (ans * 1ll * pw) % m;
}
pw = (pw * 1ll * pw) % m;
}
cout << ans;

Mở rộng

Thực tế là, với mọi hàm ff thỏa:

  • f(a+b)=f(a)f(b)f(a + b) = f(a) \circ f(b), với \circ là một phép toán tử bất kỳ nào đó.

Thì bạn luôn tính được f(n)f(n) trong O(Tlog2n)O(T\log_2{n}), với O(T)O(T) là độ phức tạp của phép toán tử \circ.

Cụ thể, tương tự với lũy thừa nhanh, bạn có thể tính f(n)f(n) bằng cách tính trước f(1),f(2),f(4),,f(2k)f(1), f(2), f(4), \dots, f(2^k), rồi tính f(n)f(n) bằng cách biểu diễn nn thành dạng nhị phân rồi gộp vào.

Thực tế, nếu ta coi f(i)=aif(i) = a^i thì bài toán sẽ trở thành bài toán tính lũy thừa nhanh.

Có một số blog gọi đây là x2 +1 trick, thường dùng để tối ưu các hàm quy hoạch động.

Áp dụng

Bài: Olympic Sinh Viên 2022 - Chuyên tin - Khôi phục dữ liệu

Tóm tắt: Đếm số cách chọn ba xâu nhị phân A,B,CA, B, C độ dài mm thỏa:

  • Có ít nhất một xâu có bit được bật.
  • Tổng số bit bật của ba xâu chia hết cho kk.
  • Không tồn tại vị trí nào bật bit trên cả ba xâu.

In ra đáp án modulo 109+7.modulo\ 10^9 + 7.

Limit: 1m5×108,1k1001 \le m \le 5 \times 10^8, 1 \le k \le 100.

Lời giải quy hoạch động:

Gọi dpi,jdp_{i, j} là số cách chọn ba xâu nhị phân A,B,CA, B, C thỏa điều kiện và có tổng bit bật mod kmod\ kjj. Ta dễ suy ra công thức truy hồi sau:

  • dp0,0=1dp_{0, 0} = 1
  • dpi,j=dpi1,j+3dpi1,(j1+k) mod k+3dpi1,(j2+k) mod kdp_{i, j} = dp_{i - 1, j} + 3 dp_{i - 1, (j - 1 + k)\ mod\ k} + 3dp_{i - 1, (j - 2 + k) \ mod\ k}

Đáp án sẽ là dpm,01dp_{m, 0} - 1 (vì ta cần loại trường hợp không có xâu nào có bit bật).

Cách này sẽ có độ phức tạp là O(mk)O(mk), đủ ăn subtask 2, nhưng chưa đủ nhanh để qua subtask cuối.

Nhận xét:

  • Dễ tính được dp1dp_1.
  • dpi,j=t=0k1dpa,t×dpia,(jt+k) mod kdp_{i, j} = \sum_{t = 0}^{k - 1} dp_{a, t} \times dp_{i - a, (j - t + k)\ mod\ k}, đúng với mọi a[0,i]a \in [0, i].

Vậy nếu ta coi f(i)=dpif(i) = dp_i và toán tử f(a+b)=f(a)f(b)f(a + b) = f(a) \circ f(b)dpa,t×dpb,(jt+k) mod k\sum dp_{a, t} \times dp_{b, (j - t + k)\ mod\ k} với mọi j[0,k)j \in [0, k) cần tính thì ta có thể làm bài này trong O(k2log2m)O(k^2\log_2{m}), với k2k^2 là độ phức tạp của toán tử.

Code:

const int MOD = 1e9 + 7;
inline void add(int &x, int y, int mod = MOD) { x += y; while (x >= mod) x -= mod; while (x < 0) x += mod;}
inline void mul(int &x, int y, int mod = MOD) { x = (x * 1LL * y) % mod;}
inline int prod(int x, int y, int mod = MOD) { return mul(x, y, mod), x;}
inline int sum(int x, int y, int mod = MOD) { return add(x, y, mod), x;}
inline int bpow(int x, int y, int mod = MOD) { int ans = 1; while (y) { if (y & 1) mul(ans, x, mod); mul(x, x, mod); y >>= 1;} return ans;}
inline int Inv(int x, int mod = MOD) { return bpow(x, mod - 2, mod);}
inline int Div(int x, int y, int mod = MOD) { return prod(x, Inv(y, mod), mod);}
const int N = 2e5 + 5;
const long long INF = 1e18 + 7;
const int MAXA = 1e9;
const int B = sqrt(N) + 5;
int n, k;
int pw[32][101], dp[32][101];
void solve()
{
int n, k; cin >> n >> k;
pw[0][0]++; pw[0][1 % k] += 3; pw[0][2 % k] += 3;
for(int i = 1; i < 32; ++i){
for(int s1 = 0; s1 < k; ++s1){
for(int s2 = 0; s2 < k; ++s2){
add(pw[i][(s1 + s2) % k], prod(pw[i - 1][s1], pw[i - 1][s2]));
//pw[i] = f(2^i) = dp[2 ^ i][]
}
}
}
dp[0][0] = 1;
for(int i = 0; i < 30; ++i){
if (!(n >> i & 1)) {
for(int s = 0; s < k; ++s) dp[i + 1][s] = dp[i][s];
continue;
}
for(int s1 = 0; s1 < k; ++s1){
for(int s2 = 0; s2 < k; ++s2){
add(dp[i + 1][(s1 + s2) % k], prod(dp[i][s1], pw[i][s2]));
}
}
}
cout << sum(dp[30][0], -1);
}

Ngoài ra bài còn có một cách dùng nhân ma trận trong O(k3×log2n)O(k^3 \times log_2{n}) (về bản chất thì nhân ma trận cũng dùng lũy thừa nhanh để tính matrixkmatrix^k).

Bài tập

Author: Võ Khắc Triệu (DeMen100ns) @ DeMen100ns's Blog

Permalink: https://demen100ns.github.io/blog/2026-07-12-demen-blog-3-binary-exponentiation/

Title: DeMen Blog #3: Lũy thừa nhanh và hơn thế nữa!

License: All articles on this blog are licensed under the BY-NC-SA license agreement unless otherwise stated. Please indicate the source when reprinting!