Bài toán nhân ma trận và Quy hoạch động
Trang 1Quy hoạch động với bài toán nhân ma trận
Đỗ Quốc Trí
Như ta đã biết quy hoạch động là một phương pháp phổ biến để giải các bài toán, và nếu bạn đã học lập trình thì không thể không biết đến phương pháp này Trong bài viết tôi muốn đề cập đến phương pháp này với bài toán nhân ma trận đơn giản tiến tới áp dụng giải các bài toán thực tế phức tạp hơn
Cho ma trận A có kích thước p*q và ma trận B có kích thước q*r thực hiện phép nhân ma trận để có ma trận C có kích thước p*r
C[i,j] = ồ A[i,k]*B[k,j] (k : 1 ->q ; 1 <= i <= p , 1 <= j <= q)
Vi dụ:
A * B = C
Ma trận A có kích thước 3*4, Ma trận B có kích thước 4*5, Ma trận C có kích thước 5*3
Để nhân ma trận ta Ăp*q) voi B (q*r) ta thực hiện chương trình sau
For i := 1 to p do
For j := 1 to r do
Begin
C[i,j] := 0 ;
For k := 1 to q do C[i,j] := C[i,j] + A[i,k] * A[k,j] ;
end ;
Dễ dàng thấy để nhân 2 ma trận này với nhau ta cần phải dùng p *q* r phép nhân, ta gọi đây là phí tổn để nhân 2 ma trận này
Vấn đề đặt ra là nhân một dãy ma trận M1 , M2 , M3 …Mn ;
Ta chú ý là phép nhân ma trận khôngcó tính chất giao hoán nhưng có tính chất kết hơp
Ví dụ (A* B)*C = A*(B*C) ;
Dữ liệu vào File MATRAN.INP
Trang 2Dòng đầu ghi n
Dòng thứ 2 ghi n+1 so a[1] a[n+1] với ý nghĩa kích thước của ma trận M[i] là
a[i]*a[i+1]
Dữ liệu ra File MATRAN.OUT
Ghi số n là số phép nhân cần thực hiện
Ví dụ:
Bây giờ ta phân tích bài toán
Nếu n = 1 thì số phép nhân sẽ là 0;
Nếu n = 2 thì ta cung tính được số phép nhân như đã nói ở trên
Xét n >=3:
Ta gọi F[i,j] là chi phí cần dùng để nhân dãy ma trận M[i] , M[i+1] M[j]
Để nhân dãy ma trận này ta có cách kết hợp sau:
M[i]* M[i+1]*… M[j] = (M[i]* M[i+1]*…M[k])*( M[k+1]* M[k+2]*… M[j]) (i≤k<j)
Nếu ta biết F[i,k] và F [k+1,j] với mọi i ≤ k < j thì ta sẽ tính được F [i,j] theo công thức
F [i,j]:= min(F [i,k] * F [k+1,j]) với mọi i≤k<j
Vậy ta có thể tính được công thức truy hồi bằng chương trình quy hoạch động sau: Procedure Optimize ;
Begin
For i := 1 to n do
For j := 1 to n do
IF i <> j then F [i,j] := maxlongint
Else F [i,j] := 0 ; {F[i,i] = 0 với mọi i }
For l := 1 to n-1 do {l là độ dài của dãy}
For i := 1 to n - l do
Begin
j := i+l ;
For k := i to j-1 do {k chạy từ i tới j-1}
if F [i,j] > F [i,k] + F [k+1,j] + a[i] * a[k+1] * a[j+1] then
F[i,j] := F[i,k] + F[k+1,j] + a[i] * a[k+1] * a[j+1] ;
{tối ưu F[i,j]}
End;
End;
Và ta chỉ cần viết ra output là F[1,n]
Nếu đề bài bắt ta truy vết thì ta chỉ cần lưu thêm mảng Trace với Trace[i,j] = k ;
Bây giờ là một bài toán áp dụng của bài toán này
Cho mảng gồm 3 phần tử a, b, c ta xác định phép toán nhân sau
Trang 3chẳng hạn ta có aa = b ; ac = c ; chú ý ở đây không có tính kết hợp và giao hoán
Bài toán đặt ra là cho 1 sâu có độ dài không quá 100, hãy tìm cách đặt dấu ngoặc để thể hiện phép nhân sao cho kết quả là a
Input GIATRI.INP: ghi sâu S
Output GIATRI.OUT nếu không đạt được thì ghi 0 nếu đạt được thì ghi cách đặt
Ví dụ:
Trước tiên ta phân tích bài toán
Khởi tạo mảng F[i,j,u] với 1<=i, j <= 100, u = ’a’ ’c’ kiểu Boolean với ý nghĩa
Có thể biến đổi được từ kí tự i đến kí tự j thành k được không (F[i,j] = true nếu được,
= false nếu ngược lại)
ở đây ta có F[i,j,u] = true nếu tồn tại k sao cho F[i,k,x] = true và F[k+1,j,y] = true và
xy = a; (x , y = ’a’ c);
Để ý thấy ta hoàn toàn có thể tiết kiệm bộ nhớ bằng cách để mảng F kiểu byte để ta dễ dàng truy vết lúc về sau
F[i,j,u] = 0 nếu không thể phân tích được
F[i,j,u] = k nếu F[i,k,x] <> 0 và F[k+1,j,y] <> 0 và xy = u;
Cách tính mảng F ta hoàn toan áp dụng bài toán trên
Ta sẽ trình bày thuật toán như sau:
Const
InputFile = ’GIATRI.INP’ ;
OutputFile = ’GIATRI.OUT’ ;
max = 100 ;
X : array[’a’ ’c’, ’a’ ’c’] of char = ((’b’,’b’,’a’) , (’c’,’b’,’a’) ,(’a’,’c’,’c’));
{X là mảng để lưu phép nhân}
Var
A : string ;
F : array[1 max , 1 max,’a’ ’c’] of byte ;
i , j , n , l , k: integer ;
c , b : char ;
Fi , Fo : text ;
Procedure enter ; { nhập dữ liệu }
begin
Assign(Fi, InputFile) ; Reset(Fi) ;
Assign(Fo , OutputFile) ; Rewrite(Fo ) ;
Readln(Fi , A);
n := length(A) ;
end;
Procedure Init ; { Khởi tạo mảng F }
begin
Fillchar(F , sizeof (F) , 0) ;
For i := 1 to n do
begin
Trang 4if i < n-1 then
F[i,i+1,X[A[i],A[i+1]]] := i ;
F[i,i,A[i]] := 101 ;
end;
end;
Procedure optimize ; {Ch ương trình quy hoạch động }
begin
for l := 2 to n-1 do
For i := 1 to n-l do
For k := i to i+l-1 do
For b := ’a’ to ’c’ do
if F[i,k,b] <> 0 then
For c := ’a’ to ’c’ do
if F[k+1,i+l,c] <> 0 then
F[i,i+l,X[b,c]] := k ;
end;
Procedure trace(i,j : byte; c : char) ; {dùng đệ quy để truy vết in kết quả } var
k : byte ;
u , v : char ;
begin
if i = j then
begin
write(Fo,A[i]) ;
exit ;
end;
Write(Fo , ’(’) ;
k := F[i,j,c] ;
for u := ’a’ to ’c’ do
For v := ’a’ to ’c’ do
if (F[i,k,u] <> 0) and (F[k+1,j,v] <> 0) and (x[u,v] = c) then
begin
Trace(i,k,u) ; trace(k+1,j,v) ;
end;
Write(Fo , ’)’) ;
end;
{ Ch ương trình chính }
Begin
Enter ;
Init ;
Optimize ;
If F[1,n,’a’] = 0 then write(Fo , 0)
else Trace(1,n,’a’) ;
Close(Fo) ;
Trang 5End
Ta cũng có thể dùng chương trình đệ quy trên để ghi ra cách nhân trong bài nhân ma trận
Từ các bài toán trên ta rút ra công thúc truy hồi cho các bài có dạng như trên:
F[i,j] := cấu hình tốt nhất (F[i,k] , F[k+1,j]) với i <= k < j
Các bạn có thể tham khảo bài toán sau
Cho n quân bài đặt liền nhau, trên quân bài thứ i có ghi số A[i] Nếu bạn rút quân bài k
thì bạn sẽ nhận số tiền là A[k-1] * A[k] *A[k+1] (1<k<n) Bài toán đặt ra là hãy rút n -2
sao cho số tiền nhận được là lớn nhất
Input
Dòng 1: ghi số n
Dòng thứ 2: ghi n số, số thứ i là số ghi trên quân bài thứ i
Output
Ghi số tiền lớn nhất nhận được