Quy hoạch động -- Dynamic Programming V

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:

Problem 5: Có $n$ thành phố đánh số từ $1$ đến $n$ và các tuyến đường giao thông hai chiều giữa chúng, mạng lưới giao thông này ñược cho bởi mảng $C[1..n,1..n]$, ở đây $C[i,j]$ là chi phí đi đoạn đường trực tiếp từ thành phố $i$ đến thành phố $j$. Một người du lịch xuất phát từ thành phố $1$, muốn đi thăm tất cả các thành phố còn lại mỗi thành phố đúng $1$ lần và cuối cùng quay lại thành phố $1$. Hãy tìm hành trình với chi phí ít nhất cho người đó.

 
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.
tsp-ex-fig

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:

 

Do đó, để tính $Cost(S,i)$, ta chỉ cần đảm bảo các giá trị $C(S',j)$ với $S' = S\backslash \{j\}$ đã được tính trước. Giả mã như sau:

DynamicTSP($C[1,2,\ldots, n,1,2,\ldots,n]$):
    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:

SubtourCost($S,i$):
    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:

  1. Phương pháp sinh các tập con của $\{1,2,\ldots,n\}$
  2. 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ả
  3. 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.
set-encode.

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:

Problem: Cho một số nguyên $n$-bít $v$ có $k$ bít $1$. Tìm số nguyên $n$-bít $w$ tiếp sau $v$ theo thứ tự từ điển và có $k$ bít $1$.

 
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).
bit-dic-ex
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:

int NextSet(unsigned int $v$, int $n$):
    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 &amp; -t) / (v &amp; -v)) &gt;&gt; 1) - 1);
if(w &gt;&gt; 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:

Problem: Cho một tập $S \subseteq \{1,2,\ldots, n\}$ với kích thước $|S|$, liệt kê các tập con của $S$ có kích thước $|S|-1$.

 
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$.

GenerateSubset(int seed, $n$):
    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-&gt;next = NULL;
head-&gt;id = seed;
struct node *iterator = head;
int i = 0;
for(i = 0; i&lt; n; i++){
if(((seed &gt;&gt; i)&amp;1) == 1){
struct node* elem =  malloc( sizeof(struct node) );
elem-&gt;id = (1&lt;&lt;i)^seed;
elem-&gt;x = i+1;
elem-&gt;next = NULL;
iterator-&gt;next = elem;
iterator = iterator-&gt;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:

GenerateAllSubsets($n$):
       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 &lt; 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:

DynamicTSPinC($A[1,2,\ldots, n]$):
    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 &lt;&lt; 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 &lt; n; s++){
set = (1&lt;&lt;s)-1; // the first set of size s in lexicographic order
while(1){
if((set&amp;1) == 0){
subset = subsets[set]; // subsets of the set "set"
iterator = subset-&gt;next; // root holds no information
while(iterator != NULL){
subtour_cost(subsets, iterator-&gt;id, iterator-&gt;x);
if(s == n-1){// save Cost[S][i] for S such that |S| = n-2
opt_tour = min(opt_tour,Cost[iterator-&gt;id][iterator-&gt;x] + C[iterator-&gt;x-1][0]);
}
iterator = iterator-&gt;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:

SubtourCost($S,i$):
    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-&gt;next;
if(iterator-&gt;id == 0){// if the set with id set_id has only one element
Cost[set_id][i] = C[0][iterator-&gt;x-1] + C[iterator-&gt;x-1][i-1];
}else{
Cost[set_id][i] = INFTY;
while(iterator != NULL){
Cost[set_id][i] = min(Cost[set_id][i], Cost[iterator-&gt;id][iterator-&gt;x] + C[iterator-&gt;x-1][i-1]);
iterator = iterator-&gt;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:
next-set-fig

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 .

Facebook Comments

Tags: , , ,

  1. Nguyễn Hòa’s avatar

    Ad giải thích hộ mình đoạn công thức đệ quy với. đoạn minj thuộc s ấy. minj là gì và tính thế nào vậy ?

    Reply

    1. Hùng Lê’s avatar

      Hi bạn Hòa,

      Cám ơn bạn đã đặt câu hỏi. Theo như định nghĩa của hàm Cost: là chi phí nhỏ nhất đi từ tới i qua các thành phố trong tập hợp (không chứa i). Do đường đi với chi phí nhỏ nhất đi từ tới i chỉ qua S, đường đi này phải đi qua một đỉnh nào đó thuộc trước khi đến i. Tức là đường đi có dạng . Do đó . Tuy nhiên ở đây ta không biết cụ thể mà chỉ biết thuộc . Do đó, ta phải duyệt qua tất cả các có thể thuộc và lấy min. Hi vọng đoạn này giải đáp thắc mắc của bạn.

      Hùng

      Reply

      1. Nguyễn Hòa’s avatar

        Mình hiểu rồi. Cảm ơn bạn nhiều lắm

        Reply

        1. Hùng Lê’s avatar

          Không có gì đâu. Cám ơn bạn đã ghé thăm.

          Reply

        2. Nguyễn Tuấn’s avatar

          anh cho e hỏi file tsp-code được viết theo thuật toán quay lui hay quy hoạch động vậy . tại e thấy ghi trong file là quay lui

          Reply

        3. Nguyễn Tuấn’s avatar

          Nếu em muốn hiện tất cả những thành phố đi qua thì nên làm ntn . anh gợi ý chút cho e được không

          Reply

        4. Crewst’s avatar

          em muốn truy xuất lại những thành phố đi qua thì làm như thế nào ạ?

          Reply

Reply

Your email address will not be published. Required fields are marked *