Đếm ước của một số trong O(N^1/3)

I. Đặt vấn đề Chắc hẳn, ai trong chúng ta cũng đã quá quen thuộc với bài toán đếm số ước nguyên dương của nnn. Giải thuật thông thường nhất mà mọi người thường sử dụng là giải thuật O(n),O(sqrt{n}),O(n​), dựa trên một nhận định rằng nếu như số nnn có một ước là i (1≤i≤n)i

I. Đặt vấn đề

Chắc hẳn, ai trong chúng ta cũng đã quá quen thuộc với bài toán đếm số ước nguyên dương của nn. Giải thuật thông thường nhất mà mọi người thường sử dụng là giải thuật O(n),O(sqrt{n}), dựa trên một nhận định rằng nếu như số nn có một ước là i (1≤i≤n)i (1 le i le sqrt{n}) thì nó cũng sẽ có một ước nữa là ni (n≤ni≤n)frac{n}{i} left(sqrt{n} le frac{n}{i} le n right). Bằng phương pháp này, chúng ta có thể giải quyết mọi bài toán với giới hạn nn khoảng 101510^{15} trở xuống (Các bạn xem lại trong chuyên đề Tìm các ước của một số nguyên).

Một phương pháp hay khác mà chúng ta cũng sử dụng là phân tích thừa số nguyên tố và đếm ước của nn dựa trên phân tích nguyên tố của nó. Cách làm này có thể khiến thao tác đếm ước của số nn giảm xuống độ phức tạp O(log⁡(n))O(log(n)) khi kết hợp với sàng lọc số nguyên tố, và thường được áp dụng trong các bài toán multi-testcase (các bạn xem lại trong chuyên đề Số nguyên tố). Tuy nhiên, nhược điểm của phương pháp này là bạn buộc phải tạo ra được một mảng có độ dài nn để đánh dấu các số nguyên tố, đồng nghĩa với việc nếu n≤109,n le 10^9, các bạn không thể sử dụng được nó.

Điều gì sẽ xảy ra khi chúng ta cần đếm ước của một số nguyên dương n≤1018?n le 10^{18}? Phân tích thừa số nguyên tố? Không thể, vì ta không thể sàng lọc được số nguyên tố ở giới hạn này. Vậy phân tích theo phương pháp O(n)O(sqrt{n}) truyền thống thì sao? Cũng không thể, vì O(n)≈O(109),O(sqrt{n}) approx O(10^9), độ phức tạp này không đủ tốt. Khi đó, người ta sử dụng phương pháp đếm ước trong O(n13),O(n^{frac{1}{3}}), một phương pháp rất hiệu quả nhưng lại được ít người biết đến, có lẽ vì chúng ta không thường xuyên gặp phải những bài toán như vậy. Trong chuyên đề này, chúng ta sẽ cùng nghiên cứu ý tưởng và cách cài đặt của phương pháp này bằng C++. Trước khi đọc bài viết, các bạn cần có kiến thức đầy đủ về sàng lọc số nguyên tố, đếm ước theo phương pháp thông thường cũng như kĩ năng code ở mức khá. Nếu chưa nắm được những kiến thức này, các bạn hãy quay lại nghiên cứu những chuyên đề cũ mà mình đã để link ở trên nhé!

II. Phương pháp đếm số ước của một số trong O(n13)O(n^{frac{1}{3}})

1. Phương pháp kiểm tra nguyên tố Fermat

Để sử dụng được giải thuật đếm ước trong O(n13),O(n^{frac{1}{3}}), trước tiên ta cần tìm hiểu về phương pháp của Fermat dùng để kiểm tra tính nguyên tố của một số. Đây là một phương pháp kiểm tra nguyên tố có tính xác suất, nghĩa là nó có thể xảy ra trường hợp sai, tuy nhiên, trong giải thuật này sự sai khác đó có thể chấp nhận được.

Ý tưởng:
Phương pháp kiểm tra tính nguyên tố của Fermat được xây dựng dựa trên định lý Fermat nhỏ: Nếu nn là một số nguyên tố, thì với mọi giá trị aa sao cho 1≤a<n1 le a < n ta đều có: an−1≡1 (mod n)a^{n – 1} equiv 1 (text{mod }n). Chi tiết chứng minh định lý các bạn có thể đọc thêm ở Wikipedia.

