Giả sử chúng ta cần giải quyết bài toán tối ưu tổ hợp với mô hình tổng quát như sau:
Min{f(x): x €D}
Trong đó D là tập hữu hạn các phần tử, D được mô tả như sau:
D={(x1,x2,…,xn); x thỏa mãn tính chất P}
Với giả thiết nêu trên, chúng ta có thể sử dụng thuật toán quay lui để liệt kê các lời giải của bài toán trên. Ta sẽ xây dựng dần các thành phần của phương án.
Một bộ phận k thành phần (a1,a2,…,ak) xuât hiện trong quá trình thực hiện thuật toán được gọi là phương án bộ phận cấp k của bài toán.
Thuật toán nhánh cận có thể được áp dụng cho bài toán đặt ra nếu như có thể tìm được một hàm g xác định trên tập tất cả các phương án bộ phận của bài toán thỏa mãn bất đẳng thức sau:
g(a1,a2,…,ak) min{f(x), x thuộc D, xi=ai, i=1,2,3,..,k} (*)
Với mọi lời giải bộ phận (a1,a2,…,ak) với mọi k=1,2,3,…,n
Bất đẳng thức (*) có nghĩa là giá trị của hàm tại phương án bộ phận (a1,a2,…,ak) không vượt quá giá trị nhỏ nhất của hàm mục tiêu của bài toán trên tập các phương án.
Giả sử ta đã tìm được hàm g, ta sẽ sử dụng hàm g để hạn chế khối lượng duyệt trong bài toán quay lui. Trong quá trình liệt kê các phương án của bài toán, có thể đã thu được một số phương án. Gọi X là giá trị hàm mục tiêu nhỏ nhất trong số các phương án đã duyệt, ký hiệu f=f(X). Ta gọi X là phương án tốt nhất hiện có và f là kỷ lục. Khi đó, nếu:
g(a1,a2,…,ak)≥ f(**)
Như vậy tập con các phương án của bài toán D(a1,a2,...,ak) không chứa phương án tối ưu. Trong trường hợp này, ta có thể loại bỏ các phương án trong tập hợp D(a1,a2,...,ak).
Thuật toán quay lui được thay đổi như sau:
//Phát triển thuật toán quay lui có kiểm tra cận dưới trước khi tiếp tục phát triển phương án void try(int k) { for ak Ak {if (chấp nhận ak) { xk=ak; if(k==n)<cập nhật kỷ lục mới> else if(g(a1,a2,…,ak) f) try(k+1) }
}
}
Thuật toán nhánh cận mô tả như sau: void nhanh_can()
{
f =;
//Nếu đã biết một phương án X nào đó thì có thể đặt f=f(X) try(1);
if(f<) <f là giá trị tối ưu và X là lời giải bài toán> else <Không tồn tại lời giải bài toán>
}
Việc xây dựng hàm g phụ thuộc vào từng bài toán cụ thể nhưng cố gắng sao cho hàm g tính toán không quá phức tạp và giá trị g(a1,a2,…,ak) sát với hàm mục tiêu.
Ví dụ giải “Bài toán người du lịch” bằng thuật toán nhánh cận:
Phát biểu bài toán: Một người du lịch muốn đi thăm n thành phố T1,T2,…,Tn. Xuất phát từ một thành phố nào đó, người du lịch đi qua tất cả các thành phố còn lại, mỗi thành phố đi qua đúng 1 lần sau đó quay trở về thành phố xuất phát. Biết cij là chi phí đi từ thành phố Ti đến thành phố Tj (i,j=1,2,…,n), hãy tìm hành trình với tổng chi phí nhỏ nhất (một hành trình là 1 cách đi thỏa mãn điều kiện).
Phân tích bài toán:
Cố định thành phố xuất phát là T1, bài toán dẫn đến tìm cực tiểu của hàm:
f(x1,x2,...,xn)=c[x1,x2]+ c[x2,x3]+…+ c[xn-1,xn]+ c[xn,x1]+ -> min với
cmin={min c[i,j] là chi phí đi lại giữa các thành phố i,j=1,2,..n và i j}
Giả sử ta đã có phương án bộ phận (u1,u2,…,uk) tương ứng với hành trình giữa các thành phố T1->T2->…->Tk.
Chi phí theo hành trình bộ phần này hiện tại: ∂ =c[u1,u2]+…+ c[uk-1,uk]
Để phát triển bộ phận này thành hành trình của bài toán, ta phải đi thêm (n-k) thành phố còn lại và quay lại thành phố xuất phát T1, như vậy cần đi qua (n-k+1) thành phố. Chi phí đi qua 2 thành phố bất kỳ không nhỏ hơn chi phí nhỏ nhất cmin.
Do đó, cận dưới cho phương án bộ phận này:
Ta có, cmin=2. Quá trình thực hiện bài toán được mô tả bằng cây tìm kiếm lời giải Thông tin về một phương án bộ phận được đưa ra theo thứ tự sau:
Đầu tiên là các thành phần của phương án.
Tiếp đến ∂ là chi phí theo hành trình bộ phận g là cận dưới
Kết thúc thuật toán ta thu được lời giải (1,2,3,5,4,1) là phương án tối ưu tương ứng với hành trình: T1->T2->T3->T5->T4->T1
Chi phí nhỏ nhất là 22.
Hình 2.2. Lời giải bài toán người du lịch
Minh họa cài đặt lời giải bài toán: #include <cstdlib>
#include <iostream> #include <fstream>
using namespace std; int n,c[100][100],x[100],chuaxet[100],kq[100],A[20]; int MIN=0; int a=1; void doc_file() {
ifstream SoChan ("Dulich.inp"); if(! SoChan.is_open())
{
cout<<"Khong the mo file.\n"; } else { for(int i = 1; i <20; i++) { SoChan>>A[i]; }
cout<<"Bat dau doc file"<<endl; for(int i = 1; i <20; i++) { cout<<A[i]<<endl; } } SoChan.close(); } void Init(){
cout<<"\n So thanh pho can di qua: "<<A[1]; cout<<"\n Chi phi de di qua tung thanh pho: \n"; n=A[1]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++){ if(i!=j){ c[i][j]=A[3*i+1]; cout<<"c["<<i<<"]["<<j<<"]="<<c[i][j]<<endl;
}
else c[i][j]=0; }
x[1]=1;
for(int i=2;i<=n;i++) chuaxet[i]=1; }
void Result(){
ofstream ketqua ("Dulich.out"); cout<<"\n T1->"; ketqua<<"\n T1->"; for(int i=2;i<=n;i++) { cout<<"T"<<kq[i]<<"->"; ketqua<<"T"<<kq[i]<<"->"; } cout<<"T1"; ketqua<<"T1";
cout<<"\n Tong chi phi la: "<<MIN<<endl; ketqua<<"\n Tong chi phi la: "<<MIN<<endl; ketqua.close(); } void Work(){ int S=0; for(int i=1;i<=n-1;i++){ S=S+c[x[i]][x[i+1]]; } S=S+c[x[n]][1]; if(S<MIN||a==1){ a=0; MIN=S;
for(int i=1;i<=n;i++) kq[i]=x[i]; }
}
void Try(int i){
for(int j=2;j<=n;j++){ if(chuaxet[j]){ x[i]=j; chuaxet[j]=0; if(i==n) Work(); else Try(i+1); chuaxet[j]=1; } } }
int main(int argc, char *argv[]) { doc_file(); Init(); Try(2); Result(); system("PAUSE"); return EXIT_SUCCESS; }
CHƯƠNG 3: NGĂN XẾP, HÀNG ĐỢI VÀ DANH SÁCH LIÊN KẾT
Chương này tập trung làm rõ phương pháp sử dụng, kỹ thuật lập trình trên các kiểu dữ liệu trừu tượng bao gồm: Ngăn xếp, hàng đợi, danh sách. Các kiểu dữ liệu trừu tượng này không chỉ áp dụng trong các ứng dụng lập trình mà còn sử dụng để giải quyết nhiều bài toán khác nhau trong máy tính.