Cho mảng A[1..n]. Mảng A[p..q] được gọi là mảng con của A. Trọng lượng mảng bằng tổng các phần tử. Tìm mảng con có trọng lượng lớn nhất (1≤ p ≤ q ≤ n).
Để đơn giản ta chỉ xét bài toán tìm trọng lượng của mảng con lớn nhất còn việc tìm vị trí thì chỉ là thêm vào bước lưu lại vị trí trong thuật toán. Ta có thể dễ dàng đưa ra thuật toán tìm kiếm trực tiếp bằng cách duyệt hết các dãy con có thể của mảng A như sau:
void BruteForceNaice; {
Max1 = -MaxInt;
for (i = 1; i<= n; i++) // i là điểm bắt đầu của dãy con for( j =i; j<= n; j++) // j là điểm kết thúc của dãy con {
s= 0;
for ( k = i; k<= j; k++) // Tính trọng lượng của dãy s = s + A[k]
if (s > Max1) Max1 = S }
}
Phân tích độ phức tạp của thuật toán:Lấy s = s + A[k] làm câu lệnh đặc trưng, ta có số lần thực hiện câu lệnh đặc trưng là 3
1
( )
j
n n
i j i k i
k O n . Do đó, thời gian T(n) = O(n3) . Nếu để ý, ta có thể giảm độ phức tạp của thuật toán bằng cách giảm bớt vòng lặp trong cùng (vòng lặp theo k): 1 1 1 [ ] [ ] [ ] j j k k
a k a j a k .Khi đó thuật toán có thể được viết một cách tóm tắt như sau:
for ( i = 1; i<= n; i++) for ( j = i; j<= n; j++) {
s = s + A[j]; //Câu lệnh đặc trưng if (s > max1) max1 = s;
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Lấy s = s + A[j] làm câu lệnh đặc trưng thì ta có số lần thực hiện câu lệnh đặc trưng
là 2
1
( )
n n
i j i
j O n Thời gian của thuật toán T(n) = O(n2). Cách tiếp cận chia để trị
Chia: Chia mảng A ra thành hai mảng con với chênh lệch độ dài ít nhất, kí hiệu là AL, AR.
Trị: Tính mảng con lớn nhất của mỗi nửa mảng A một cách đệ quy. Gọi WL, WR là trọng lượng của mảng con lớn nhất trong AL, AR.
Tổng hợp: Max (WL, WR).
WM = WML + WMR Cài đặt thuật toán:
void MaxSubVector(A, i, j); { If (i == j) return a[i] Else { m = (i + j)/2; WL = MaxSubVector(a, i, m); WR = MaxSubVector(a, m+1, j); WM = MaxLeftVetor(a, i, m) + MaxRightVector(a, m+1, j); Return Max(WL, WR, WM ) } }
Các hàm MaxLeftVector, Max RightVector được cài đặt như sau: void MaxLeftVector(a, i, j);
{
MaxSum = -Maxint; Sum = 0; for( k = j;k>= i;k--) {
Sum = Sum + A[k];
MaxSum = Max(Sum,MaxSum); }
Return MaxSum; }
Tương tự với hàm MaxRightVector là. for (k = i;k<= j;k++)
{
Sum = Sum + A[k];
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
}
Phân tích độ phức tạp: Thời gian chạy thủ tục MaxLeftVector và MaxRightVector là O(m) (m = j - i + 1). Gọi T(n) là thời gian tính, giả thiết n = 2k . Ta có:
Nếu n = 1 thì T(n) = 1
Nếu n > 1 thì việc tính WM đòi hỏi thời gian n/2 + n/2 = n T(n) = 2T(n/2) + n
Theo định lý thợ ta có: ( )T n O(log )n
2.4.3.2 Bài toán tháp Hà Nội
Có 3 chiếc cọc và một bộ n chiếc đĩa. Các đĩa này có kích thước khác nhau và mỗi đĩa đều có 1 lỗ ở giữa để có thể xuyên chúng vào các cọc. Ban đầu, tất cả các đĩa đều nằm trên 1 cọc, trong đó, đĩa nhỏ hơn bao giờ cùng nằm trên đĩa lớn hơn. Yêu cầu của bài toán là chuyển bộ n đĩa từ cọc ban đầu A sang cọc đích C (có thể sử dụng cọc trung gian B), với các điều kiện: mỗi lần chuyển 1 đĩa, trong mọi trường hợp, đĩa có kích thước nhỏ hơn bao giờ cũng phải nằm trên đĩa có kích thước lớn hơn [3].
Với n = 1, có thể thực hiện yêu cầu bài toán bằng cách chuyển trực tiếp đĩa 1 từ cọc A sang cọc C. Với n = 2 có thể thực hiện như sau:
- Chuyển đĩa nhỏ từ cọc A sang cọc trung gian B. - Chuyển đĩa lớn từ cọc A sang cọc đích C.
- Cuối cùng, chuyển đĩa nhỏ từ cọc trung gian B sang cọc đích C.
Như vậy, cả 2 đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ.Với n > 2, giả sử ta đã có cách chuyển n – 1 đĩa, ta thực hiện như sau:
- Lấy cọc đích C làm cọc trung gian để chuyển n – 1 đĩa bên trên sang cọc trung gian B.
- Chuyển cọc dưới cùng (cọc thứ n) sang cọc đích C.
- Lấy cọc ban đầu A làm cọc trung gian để chuyển n – 1 đĩa từ cọc trung gian B sang cọc đích C.
Như vậy, cả hai đĩa đã được chuyển sang cọc đích C và không có tình huống nào đĩa lớn nằm trên đĩa nhỏ. Ta thấy toàn bộ n đĩa đã được chuyển từ cọc A sang cọc C và không vi phạm bất cứ điều kiện nào của bài toán.Ở đây, ta thấy rằng bài toán chuyển n cọc đã được chuyển về bài toán đơn giản hơn là chuyển n - 1 cọc. Điểm dừng của thuật toán đệ qui là khi n = 1 và ta chuyển thẳng cọc này từ cọc ban đầu sang cọc đích. Tính chất chia để trị của thuật toán này thể hiện ở chỗ: Bài toán chuyển n đĩa được chia làm 2 bài toán nhỏ
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
hơn là chuyển n - 1 đĩa. Lần thứ nhất chuyển n - 1 đĩa từ cọc A sang cọc trung gian B, và lần thứ 2 chuyển n - 1 đĩa từ cọc trung gian B sang cọc đích C.
Cài đặt đệ quy cho thuật toán như sau: hàm Chuyển (int n, char a char c) thực hiện việc chuyển đĩa thứ n từ cọc A sang cọc C. Hàm Thaphanoi (int n, char a, char c, char b)
là hàm đệ quy thực hiện việc chuyển n đĩa từ cọc A sang cọc C, sử dụng cọc trung gian là cọc B.
VoidChuyen (int n, char a, char c) {
Print (“Chuyendia thu %d tu coc %c sang coc %c\n”, n, a, c); Return;
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Void ThapHaNoi (int n, char a, char c, char b) { If (n = = 1) chuyen (1, a, c); Else { Thaphanoi (n-1, a, b, c); Chuyen (n, a, c); Thaphanoi (n – 1, b, c, a); } Return; }
Hàm Chuyển thực hiện thao tác in ra 1 dòng cho biết chuyển đĩa thứ mấy từ cọc nào sang cọc nào. Hàm ThapHaNoi kiểm tra nếu số đĩa bằng 1 thì thực hiện chuyển trực tiếp đĩa từ cọc A sang cọc C. Nếu số đĩa lớn hơn 1, có ba lệnh được thực hiện:
- Lời gọi đệ quy ThapHaNoi(n – 1, a, b, c) để chuyển n – 1 đĩa từ cọc A sang cọc B, sử dụng cọc C làm trung gian.
- Thực hiện chuyển đĩa thứ n từ cọc A sang cọc C.
- Lời gọi đệ quy ThapHaNoi(n-1, b, c, a) để chuyển n – 1 đĩa từ cọc B sang cọc C, sử dụng cọc A làm cọc trung gian.
Độ phức tạp của thuật toán là 2n – 1. Nghĩa là để chuyển n cọc thì mất 2n – 1 thao tác chuyển. Ta sẽ chứng minh điều này bằng phương pháp quy nạp toán học. Với n = 1 thì số lần chuyển là 1 = 2*1 – 1. Giả sử giả thiết đúng với n - 1, tức là để chuyển n - 1 đĩa cần thực hiện 2n – 1 thao tác chuyển. Ta sẽ chứng minh rằng để chuyển n đĩa cần 2n - 1 thao tác chuyển. Thật vậy, theo phương pháp chuyển của giải thuật thì có 3 bước. Bước 1 chuyển n – 1 đĩa từ cọc A sang cọc B mất 2n - 1 thao tác. Bước 2 chuyển 1 đĩa từ cọc A sang cọc C mất 1 thao tác. Bước 3 chuyển n – 1 đĩa từ cọc B sang cọc C mất 2n - 1 - 1 thao tác. Tổng cộng ta mất (2n - 1 - 1) + (2n - 1 - 1) + 1 = 2 * 2n - 1 - 1 = 2n - 1 thao tác chuyển. Đó là điều cần chứng minh. Như vậy, thuật toán có cấp độ tăng rất lớn. Nói về cấp độ tăng này, có một truyền thuyết vui về bài toán tháp Hà Nội như sau: Ngày tận thế sẽ đến khi các nhà sư ở một ngôi chùa thực hiện xong việc chuyển 40 chiếc đĩa theo quy tắc như bài toán vừa trình bày. Với độ phức tạp của bài toán vừa tính được, nếu giả sử mỗi lần chuyển 1 đĩa từ cọc này sang cọc khác mất một giây thì với 240 – 1 lần chuyển, các nhà sư này phải mất ít nhất 34.800 năm thì mới có thể chuyển xong toàn bộ số đĩa này.
2.4.3.5 Bài toán xếp lịch thi đấu
Giả sử cần lập một lịch thi đấu Tennis cho n = 2 vận động viên (VĐV). Mỗi vận động viên phải thi đấu với lần lượt n-1 vận động viên khác, mỗi ngày thi đấu 1 trận. Như vậy n-1 là số ngày thi đấu tối thiểu phải có. Chúng ta cần lập lịch thi đấu bằng cách thiết
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
lập ma trận có n hàng, n-1 cột. Giá trị số tại vị trí (i,j) (hàng i, cột j) chỉ ra vận động viên cần thi đấu với vận động viên i trong ngày thứ j.
Sử dụng kỹ thuật Chia để trị, chúng ta hãy lập lịch thi đấu cho nửa (n/2) số vận động viên đầu tiên. Bằng việc sử dụng lời gọi đệ quy chúng ta đưa bài toán về trường hợp chỉ có 2 vận động viên. Trong trường hợp n=8, lịch thi đấu cho 4 người đầu tiên của danh sách chiếm nửa trái trên của ma trận (4 hàng, 3 cột). Phần nửa trái dưới (4 hàng, 3 cột) của ma trận là lịch thi đấu của 4 VĐV còn lại (từ 5 đến 8). Phần này thu được từ nửa trái trên bằng cách cộng 4 vào mỗi phần tử tương ứng của ma trận. Để điền nốt các phần còn lại của ma trận chúng ta chỉ cần xác định lịch thi đấu giữa các VĐV với số thấp (≤ n/2) với các VĐV với số cao (≥ n/2). Để làm việc này chúng ta xếp các VĐV từ 1 đến n/2 đấu lần lượt với các VĐV số cao vào ngày 4. Các ngày còn lại thu được từ ngày 4 bằng cách hoán vị vòng quanh các VĐV với số thứ tự cao.
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
CHƢƠNG 3. ỨNG DỤNG THUẬT TOÁN CHIA ĐỂ TRỊ GIẢI BÀI TOÁN NHÂN HAI SỐ NGUYÊN LỚN
3.1 Mô tả bài toán
Rất nhiều ứng dụng trong thực tế đòi hỏi phải xử lí các số rất lớn, nằm ngoài khoảng biểu diễn của các kiểu cơ sở của ngôn ngữ lập trình. Để giải quyết các yêu cầu đó, chúng ta phải xây dựng các kiểu số rất lớn và xây dựng các phép toán tương ứng. Trong phần này ta chỉ xét phép toán nhân đối với hai số rất lớn. Giả thiết cả hai đều có n chữ số và được biểu diễn bằng mảng. Bài toán nhân 2 số lớn phát biểu như sau:
- Input. A,B là 2 số nguyên có n chữ số: A=A1A2….An và B=B1B2….Bn. - Output. C=A.B
Trong các ngôn ngữ lập trình đều có kiểu dữ liệu số nhưng nhìn chung các kiểu này đều có miền giá trị hạn chế nên khi có một ứng dụng trên số nguyên lớn (hàng chục, hàng trăm chữ số) thì kiểu số nguyên định sẵn không đáp ứng được. Trong trường hợp đó, người lập trình phải tìm một cấu trúc dữ liệu thích hợp để biểu diễn cho một số nguyên, chẳng hạn ta có thể dùng một chuỗi kí tự để biểu diễn cho một số nguyên, trong đó mỗi kí tự lưu trữ một chữ số. Để thao tác được trên các số nguyên được biểu diễn bởi một cấu trúc mới, người lập trình phải xây dựng các phép toán cho số nguyên. Trong chương này, tôi sẽ đề cập đến bài toán nhân hai số nguyên lớn và các cách tiếp cận để giải quyết.
3.2 Thuật toán nhân tự nhiên
Thuật toán tự nhiên (Brute-force) của bài toán nhân 2 số lớn là giải thuật nhân tay ta vẫn thực hiện: lần lượt nhân từng chữ số của số thứ hai với số thứ nhất, dịch kết quả theo vị trí và cộng các kết quả trung gian lại [7].
Chẳng hạn để nhân A=981 và B=1234 ta tiến hành như sau: 981 1234 --- 3924 + 1943 1962 981 --- 1210554
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
Thuật toán nhân 2 số lớn kiểu nhân tay được mô tả bằng giả mã:
Thuật toán nhân Brute-force Mul_BruteForce(A, B, n) {
T = 0;
for (i = 1; i<= n; i++) {
D = A* B[i];
D = D SHL(i-1);{dịch D sang trái i-1 số, nhân D với 10i-1} T = T + D;
}
return T; };
Hình 3.7Thuật toán nhân Brute-force
Dễ dàng tính được độ phức tạp tính toán của thuật toán này là O(n2). Chúng ta cố gắng giảm bậc của thuật toán với tư tưởng chia để trị. Để đơn giản, ta giả thiết n=2k và tách A,B dưới dạng XY và UV trong đó X,Y,U,V là các số có k chữ số. Như vậy phép nhân 2 số A,B được tính như sau:
A.B = (X.10k + Y).(U.10k+V) = XU.102k + (XV+YU).10k+UV.
Kết quả là bài toán nhân 2 số A.B có 2k chữ số được chia thành 4 bài toán con nhân các số k chữ số và một số phép cộng, trừ. Tuy nhiên, số lượng phép toán vẫn còn nhiều.
3.3 Thuật toán nhân cơ bản
Thuật toán nhân cơ bản (Standard MultiplicationAlgorithm)[7] xem xét 2 số a và b là 2 số gồm s-chữ số (s-words) được biểu diễn dưới dạng cơ số W như sau:
ở đây, ai, bjtrong khoảng [0, W-1].
Thuật toán thực thi theo các bước cụ thể như sau:
- Tại mỗi bước sẽ thực thi nhân theo từng số bjvới các số aitương ứng - Sau đó thực hiện phép cộng
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
trong đó, tij= (Carry, Sum) của phép nhân ai * bj. Dòng cuối cùng thể hiện giá trị của phép nhân, giá trị này gồm 2s-words.
Dưới đây là chi tiết thuật toán nhân cơ bản:
Thuật toán nhân Standard Multiplication Standard Multiplication (A, B, s)
{
Khởi tạo ti = 0 for all i=0,1,…,2s-1;
For (i = 0; i< s; i++) { C = 0; For (j = 0; j < s; j++) (C, S) = ti+j + aj*bi + C; ti+j = S; ti+s = C; } return (t2s-1, t2s-2,…,t0); };
Hình 3.8Thuật toán nhân chuẩn
Sau đây là ví dụ cụ thể thực thi từng bước của thuật toán cơ bản này cho phép nhân:
a*b = 348 * 857
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/
(C, S) = ti+j + aj*bi + C;
Mỗi biến ti+j, aj, bi, C, và S là các số gồm W-bits. Bước này gồm 1 phép nhân (*) 2 số
W-bits và 2 phép cộng (+) với các số gồm W-bits. Vì vậy giá trị lớn nhất cho phép toán tính biểu thức ở bước này là một số 2W-bits:2W– 1 + (2W– 1) (2W– 1) + 2W– 1 = 22W– 1
Dễ thấy, bước tính toán này là bước tính cơ sở nhất trong toàn thuật toán, nên khi cài đặt bước này càng đơn giản, càng hiệu quả thì thuật toán sẽ càng hiệu quả. Thuật toán thực thi s2 lần tính toán cho bước này, và k
s
wvới w là hằng số; vậy thuật toán này có độ phức
tạp là O(k2) (theo toán tử trên bit khi nhân 2 số k-bits với nhau).
Thuật toán nhân cơ bản nhìn chung là đơn giản và dễ cài đặt khi nhân 2 số đủ nhỏ thì cho kết quả gần như tốt bằng các thuật toán tối ưu hơn như Karatsuba-Ofman. Phần tiếp theo luận văn sẽ trình bày thêm 2 thuật toán nhân Karatsuba-Ofman và Thuật toán nhân dựa trên FFT. Đây là 2 thuật toán có độ phức tạp (thời gian/phép toán thực thi) nhỏ hơn nhiều so với thuật toán cơ bản này.
3.4 Thuật toán nhânKaratsuba-Ofman
Thuật toán Karatsuba-Ofman được giới thiệu bởi hai nhà toán học Nga là Karatsuba và Ofman vào năm 1962 [7] sử dụng thuật toán đệ quy đòi hỏi tính gần đúng ít hơn O(k2) phép tính bit để nhân 2 số k-bit. Ta sẽ phân tích thuật toán này, đầu tiên, phân tích a và b thành 2 phần kích thước bằng nhau: 1 0 1 0 2 2 h h a a a b b b
Nghĩa là a1 là h bit cao hơn của a và a0 là h bits thấp hơn của a, giả sử rằng k là chẵn và 2h=k. Khi đó, tác giả chỉ quan tâm đến tính gần đúng của thuật toán, giả sử rằng k là lũy thừa của 2. Thuật toán này chia phép nhân a và b thành nhân những phần a0, a1, b0và
b1. Vì vậy: 1 0 1 0 2 1 1 1 0 0 0 0 0 2 2 1 0 : : 2 2 : 2 2 : 2 2 h h h h h h t a b a a b b a b a b a b a b t t t
Phép nhân 2 số 2h-bits đòi hỏi nhân bốn số h-bit. Công thức này thực hiện một thuật toán đệ quy mà tác giả gọi là thuật toán nhân đệ quy chuẩn (SRMA) như sau:
Thuật toán SRMA SRMA(A, B)
Số hóa bởi Trung tâm Học liệu http://www.lrc-tnu.edu.vn/ { t0 = SRMA(A0, B0); t1 = SRMA(A1, B1); u0 = SRMA(A0, B1); u1 = SRMA(A1, B0); t1 = u0 + u1; return (22ht2+2ht1+t0);