Dựa vào định lý trên, ta triển khai giải thuật như sau:

  • Lấy ngẫu nhiên một số aa thuộc đoạn [1,n−1][1, n – 1].
  • Kiểm tra đẳng thức an−1≡1 (mod n),a^{n – 1} equiv 1 (text{mod }n), nếu đẳng thức sai thì nn không phải là số nguyên tố, ngược lại nncó khả năng là một số nguyên tố.
  • Lặp lại hai thao tác kiểm tra trên kk lần, giá trị kk càng lớn thì xác suất chính xác sẽ càng tăng.

Giải thuật kiểm tra tính nguyên tố của Fermat sẽ luôn luôn đúng nếu như nn đã là một số nguyên tố, ngược lại nó sẽ có thể sai. Tuy nhiên, như đã nói xác suất xảy ra sai khi nn là hợp số khá nhỏ, nên chúng ta hoàn toàn có thể sử dụng giải thuật Fermat trong một số trường hợp cụ thể. Bên cạnh đó, các bạn có thể tìm hiểu về một số giải thuật kiểm tra nguyên tố xác suất khác như Miller – Rabin hay Solovay – Strassen.

Cài đặt:

// Phép nhân Ấn Độ (a * b) % mod. Sử dụng để tránh tràn số khi thực hiện phép nhân.longlongindian_multiplication(longlong a,longlong b,longlong mod){if(b ==0)return0LL;longlong half =indian_multiplication(a, b /2LL, mod)% mod;if(b &1)return(half + half + a)% mod;elsereturn(half + half)% mod;}// Tính (a^b) % mod. Sử dụng kết hợp với phép nhân Ấn Độ để tránh tràn số khi thực hiện phép nhân.longlongmodular_exponentiation(longlong a,longlong b,longlong mod){if(b ==0)return1LL;longlong half =modular_exponentiation(a, b /2LL, mod)% mod;longlong product =indian_multiplication(half, half, mod);if(b &1)returnindian_multiplication(product, a, mod);elsereturn product;}// Thực hiện kiểm tra Fermat với k = 50 lần.boolfermat_checking(longlong n,int k =50){// Xủ lý trước một số trường hợp để tăng tính chính xác.// Cần tránh trước trường hợp n = 4, do trường hợp này kiểm tra Fermat bị sai.if(n <4)return n ==2|| n ==3;if(n !=2&& n %2==0)returnfalse;for(int i =1; i <= k;++i){longlong a =2+rand()%(n -3);if(modular_exponentiation(a, n -1, n)!=1)returnfalse;}returntrue;}

Giải thuật có độ phức tạp O(k×log⁡(n))O(k times log(n)).

2. Phương pháp kiểm tra nguyên tố Miller – Rabin

Phương pháp nói trên của Fermat có ưu thế là cài đặt đơn giản, ngắn gọn, tuy nhiên xác suất sai sẽ dễ xảy ra trong trường hợp số nn đưa vào là một số giả nguyên tố (tức là hợp số nhưng vẫn thỏa mãn an≡1 (mod n)a^n equiv 1 (text{mod }n) với aa nào đó). Chính vì thế, khi cần tới độ chính xác cao, người ta thường sử dụng phương pháp kiểm tra tính nguyên tố Miller – Rabin, một phương pháp rất mạnh trong các phương pháp kiểm tra nguyên tố có tính xác suất.

Ý tưởng:
Giải thuật Miller – Rabin được xây dựng dựa trên một số nhận định sau:

  • Đối với một số chẵn nn bất kỳ, ta luôn luôn có thể viết nó dưới dạng n=2r×d,n = 2^r times d, với dd là một số lẻ và r>0 (1)r > 0 (1).
  • Theo định lý nhỏ Fermat, nếu nn là một số nguyên tố, thì với mọi giá trị aa sao cho 1≤a<n1 le a < n ta đều có: an−1≡1 (mod n) (2)a^{n – 1} equiv 1 (text{mod }n) (2).
  • Theo bổ đề Euclid, giả sử nn là một số nguyên tố và n=a×b,n = a times b, thì chắc chắn nn phải chia hết cho một trong hai số aa hoặc bb. Vậy nếu giả sử n=(x2−1)=(x−1).(x+1)n = (x^2 – 1) = (x – 1).(x + 1) thì chắc chắn x≡1 (mod n)x equiv 1 (text{mod } n) hoặc x≡−1 (mod n) (3)x equiv -1 (text{mod }n) (3).
  • Từ (1)(1)(2),(2), ta xây dựng dãy số xk=a2k×d,∀k:0≤k≤rx_k = a^{2^k times d}, forall k: 0 le k le r. Khi đó:

