Phép nhân Ấn Độ - Thuật toán bình phương và nhân
Trong chuyên đề này, chúng ta sẽ cùng nghiên cứu về hai kĩ thuật khá quen thuộc và có tính ứng dụng cao trong các bài toán số học, đó là Phép nhân Ấn Độ và Thuật toán bình phương và nhân - những kĩ thuật sẽ giúp các bạn tính toán và trong thời gian . Mặc dù nghe có vẻ khá vô dụng (bởi vì thực ra phép nhân chỉ cần thực hiện trực tiếp), nhưng đối với một số trường hợp thì chúng lại rất có ích (đặc biệt với ngôn ngữ C++).
Trước khi nghiên cứu chuyên đề này, các bạn cần phải có kiến thức về Giải thuật đệ quy, bởi vì các cài đặt đều sẽ sử dụng đệ quy. Nếu chưa nắm được thì các bạn hãy vào đọc về nó tại <i>đây</i>.
I. Phép nhân Ấn Độ
1. Đặt vấn đề
Xét một bài toán đơn giản như sau: Cho hai số . Tính giá trị biểu thức .
Bài toán trên có thể dễ dàng giải quyết bằng tính chất phân phối của phép nhân đối với phép đồng dư thức: . Tuy nhiên, nếu như ta cần lấy số dư cho thì sao? Phép toán bằng tính chất phân phối bây giờ sẽ không thể thực hiện được, vì dẫn đến kết quả của bước này sẽ bị vượt quá khả năng biểu diễn của kiểu số nguyên bit trong C++.
Phép nhân Ấn Độ sử dụng để tính trong trường hợp tính chất phân phối với phép đồng dư thức không thể áp dụng được vì lí do tràn số. Tuy nhiên điều này chỉ xảy ra với C++, còn đối với Python thì sẽ không ảnh hưởng gì cả.
2. Phép nhân Ấn Độ với đồng dư thức
Nguyên lí phép nhân Ấn Độ rất đơn giản như sau:
Dựa trên lý thuyết này, ta sẽ kết hợp phép nhân Ấn Độ với tính chất phân phối của phép nhân, phép cộng với phép đồng dư thức để tính được với mà không bị tràn số.
Lưu ý tính chất phân phối của phép cộng đối với phép đồng dư thức, tính chất này có thể áp dụng với cả phép nhân và phép trừ:
Cài đặt
Ngôn ngữ C++:
// Tính a * b % M
long long multiply_modulo(long long a, long long b, long long M)
{
if (b == 0)
return 0;
long long t = multiply_modulo(a, b / 2, M) % M;
if (b & 1)
return ((t + t) % M + a % M) % M;
else
return (t + t) % M;
}
Ngôn ngữ Python:
# Tính a * b % M
def multiply_modulo(a, b, M):
if b == 0:
return 0
t = multiply_modulo(a, b // 2, M)
if b & 1:
return ((t + t) % M + a % M) % M
else:
return (t + t) % M
Đánh giá độ phức tạp
Trong thuật toán, liên tục giảm đi một nửa, nên độ phức tạp của giải thuật là .
I. Thuật toán bình phương và nhân
1. Giải thuật chia để trị
Thông thường, để tính lũy thừa ta sẽ cần sử dụng phép nhân liên tiếp trong một vòng lặp. Nếu như là một số lớn cỡ trở lên, thì việc tính toán sẽ mất rất nhiều thời gian, không thể đáp ứng được yêu cầu trong các bài toán lập trình. Mặt khác, kết quả của phép lũy thừa thường rất lớn, nên đề bài sẽ yêu cầu thí sinh in ra kết quả sau khi chia lấy dư cho một giá trị nào đó.
Dựa trên tư tưởng phép nhân Ấn Độ, ta có thể điều chỉnh công thức một chút để tính được lũy thừa với . Công thức đơn giản như sau:
Cài đặt
Trong cài đặt dưới đây, tác giả sẽ cài đặt mẫu cả phiên bản đệ quy và phiên bản khử đệ quy của thuật toán.
Ngôn ngữ C++:
// Tính a^b mod M.
long long power_modulo(long long a, long long b, long long M)
{
if (b == 0)
return 1LL;
long long half = power_modulo(a, b / 2, M) % M;
if (b & 1)
return (((half * half) % M) * (a % M)) % M;
else
return (half * half) % M;
}
Ngôn ngữ Python:
# Tính a^b % M
def power_modulo(a, b, M):
if b == 0:
return 1
half = power_modulo(a, b // 2, M) % M
if b & 1:
return (half * half * a) % M
else:
return (half * half) % M
Ngoài ra, ta cũng có thể cài đặt giải thuật bằng phương pháp khử đệ quy để đẩy nhanh tốc độ thuật toán hơn một chút:
Ngôn ngữ C++:
// Tính a^b % M khử đệ quy.
long long power_modulo_non_recur(long long a, long long b, long long M)
{
long long res = 1;
while (b)
{
if (b & 1)
res = (res * a) % M;
a = (a * a) % M;
b /= 2;
}
return res;
}
Ngôn ngữ Python:
def power_modulo_non_recur(a, b, M):
res = 1
while b != 0:
if b % 1:
res = (res * a) % M
a = (a * a) % M
b = b // 2
return res
Đánh giá độ phức tạp
Trong thuật toán, liên tục giảm đi một nửa, nên độ phức tạp của giải thuật là . Mặc dù độ phức tạp của cả hai cách cài đặt là tương tự nhau, nhưng trong thực tế cách làm khử đệ quy sẽ chạy nhanh hơn một chút do không phải gọi đệ quy.
2. Tính với
Trong trường hợp , dựa trên những gì đã phân tích ở phần I, phép nhân thông thường sẽ không thể áp dụng trong C++ vì lí do xảy ra tràn số. Vì vậy, ta sẽ kết hợp thêm phép nhân Ấn Độ trong trường hợp này. Độ phức tạp sẽ trở thành
Cài đặt
Ngôn ngữ C+++:
long long power_modulo(long long a, long long b, long long M)
{
if (b == 0)
return 1LL;
long long half = power_modulo(a, b / 2LL, M) % M;
half = multiply_modulo(half, half, M);
if (b & 1)
return multiply_modulo(half, a, M);
else
return half;
}
Ngôn ngữ Python:
Mặc dù phép nhân trong Python không bị tràn số, tuy nhiên bản chất ngôn ngữ này vẫn sẽ phải sử dụng các thuật toán xử lý số nguyên lớn để thao tác khi dữ liệu quá to, nên có khả năng chương trình sẽ bị chạy quá thời gian. Vì vậy, tác giả vẫn đưa vào đoạn code bằng Python để các bạn sử dụng khi cần thiết:
def power_modulo(a, b, M):
{
if b == 0:
return 1;
half = power_modulo(a, b // 2, M) % M;
half = multiply_modulo(half, half, M);
if (b & 1)
return multiply_modulo(half, a, M);
else
return half;
}
Đánh giá độ phức tạp
Do sử dụng kết hợp cả thuật toán bình phương và nhân lẫn phép nhân Ấn Độ, nên độ phức tạp tổng quát sẽ là .
3. Tính trong trường hợp là số lớn và là số nguyên tố
Đối với các trường hợp là số lớn - hiểu là các số nằm ngoài khả năng lưu trữ của kiểu số trong C++ và phải lưu bằng kiểu chuỗi - khi đó giải thuật tính sẽ trở nên hơi phức tạp nếu như chúng ta cài đặt bằng các phép toán số lớn. Tuy nhiên, trong trường hợp là một số nguyên tố, dựa vào một số tính chất số học, ta có thể thu gọn được việc tính toán như sau:
-
Thứ nhất, cần biết định lý nhỏ Fermat được phát biểu như sau: Nếu là một số nguyên tố thì:
-
Lại có: với lặp lại lần và . Từ đây suy ra:
Tới đây chúng ta có thể áp dụng thuật toán bình phương và nhân một cách bình thường mà không sợ bị tràn số. Tất nhiên vẫn sẽ cần lưu ý về giới hạn của để lựa chọn phép nhân thông thường hay phép nhân Ấn Độ.
Việc cài đặt xin dành lại cho bạn đọc.
III. Một số bài toán minh họa
1. Chữ số tận cùng
Đề bài
Cho hai số nguyên và . Cần tìm chữ số tận cùng của
Input:
- Một dòng duy nhất chứa hai số nguyên và .
Ràng buộc:
- .
Output:
- Đưa ra chữ số tận cùng của .
Sample Input:
3 10
Sample Output:
9
Ý tưởng
Để lấy chữ số tận cùng của một số nguyên dương ta lấy chia cho và kết quả là số dư của phép chia.
Áp dụng công thức trên, ta có kết quả bài toán là .
Cài đặt
Ngôn ngữ C++:
#include <bits/stdc++.h>
using namespace std;
void enter(long long &a, long long &b)
{
cin >> a >> b;
}
long long power_modulo(long long a, long long b, long long mod)
{
if (b == 0)
return 1LL;
long long half = power_modulo(a, b / 2, mod) % mod;
if (b & 1)
return (((half * half) % mod) * (a % mod)) % mod;
else
return (half * half) % mod;
}
main()
{
long long a, b;
enter(a, b);
cout << power_modulo(a, b, 10);
return 0;
}
Ngôn ngữ Python:
def power_modulo(a, b, mod):
if b == 0:
return 1
half = power_modulo(a, b // 2, mod) % mod
if b & 1:
return (half * half * a) % mod
else:
return (half * half) % mod
if __name__ == '__main__':
a = int(input())
b = int(input())
print(power_modulo(a, b, 10))
2. Đỗ xe
Đề bài
Một bãi đỗ xe có chỗ đỗ xe liên tiếp, và các CEO của bãi đỗ xe dự định lấp đầy bãi đỗ xe bằng loại xe ở màu khác nhau để làm lễ khai trương. Khi nhìn những chiếc xe xếp thành một đường thẳng, họ thấy rằng nếu như sắp xếp lại các xe sao cho có ít nhất một đoạn gồm đúng chiếc xe cùng màu đứng cạnh nhau, thì bãi đỗ xe sẽ đẹp hơn.
Hãy giúp các CEO đếm xem có bao nhiêu cách sắp xếp như vậy? Biết rằng số lượng xe của mỗi loại đều lớn hơn số lượng chỗ đỗ xe.
Input:
- Chứa duy nhất số nguyên dương .
Ràng buộc:
- .
Output:
- In ra số nguyên duy nhất là số lượng cách sắp xếp những chiếc xe vào chỗ đỗ xe theo yêu cầu. Do kết quả có thể rất lớn, chỉ cần in ra số dư của nó sau khi chia cho .
Sample Input:
3
Sample Output:
24
Ý tưởng
Ta phải chọn ra tổng cộng chiếc xe từ loại xe khác nhau để đặt trên một đường thẳng, sao cho có chính xác chiếc xe cùng màu đứng liên tiếp nhau. Những phương án chọn có thể là: Đặt chiếc cùng màu ở vị trí liên tiếp ở đầu đường thẳng, hoặc đặt ở vị trí liên tiếp ở cuối đường thẳng, hoặc đặt ở vị trí liên tiếp ở giữa đường thẳng.
Nếu như chiếc cùng màu được đặt ở vị trí đầu tiên hoặc vị trí cuối cùng, thì có cách để chọn màu cho chiếc đó, và cách để chọn ra một chiếc xe khác màu đặt bên cạnh chiếc đó. Còn lại vị trí ở giữa, ta có thể chọn một trong màu cho mỗi vị trí. Vậy tổng số cách chọn trong trường hợp này là (vì có thể đặt chiếc vào đầu hoặc cuối đường thẳng).
Nếu như chiếc cùng màu được đặt ở vị trí giữa đường thẳng, thì vẫn có cách chọn màu cho chiếc này, và có thêm cách chọn màu khác với màu đó cho mỗi chiếc kề trái kề phải của chiếc đó. Còn chiếc còn lại, mỗi chiếc có khả năng chọn màu. Vậy tổng số cách chọn cho một đoạn gồm chiếc cùng màu là . Ngoài ra lại có vị trí để đặt chiếc cùng màu vào giữa đoạn, nên tổng số cách chọn trong trường hợp này là: .
Vậy kết quả cuối cùng là: . Vì kết quả phải chia dư cho nên ta sẽ áp dụng giải thuật bình phương và nhân với đồng dư thức.
Cài đặt
Ngôn ngữ C++:
#include <bits/stdc++.h>
using namespace std;
const long long mod = 1e9 + 7;
long long power_modulo(long long a, long long b, long long mod)
{
if (b == 0)
return 1LL;
long long half = power_modulo(a, b / 2, mod) % mod;
if (b & 1)
return (((half * half) % mod) * (a % mod)) % mod;
else
return (half * half) % mod;
}
main()
{
int n;
cin >> n;
long long x = (24 * power_modulo(4, n - 3, mod)) % mod;
long long y = ((((n - 3) * 36) % mod) * power_modulo(4, n - 4, mod)) % mod;
cout << (x + y) % mod;
}
Ngôn ngữ Python:
def power_modulo(a, b, mod):
if b == 0:
return 1
half = power_modulo(a, b // 2, mod) % mod
if b & 1:
return (half * half * a) % mod
else:
return (half * half) % mod
if __name__ == '__main__':
n = int(input())
mod = int(1000000007)
x = (24 * power_modulo(4, n - 3, mod)) % mod
y = ((((n - 3) * 36) % mod) * power_modulo(4, n - 4, mod)) % mod
print((x + y) % mod)
IV. Tài liệu tham khảo
- https://cowboycoder.tech/article/phep-nhan-an-do-va-phep-tinh-luy-thua
- https://vnoi.info/wiki/translate/he/Number-Theory-3.md
- https://cp-algorithms.com/algebra/binary-exp.html
©️ Tác giả: Vũ Quế Lâm từ Viblo
All rights reserved