# 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 - [Lũy thừa nhanh](https://vnoi.info/wiki/translate/he/Number-Theory-3.md) (Bài viết sẽ nhắc lại cách tính lũy thừa nhanh) - [Quy hoạch động](https://vnoi.info/wiki/algo/dp/basic-dynamic-programming-1.md) ## Đặt vấn đề Xét bài toán sau: ## Bài toán Tính: $(a^b)\ modulo\ m$ $(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)$. **Code:** ```cpp= 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 $n$ chẵn: $a^b = (a^{\lfloor \frac{b}{2} \rfloor})^2$ - Với $n$ lẻ: $a^b = (a^{\lfloor \frac{b}{2} \rfloor})^2 \times a$ Như vậy ta chỉ cần tính $a^b$ bằng $a^{\lfloor \frac{b}{2} \rfloor}$, và $b$ sẽ giảm một nửa liên tục cho đến khi còn $b = 1$ vì $a^1 = a$. Dễ thấy $b$ chỉ giảm $\log_2{b}$ lần. **Độ phức tạp:** $O(\log_2{b})$ **Code:** ```cpp= 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 $a^1, a^2, a^4, a^8, \dots, a^{(2^k)}$ trong $O(k)$, vì: $a^{(2^k)} = (a^{(2^{k-1})})^2$. $(1)$ Ta cũng biết rằng, mỗi số tự nhiên $n$ có thể được phân tách dưới dạng: $2^{p_1} + 2^{p_2} + \dots + 2^{p_k} (p_1 < p_2 < \dots < p_k)$, với $k \le \log_2{n}$. Ta dễ dàng xác định các biến $p_i$ thông qua biểu diễn nhị phân của $n$. Như vậy, ta có thể tính $a^b$ nhanh bằng cách tách $b$ thành các $p_i$ theo biểu diễn nhị phân như trên rồi nhân các $a^{p_i}$ vào với nhau. Các $a^{p_i}$ đã được tính ở bước $(1)$ trong $O(k)$. **Ví dụ:** Tính $a^b$ với $b = 13$. Xét biểu diễn nhị phân của $b = 13: 1101$. $\rightarrow b = 13 = 2^3 + 2^2 + 2^0 = 8 + 4 + 1$. $\rightarrow a^b = a^{13} = a^8 \times a^4 \times a^1$. **Độ phức tạp:** $O(\log_2{b})$ ```cpp= 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 $f$ thỏa: - $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)$ trong $O(T\log_2{n})$, với $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)$ bằng cách tính trước $f(1), f(2), f(4), \dots, f(2^k)$, rồi tính $f(n)$ bằng cách biểu diễn $n$ thành dạng nhị phân rồi gộp vào. Thực tế, nếu ta coi $f(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**](https://oj.vnoi.info/problem/olp_ct22_restore) **Tóm tắt:** Đếm số cách chọn ba xâu nhị phân $A, B, C$ độ dài $m$ 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 $k$. - Không tồn tại vị trí nào bật bit trên cả ba xâu. In ra đáp án $modulo\ 10^9 + 7.$ **Limit:** $1 \le m \le 5 \times 10^8, 1 \le k \le 100$. **Lời giải quy hoạch động:** Gọi $dp_{i, j}$ là số cách chọn ba xâu nhị phân $A, B, C$ thỏa điều kiện và có tổng bit bật $mod\ k$ là $j$. Ta dễ suy ra công thức truy hồi sau: - $dp_{0, 0} = 1$ - $dp_{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à $dp_{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)$, đủ ăn subtask 2, nhưng chưa đủ nhanh để qua subtask cuối. **Nhận xét:** - Dễ tính được $dp_1$. - $dp_{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 \in [0, i]$. Vậy nếu ta coi $f(i) = dp_i$ và toán tử $f(a + b) = f(a) \circ f(b)$ là $\sum dp_{a, t} \times dp_{b, (j - t + k)\ mod\ k}$ với mọi $j \in [0, k)$ cần tính thì ta có thể làm bài này trong $O(k^2\log_2{m})$, với $k^2$ là độ phức tạp của toán tử. Code: ```cpp= 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(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 $matrix^k$). ## Bài tập - [CSES - Exponentiation](https://cses.fi/problemset/task/1095) - [Hackerearth - Perfect Permutations, Subtask 4](https://www.hackerearth.com/problem/algorithm/perfect-permutations-september-clash/) - [Codeforces - 1808E2](https://codeforces.com/contest/1808/problem/E2) - [Codeforces - 1808E3](https://codeforces.com/contest/1808/problem/E3)