{xk=(xk−1)2;∀k:1≤k≤r.xr=an−1. (4)begin{cases}x_k = (x_{k – 1})^2;&& forall k: 1 le k le r.\ x_r = a^{n – 1}. end{cases} (4)

  • Từ (2)(2)(4)(4) ta có:

    xr≡1 (mod n)x_r equiv 1 (text{mod }n)

    hay

    (xr−1)2≡1 (mod n)(x_{r – 1})^2 equiv 1 (text{mod }n)

    nên xr−1≡1 (mod n)x_{r – 1} equiv 1 (text{mod }n) hoặc xr−1≡−1 (mod n)x_{r – 1} equiv -1 (text{mod }n). Nếu thực hiện một số hữu hạn bước như trên với các giá trị rr giảm dần về 0,0, thì ta có:

    • Hoặc tồn tại k (0≤k<r)k (0 le k < r) sao cho: xk≡−1 (mod n)x_k equiv -1 (text{mod }n).
    • Hoặc ∀k:0≤k<rforall k: 0 le k < r thì xk≡1 (mod n)x_k equiv 1 (text{mod }n).

Dựa vào tất cả các nhận xét trên, ta có mệnh đề kiểm tra nguyên tố Miller – Rabin như sau: Nếu nn là một số nguyên tố lẻ và n−1=2r×d (r>0,d mod 2≠0)n – 1 = 2^r times d (r > 0, d text{ mod }2 ne 0) thì ∀a:1≤a<n,forall a: 1 le a < n, ta có:

  • Hoặc xk=a2k×d≡1 (mod n);∀k:0≤k≤rx_k = a^{2^k times d} equiv 1 (text{mod }n); forall k: 0 le k le r.
  • Hoặc ∃k:0≤k≤rexists k: 0 le k le r thỏa mãn: xk=a2k×d≡−1 (mod n)x_k = a^{2^k times d} equiv -1 (text{mod }n).

Như vậy giải thuật có thể triển khai thành các bước sau:

  • Bước 11: Phân tích n−1=2r×dn – 1 = 2^r times d với dd là số tự nhiên lẻ và r>0r > 0.
  • Bước 22: Chọn ngẫu nhiên aa thuộc [1,n−1][1, n – 1] rồi đặt text{mod_val} = a^d text{ mod } n.
  • Bước 33: Liên tục bình phương text{mod_val} và lấy số dư khi chia cho n,n, nếu như gặp một số dư khác 11 hoặc n−1 (n – 1 ( tương tự với −1)-1) thì trả về falsetext{false}.
  • Bước 44: Nếu như sau khi kiểm tra mọi xk mod nx_k text{ mod } n mà không tồn tại giá trị nào khác 11 hoặc n−1n – 1 thì kết luận truetext{true}.

Thực hiện giải thuật trên kk lần, với kk càng lớn ta sẽ có độ chính xác càng cao. Đối với giải thuật Miller – Rabin thì chỉ cần sử dụng k=10k = 10 là đã đủ an toàn.

Cài đặt:

// Phép nhân Ấn Độ (a * b) % mod. Sử dụng để tránh tràn số khi thực hiện phép nhân.intindian_multiplication(int a,int b,int mod){if(b ==0)return0;int half =indian_multiplication(a, b /2LL, mod)% mod;if(b &1)return(half + half + a)% mod;elsereturn(half + half)% mod;}// Tính (a^b) % mod. Sử dụng kết hợp với phép nhân Ấn Độ để tránh tràn số khi thực hiện phép nhân.intmodular_exponentiation(int a,int b,int mod){if(b ==0)return1LL;int half =modular_exponentiation(a, b /2LL, mod)% mod;int product =indian_multiplication(half, half, mod);if(b &1)returnindian_multiplication(product, a, mod);elsereturn product;}

vector <int>eratosthenes_sieve(int max_value){
    vector <bool>is_prime(max_value +1,true);
    is_prime[0]= is_prime[1]=false;for(int i =2; i * i <= max_value;++i)if(is_prime[i])for(int j = i * i; j <= max_value; j += i)
                is_prime[j]=false;

    vector <int> primes;for(int i =2; i <= max_value;++i)if(is_prime[i])
            primes.push_back(i);return primes;}// Kiểm tra nguyên tố Miller - Rabin k lần.boolcheck_prime_by_miller_rabin(int n,int k){// Xử lý trước các trường hợp đặc biệt.if(n <2)returnfalse;if(n !=2&& n %2==0)returnfalse;// Tìm d là số lẻ sao cho n - 1 = 2^r * d và r != 0.// Sau khi tìm ra d, r sẽ bằng số lần nhân 2 vào d để tiến tới n - 1.int d = n -1;while(d %2==0)
        d /=2;// Bắt đầu kiểm tra .for(int i =1; i <= k;++i){// Chọn a là một số ngẫu nhiên trong đoạn [2, n - 1].int a =rand()%(n -1)+1;int temp = d;// Tính a^d % n.int mod_val =modular_exponentiation(a, temp, n);// Trong khi d != n và a^(2^k * d) % n != 1 và a^(2^k * d) % n != (n - 1).// Bước này bản chất là thử kiểm tra mọi x(k) % n với k = 0...r.while(temp != n -1&& mod_val !=1&& mod_val != n -1){
            mod_val =indian_multiplication(mod_val, mod_val, n);
            temp *=2;}// Nếu không thể chạm được tới x(r) thì nghĩa là đã tồn tại giá trị k khiến cho// a^(2^k * d) % n != 1 hoặc a^(2^k * d) % n != -1if(mod_val != n -1&& temp %2==0)returnfalse;}returntrue;}

