Trong bài này chúng ta thảo luận giải thuật quy hoạch động để giải bài toán Người Du Lịch (Traveling Salesman Problem - TSP). Giải thuật này được đề xuất bởi Held-Karp và cũng được đề xuất độc lập bởi Bellman vào năm 1962.
Bài toán TSP
Bài toán như sau:
Ví dụ: Các thành phố và chi phí giữa chúng được cho bởi hình bên trái của hình dưới đây. Một hành trình tối ưu với tổng chi phí 18 được cho bởi hình bên phải.
Bài toán này có thể được giải bằng phương pháp quay lui với thời gian $O(n!)$. Trong bài này mình sẽ giới thiệu lời giải bằng quy hoạc động với thời gian $O(n^22^n)$ và thảo luận một phương pháp thực thi bằng C.
Giả sử $T[1,2,\ldots]$, $T[1]=1$, là một hành trình tối ưu đi từ thành phố $1$ đến thành phố $T[n]$ qua $n-1$ thành phố khác và quay lại $1$. Vì hành trình $T[1,2,\ldots,n]$ là tối ưu, hành trình $T[2,3,\ldots, n-1]$ đi từ $1$ tới $T[n]$ qua các thành phố $\{2,3,\ldots, n\} \setminus \{T[n]\}$ cũng phải là hành trình tối ưu. Như vậy, nếu gọi $Cost(S,i)$ là chi phí nhỏ nhất đi từ $1$ tới $i$ qua các thành phố trong tập hợp $S$ (không chứa $i$), ta có công thức đệ quy sau:
Cost$[{1},1] = + \infty$
for $s \leftarrow 1$ to $n-1$ do
for each $S \leftarrow$ a subset of $\{1,2,\ldots, n\}$ of size $s$
Cost$[{S},1] + \infty$
for each $i \leftarrow S\setminus\{1\}$
SubtourCost$(S\setminus \{i\},i)$
opt $\leftarrow +\infty$
for $j \leftarrow 2$ to $n$
opt $\leftarrow \min\{$opt,Cost$[\{2,3,\ldots, n\}\setminus \{j\},j] + C[i,j]\}$
return opt
Hàm SubtourCost$(S,j)$ tính chi phí của hành trình từ $1$ đến $j$ qua mỗi thành phố trong $S$ (không chứa $j$) chính xác 1 lần và được tính theo công thức đệ quy ở trên. Giả mã như sau:
Cost$[S, i] \leftarrow +\infty$
for each $j \leftarrow S$
Cost$[S, i] \leftarrow \min\{$Cost$[S, i],$ Cost$[S\setminus \{j\}, j] + C[i,j]\}$
Phân tích thời gian Thời gian tính của thuật toán trên có thể được quy về thời gian cập nhật bảng Cost$[S \subseteq\{2,3,\ldots,n\}][1,2,\ldots,n]$ có kíc thước $O(n2^n)$. Mỗi phần từ của bảng được cập nhật theo thủ tục SubtourCost$(S,j)$ và mất thời gian $O(|S|) = O(n)$. Do đó tổng thời gian tính toán của thuật toán là $O(n^22^n)$.
Thực thi bằng C
Thuật toán quy hoạch động của bài toán TSP cho bởi giả mã DynamicTSP($C[1,2,\ldots, n,1,2,\ldots,n]$): không quá phức tạp. Tuy nhiên khi thực thi thuật toán bằng C hay một số ngôn ngữ khác khi mà các phép toán liên quan đến tập hợp chưa sẵn có thì có một số vấn đề chúng ta cần phải giả quyết:
- Phương pháp sinh các tập con của $\{1,2,\ldots,n\}$
- Mã hóa các tập con sao cho việc truy xuất bảng Cost$[S \subseteq\{2,3,\ldots,n\}][1,2,\ldots,n]$ được thực hiện một cách hiệu quả
- Cách thức lưu trữ các tập con của một tập
Trong phần này mình thảo luận một cách để thực thi bằng $C$ với $n \leq 31$ và giới thiệu một số kĩ thuật xử lí bit. Trước hết đối với vấn đề số 2, chúng ta có thể mã hóa bằng cách gán cho mỗi tập con $S$ của $\{1,2,\ldots, n\}$ một số duy nhất, gọi là id, để việc truy xuất hay cập nhật $C[S,i]$ được hiệu qủa. Ta có thể mã hóa một tập con $S$ của tập $n$ phần tử bằng một số nguyên $n$ bít $a_1a_2\ldots a_n$, trong đó $a_i = 1$ nếu phần tử $i \in S$ và $a_i = 0$ nếu $i \not\in S$. id của $S$ chính là giá trị của số nguyên đó. Ví dụ: $n=5$, tập con $S={1,2,4}$ có thể được biểu diễn bởi số nguyên ở hình dưới đây, và có id $= 01011_2 = 11$. Việc chứng minh không có hai tập con khác nhau nào có cùng một id coi như bài tập cho bạn đọc.
.
Như vậy chúng ta đã có cách để mã hóa mỗi một tập con thành một số nguyên duy nhất tương ứng. Phần tiếp theo của bài viết dùng một số các phép toán xử lí bít. Các phép toán cơ bản gồm có: AND (&), OR (|), XOR (^), NOT (~), SHIFT LEFT (<<), SHIFT RIGHT (>>). Ví dụ một số cách xử lí bít để tính các hàm cơ bản như:
- Lũy thừa 2: $2^n = 1$<<$n$.
- Kiểm tra tính chẵn (lẻ) của số nguyên $a$: $(a\&1) == 0$ ($(a\&1) == 1$).
- Tính minimum (maximum) của hai số nguyên $x,y$: min(x,y) = y^ ((x ^ y) & -(x < y)), max(x,y) = ( x ^ ((x ^ y) & -(x < y)))
Bạn đọc có thể tham khảo thêm ở [2]
Bây giờ ta giải quyết bài toán sinh các tập con kích thước $k$ của tập hợp có kích thước $n$. Ở đây ta sẽ sinh tập con theo thứ tự từ điển. Như vậy ta có thể quy về bài toán cho một tập con $S$, tìm tập con tiếp theo có $k$ phần tử theo thứ tự từ điển. Giả sử tập con $S$ đó được cho dưới dạng mã hóa bit như trên, ta quy về bài toán:
Ví dụ: $n=5, k =3, v = 01011_2$, số nguyên tiếp theo sẽ là $ = 01101_2$. Ta có thể nhận thấy là nếu $v$ có $a$ ($a$ có thể bằng 0) bít cuối cùng là 0, trước đó là $b$ bít 1 (như ở hình bên trái của hình dưới đây), thì $w$ sẽ có $b-1$ bít cuối là 1, trước đó là $a+1$ bít 0 và trước $a+1$ bít 0 đó là 1 bít 1 (như hình bên phải của hình dưới đây).
Như vậy để giải bài toán trên, ta tìm cách chuyển $v$ thành $w$ giống như trong hình trên. Giả mã (gần giống với C) như sau:
unsgined int $w$
unsgined int $t \leftarrow (v | (v-1))+1$
$w \leftarrow t | ((((t \& -t) / (v \& -v))$>>$1) - 1)$
if $w$>>$n = 0$
return $w$
else
return 0
Để bạn đọc không mất tập trung vào giải thuật chính, mình sẽ giải thích đoạn code này ở cuối bài. Code bằng C của giả mã như sau :
int next_set(unsigned int v, int n){ unsigned int w; unsigned int t = (v |(v - 1)) + 1; w = t | ((((t & -t) / (v & -v)) >> 1) - 1); if(w >> n == 0)return w; else return 0; }
Bài toán tiếp theo chúng ta phải giải đó là liệt kê các tập con:
Với mỗi tập $S$, mình dùng danh sách liên kết để lưu các tập con của $S$ có kích thước $|S|-1$. Đầu của danh sách lưu $S$. Mỗi mắt xích gồm hai trường: trường id lưu mã của tập con $S'$ và trường x lưu phần tử $x = S\setminus S'$. Giả mã được cho ở dưới, seed là id của tập con $S$.
node *head = NewNode()
head$\rightarrow$id = seed;
node *iterator $=$ head
for $i \leftarrow 0$ to $n-1$
if ((seed>>$i$)&1) = 1 [[check $(i+1)$-th bit to be 1]]
node *elem = NewNode()
elem$\rightarrow$id $=(1$<<$i)$^seed [[set $(i+1)$-th bit of seed to 0]]
elem$\rightarrow$x $=i+1$
iterator$\rightarrow$next $=$ elem;
iterator $=$ iterator$\rightarrow$next.
return head
Code bằng C của giả mã:
struct node *generate_subset(unsigned int seed, int n){ struct node *head; head = malloc( sizeof(struct node)); head->next = NULL; head->id = seed; struct node *iterator = head; int i = 0; for(i = 0; i< n; i++){ if(((seed >> i)&1) == 1){ struct node* elem = malloc( sizeof(struct node) ); elem->id = (1<<i)^seed; elem->x = i+1; elem->next = NULL; iterator->next = elem; iterator = iterator->next; } } return head; }
Dựa vào thủ tục GenerateSubset(int seed, $n$), ta có thể tính trước mảng Subsets$[1,2,\ldots, 2^n-1]$, trong đó mỗi phần tử của mảng Subsets$[i]$ là một danh sách liên kết lưu các tập con có kích thước $|S|-1$ của tập $S$ có id là $i$. Giả mã như sau:
for $i \leftarrow 1$ to $2^n-1$
Subsets$[i] \leftarrow $ GenerateSubset$(i, n)$
Code của giả mã bằng C:
void generate_all_subsets(struct node *subsets[], int size, int n){ int i = 0; for(i = 1; i < size; i++){ subsets[i] = generate_subset(i,n); } }
Như vậy, chúng ta đã giải quyết xong bài toán mã hóa các tập con và sinh các tập con của một tập $S$ có kích thước $|S|-1$. Vấn đề còn lại chỉ là ghép các phần lại với nhau để giải quyết bài toán. Giả mã của thuật toán như sau:
size $1$<<$n$
node *Subsets$[1,2,\ldots,$size$] \leftarrow $ GenerateAllSubsets$(n)$;
opt $\leftarrow +\infty$
for $s \leftarrow 2$ to $n-1$ do
set $\leftarrow (1$<<$s)-1$ [[first set in the lexicographic order]]
repeat
if (set&$1 = 0$) [[set does not contain 1]]
node *subsetList $=$ Subsets[set]$\rightarrow$next [[skip the head]] for each elem of subsetList
$S =$ elem$\rightarrow$id
$i = $ elem$\rightarrow$x
SubtourCost(S, i):
if $s = n-1$
opt $\leftarrow \min\{$opt, Cost$[S, i] + C[i,1]\}$
set = NextSet(set, $n$)
until set $= 0$
return opt
Code bằng C của giả mã:
int dynamic_tsp (int n){ unsigned int set; int size = 1 << n; struct node *subsets[size]; generate_all_subsets(subsets,size, n); struct node *subset; struct node *iterator; int i = 0, s = 2; int opt_tour = INFTY; for(s = 2; s < n; s++){ set = (1<<s)-1; // the first set of size s in lexicographic order while(1){ if((set&1) == 0){ subset = subsets[set]; // subsets of the set "set" iterator = subset->next; // root holds no information while(iterator != NULL){ subtour_cost(subsets, iterator->id, iterator->x); if(s == n-1){// save Cost[S][i] for S such that |S| = n-2 opt_tour = min(opt_tour,Cost[iterator->id][iterator->x] + C[iterator->x-1][0]); } iterator = iterator->next; } } set = next_set(set, n);// generate next subset in lexicographic order if(set == 0){ // no more subset break; } } } return opt_tour; }
Thủ tục SubtourCost(S, i) có giả mã như sau:
node *subsetList $=$ Subsets$[S]\rightarrow$next [[skip the head]]
if susbet$\rightarrow$id $ = 0$ [[$S$ has size 1]]
Cost$[S,i] \leftarrow C[1,$ subset$\rightarrow x] + C[$subset$\rightarrow x, i]$
else
Cost$[S,i] = +\infty$
for each elem of subsetList
Cost$[S,i] = \min\{$ Cost$[S,i],$ Cost[elem$\rightarrow$id, elem$\rightarrow$x] + C[elem$\rightarrow$x, $i]\}$
Code của giả mã bằng C:
void subtour_cost(struct node *subsets[], int set_id, int i){ struct node *subset = subsets[set_id]; struct node *iterator = subset->next; if(iterator->id == 0){// if the set with id set_id has only one element Cost[set_id][i] = C[0][iterator->x-1] + C[iterator->x-1][i-1]; }else{ Cost[set_id][i] = INFTY; while(iterator != NULL){ Cost[set_id][i] = min(Cost[set_id][i], Cost[iterator->id][iterator->x] + C[iterator->x-1][i-1]); iterator = iterator->next; } } }
Hàm NextSet(unsigned int $v$, int $n$)
Các thao tác trong hàm NextSet(unsigned int $v$, int $n$) được minh hoạ bởi hình sau:
Code: tsp-code, tsp-data
Tham khảo
[1] Sanjoy Dasgupta, Christos Papadimitriou, and Umesh Vazirani. Algorithms. 1st Edition). McGraw-Hill Higher Education, (2008).
[2] Sean Eron Anderson, Bit Twiddling Hacks .
Recent Comments