少女祈祷中...

DeMen Blog #7 - Trick: Baby step, giant step trên mảng hằng

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

Đặt vấn đề

Xét bài toán:

Nhập nn (0n<109+7)(0 \le n < 10^9 + 7). Tính n!n! (mod 109+7)(mod\ 10^9+7).

Lời giải hiển nhiên ai cũng nghĩ ra chạy trong O(n)O(n). Tuy nhiên, nn trong trường hợp này khá lớn và phép modulo tương đối chậm (O(1)O(1) nhưng const to) nên rất dễ bị quá thời gian.

Trick

Mình thử code O(n)O(n) và chạy trên máy với input 10910^9 thì mất tầm 55 giây. Rõ ràng là ta có thể sinh ra đáp án với mọi input có thể có, vậy ta sẽ dùng thông tin này một cách hữu ích.

Mảng hằng độ dài 10910^9

Ý tưởng rất đơn giản: Dùng code để in ra đáp án cho tất cả các input, rồi lưu vào code bằng mảng hằng, sau đó ta chỉ cần trả lời trong O(1)O(1). Code minh họa:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e9 + 1;
const int MOD = 1e9 + 7;
int fact[N] = {1, 1, 2, 6, 24, ...};
int main(){
int n; cin >> n;
cout << fact[n];
return 0;
}

Tuy nhiên, lượng dữ liệu này là quá lớn, chưa nói đến tràn bộ nhớ (Memory Limit Exceeded) thì bạn có thể đã bị Char/File Limit Exceeded (vượt quá số ký tự cho phép trong code, thường là 32/64 KB32/64\ KB).

Như vậy ta thấy, nếu dùng quá nhiều dữ liệu thì bị tràn bộ nhớ hoặc tràn ký tự, nhưng quá ít thì lại không đủ nhanh, thế thì ta sẽ chọn ở giữa.

Mảng hằng độ dài BB

Ta đã biết: n!=(n1)!×nn! = (n-1)! \times n.

Như vậy, nếu ta biết L!L! thì sẽ tính được R!R! trong O(RL)O(R - L). Từ đây, ta có thể chọn ra các điểm cắt để tính toán nhanh hơn.

Gọi M=109BM = \frac{10^9}{B}. Ta sẽ lưu mảng hằng FF độ dài BB, với Fi=(iM)! (0i<B)F_i = (iM)!\ (0 \le i < B).

Lúc này, ta sẽ tính được n!n! bằng cách sau: Tìm jj lớn nhất sao cho jMnjM \le n. Giờ ta chỉ cần tính: jM!×(jM+1)××njM! \times (jM + 1) \times \dots \times n

Trong đó:

  • Ta đã tính trước jM!jM! và có thể lấy ra nó trong O(1)O(1).
  • Ta tính (jM+1)××n(jM + 1) \times \dots \times n trong O(M)O(M).

\rightarrow Độ phức tạp là O(M)O(M).

Nếu ta chọn B=103B = 10^3M=106M = 10^6 thì ta chỉ cần khoảng 9000B=9KB9000B = 9KB để lưu.

Vận dụng

Với bất kỳ hàm nào mà có thể tính được NN giá trị đầu tiên trong một khoảng thời gian hợp lý thì thuật toán này đều tỏ ra hiệu quả (Hàm đếm số nguyên tố, (nk)\binom{n}{k}, …).

Bài tập

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

Permalink: https://demen100ns.github.io/blog/2023-01-01-demen-blog-7-baby-step-giant-step-array/

Title: DeMen Blog #7 - Trick: Baby step, giant step trên mảng hằng

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