Giải thuật có độ phức tạp O(k×log⁡(n))O(k times log(n)).

3. Đếm số ước của một số trong O(n13)O(n^{frac{1}{3}})

Ý tưởng: Nói dài dòng như vậy, nhưng bây giờ chúng ta mới đi vào ý chính của bài viết. Trước tiên, ta sẽ viết nn dưới dạng tích của hai số x,yx, y sao cho:

  • Phân tích nguyên tố của xx chỉ gồm các số nguyên tố không vượt quá n13n^{frac{1}{3}}.
  • Phân tích nguyên tố của yy chỉ gồm các số nguyên tố lớn hơn n13n^{frac{1}{3}}.

Dễ dàng nhận thấy, xxyy là hai số nguyên tố cùng nhau, do chúng không có chung bất kỳ thừa số nguyên tố nào cả. Việc tìm ra xx có thể thực hiện rất dễ, bằng cách duyệt qua tất cả các số nguyên dương trong đoạn [2,n13][2, n^{frac{1}{3}}] và thử chia nn cho những số đó tới khi không thể chia hết được nữa (giống với cách phân tích thừa số nguyên tố trong O(n)O(sqrt{n})). Ở bước này ta sẽ áp dụng thêm sàng lọc số nguyên tố để tìm nhanh ra các số nguyên tố không vượt quá n13n^{frac{1}{3}}.

Đến đây, bạn đọc có thể thắc mắc rằng, tại sao lại cần viết nn ở dạng x×y?x times y? Cần biết rằng, hàm F(n)F(n) đếm số lượng ước nguyên dương của nn là một Hàm nhân tính, tức là F(n)=F(x)×F(y)F(n) = F(x) times F(y) nếu như xxyy là hai số nguyên tố cùng nhau. Do đó, việc tính F(n)F(n) sẽ được đưa về việc tính F(x)F(x)F(y)F(y). Cụ thể ta tính F(x)F(x)F(y)F(y) như sau:

  • Đối với F(x)F(x): Sử dụng sàng lọc số nguyên tố Eratosthenes để sinh ra tất cả các số nguyên tố không vượt quá n13n^{frac{1}{3}}. Sau đó, duyệt qua từng số nguyên tố và áp dụng Legendre’s Formula để tính số mũ của từng thừa số nguyên tố đó trong x,x, từ đó tính được F(x)F(x).
  • Đối với F(y)F(y): Giả sử yy có thể được phân tích thành tích của ba số nguyên tố khác nhau, tức là y=p1×p2×p3y = p_1 times p_2 times p_3. Vì yy chỉ bao gồm các thừa số nguyên tố lớn hơn n13,n^{frac{1}{3}}, nên p1×p2×p3>n,p_1 times p_2 times p_3 > n, điều này vô lí. Do đó điều giả sử không thể xảy ra và yy chỉ có thể chứa tối đa 22 thừa số nguyên tố. Như vậy, ta nhận xét được rằng, sau khi chia nn cho x,x, số yy chỉ có thể rơi vào một trong ba trường hợp:
    • TH1:yy là một số nguyên tố. Khi đó F(y)=2F(y) = 2.
    • TH2:yy là bình phương của một số nguyên tố. Khi đó F(y)=3F(y) = 3.
    • TH3:yy là tích của hai số nguyên tố khác nhau. Khi đó F(y)=4F(y) = 4.

Việc kiểm tra yy thuộc vào trường hợp nào có thể được thực hiện bằng cách sử dụng giải thuật kiểm tra tính nguyên tố của Fermat hoặc Miller – Rabin như mình đã đề cập ở trên! Như vậy, chúng ta đã có thể cài đặt giải thuật đếm số ước của nn trong O(n13)O(n^{frac{1}{3}})!

Cài đặt: Dưới đây là cài đặt C++ của giải thuật, đã được sử dụng để nộp thành công bài tập https://codeforces.com/gym/100753/attachments trên codeforces. Mình sẽ thực hiện bằng cả hai phương pháp kiểm tra số nguyên tố của Fermat và Miller – Rabin.

Code bằng giải thuật Fermat:

#include<bits/stdc++.h>#defineintlonglong#definetask"Divisions_Fermat."usingnamespace std;// Sàng lọc số nguyên tố.
vector <int>eratosthenes_sieve(int max_value){
    vector <bool>is_prime(max_value +1,true);
    is_prime[0]= is_prime[1]=false;for(int i =2; i * i <= max_value;++i)if(is_prime[i])for(int j = i * i; j <= max_value; j += i)
                is_prime[j]=false;

    vector <int> primes;for(int i =2; i <= max_value;++i)if(is_prime[i])
            primes.push_back(i);return primes;}voidsolution(int n){// Sàng lọc các số nguyên tố từ 1 tới 10^6 (bằng với n^(1/3) trong trường hợp lớn nhất).
    vector <int> primes =eratosthenes_sieve(1000000);// Tính F(x) với x bao gồm tất cả các thừa số nguyên tố nhỏ hơn hoặc bằng n^(1/3). Lưu luôn F(x) vào res.longlong res =1;for(int p: primes){if(p * p * p > n)break;int cnt =0;while(n % p ==0){
            n /= p;++cnt;}

        res *=(cnt +1);}// Tính F(y) với y bao gồm tất cả các thừa số nguyên tố lớn hơn n^(1/3). Chắc chắn y chỉ có thể ở một trong ba// trường hợp: là số nguyên tố, là bình phương một số nguyên tố hoặc là tích của hai số nguyên tố phân biệt.if(fermat_checking(n))
        res *=2LL;else{int squaroot =sqrt(n);if(squaroot * squaroot == n &&fermat_checking(squaroot))
            res *=3;elseif(n !=1)
            res *=4;}

    cout << res;}main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);int n;
    cin >> n;solution(n);return0;}

Code bằng giải thuật Miller – Rabin:

#include<bits/stdc++.h>#defineintlonglong#definetask"Divisions_Miller_Rabin."usingnamespace std;boolsqrt_is_integer(int n){int squaroot =sqrt(n);return(squaroot * squaroot == n);}voidsolution(int n,int k){// Sàng lọc các số nguyên tố từ 1 tới 10^6 // (bằng với n^(1/3) trong trường hợp lớn nhất).
    vector <int> primes =eratosthenes_sieve(1000000);// Tính F(x) với x bao gồm tất cả các thừa số nguyên tố nhỏ hơn hoặc bằng n^(1/3). // Lưu luôn F(x) vào res.longlong res =1;for(int p: primes){if(p * p * p > n)break;int cnt =0;while(n % p ==0){
            n /= p;++cnt;}

        res *=(cnt +1);}// Tính F(y) với y bao gồm tất cả các thừa số nguyên tố lớn hơn n^(1/3). // Chắc chắn y chỉ có thể ở một trong ba trường hợp: là số nguyên tố, là bình phương // của một số nguyên tố hoặc là tích của hai số nguyên tố phân biệt.if(check_prime_by_miller_rabin(n, k))
        res *=2LL;elseif(sqrt_is_integer(n)&&check_prime_by_miller_rabin((int)sqrt(n), k))
        res *=3LL;elseif(n !=1)
        res *=4LL;

    cout << res;}main(){
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);int n;
    cin >> n;solution(n,10);return0;}

III. Tài liệu tham khảo

Nguồn: viblo.asia

Bài viết liên quan

WebP là gì? Hướng dẫn cách để chuyển hình ảnh jpg, png qua webp

WebP là gì? WebP là một định dạng ảnh hiện đại, được phát triển bởi Google

Điểm khác biệt giữa IPv4 và IPv6 là gì?

IPv4 và IPv6 là hai phiên bản của hệ thống địa chỉ Giao thức Internet (IP). IP l

Check nameservers của tên miền xem website trỏ đúng chưa

Tìm hiểu cách check nameservers của tên miền để xác định tên miền đó đang dùn

Mình đang dùng Google Domains để check tên miền hàng ngày

Từ khi thông báo dịch vụ Google Domains bỏ mác Beta, mình mới để ý và bắt đầ