題目來源:
HihoCoder1312
題目描述:
給出一個九宮格的拼圖遊戲的棋局,求完成拼圖最少需要一定的步數。
解答:
·規則:
首先簡要說明遊戲規則。
遊戲的棋局如下:
九宮格中放置8個標有不同數位棋子,其中一個位置為空白,通過移動棋子,使得數字有序排列,則遊戲完成,如下:
在移動的過程中,只有和空白位置相鄰的棋子才可以移動,並僅可以移動到空白位置。下面的例子中可以通過6次移動完成遊戲:
以上為遊戲規則。
·編碼:
本題的思路還是比較簡單的。通過dijkstra演算法,計算每個棋盤狀態到最終狀態的最短路徑即可。這裡的“最短路徑”定義為最少的移動的次數。那麼首先的問題就是以某種方式記錄棋局的狀態。方法如下:
假設空白位置記為數字9,那麼不同的棋局的數目為:9。= 362880種。如果以字典序將所有的相片順序排序的話,那麼每個排列結果就會有唯一的一個編號。因此,編號為1的排列為:123456789,編號為362880的排列為:987654321。所以,目前的問題是給出一個排列,找出它在字典序中的編號。這個問題可以採用一種叫做“康托展開”的方法來求解,方法如下:
對於某一個排列:s[1,2,...9], 定義序列a[1,2,...,9],其中a[i]表示序列s[i+1, i+2, ... 9]中比a[i]小的數值的個數, 例如,排列 9 1 2 3 6 4 7 8 5 對應的a[i] 序列為:8 0 0 2 0 1 0 0。
在給出a[i]序列後,排列s對應的序號為:
X = a[1] * (9 - 1)! + a[2] * (9 - 2)! + ... + a[9] * (9 - 9)!
遊戲的目標結果為:1 2 3 4 5 6 7 8 9,對應的序號為1。
·解碼:
在知道編碼方法後,我們還需要知道如何在給定編號X的情況下,找到對應的排列結果。這個過程是康托展開的逆過程,可以稱為“逆康托展開”。方法如下:
① 用X除以(9-1)!得到商q和餘數r。
② 從1,2,...9中的尚未使用過的數字中選擇第q+1小的數字,該數字就是s[1]。
③ 令: X=r。
④ 反覆執行步驟①-③,在每次迭代中依次令X除以(9-2)!, (9-3)!, ..., (9-9)!,就可以依次得到s[2], s[3]...s[9],進而得到對應的排列結果。
下面給出一個例子,求解1-9的排列中,排在第5000的排列結果:
① 令:X=5000, X ÷ (9-1)! = 0...........5000, q=0 r=5000, 當前未使用的數字有{1,2,3, 4,5,6,7,8,9},選擇第1小的數字,即1, 因此s[1] = 1。
② 令:X=5000, X ÷ (9-2)! = 0...........5000, q=8 r=5000, 當前未使用的數字有{2,3,4,5,6,7,8,9},選擇第1小的數字,即2, 因此s[2] = 2。
③ 令:X=5000, X ÷ (9-3)! = 6...........680, q=6 r=680, 當前未使用的數字有{3,4,5,6,7,8,9},選擇第7小的數字,即9, 因此s[3] = 9。
④ 令:X=680, X ÷ (9-4)! = 5...........80, q=5 r=80, 當前未使用的數字有{3,4,5,6,7,8},選擇第6小的數字,即8, 因此s[4] = 8。
⑤ 令:X=80, X ÷ (9-5)! = 3...........8, q=3 r=8, 當前未使用的數字有{3,4,5,6,7},選擇第4小的數字,即6, 因此s[5] = 6。
⑥ 令:X=8, X ÷ (9-6)! = 1...........2, q=1 r=2, 當前未使用的數字有{3,4,5,7},選擇第2小的數字,即4, 因此s[6] = 4。
⑦ 令:X=2, X ÷ (9-7)! = 1...........0, q=1 r=0, 當前未使用的數字有{3,5,7},選擇第2小的數字,即5, 因此s[7] = 5。
⑧ 令:X=0, X ÷ (9-8)! = 0...........0, q=0 r=0, 當前未使用的數字有{3,7},選擇第1小的數字,即3, 因此s[8] = 3。
⑨ 令:X=0, X ÷ (9-9)! = 0...........0, q=0 r=0, 當前未使用的數字有{7},選擇第1小的數字,即7, 因此s[9] = 7。
因此,排在第5000位的排列結果為:1 2 9 8 6 4 5 3 7。
·搜尋:
在知道如何將棋局的狀態表示為數字後,就可以進行搜尋了。我們可以將整個遊戲抽象為一個有向圖,圖中的點為各個不同的棋局狀態。而如果兩個狀態可以通過移動棋子一次達到互相轉換,那麼,兩個點之間就包含一條長度為1的無向邊:
構建無向圖的工作完成後,可以採用兩種方式進行搜尋:
方法一:從終點到起點
以最終局面——狀態1為起點,運用dijkstra演算法進行搜尋,就可以知道每一個狀態到最終狀態的最短距離,然後在接下來給定初始狀態後,就可以直接找到對應的最少移動次數。此方法只要搜尋一次,就可以對所有的輸入的初始狀態直接給出答案。
方法二:從起點到終點
也可以在給定起始狀態的情況下,進行搜尋到達終點的最短距離。此時,對於每一組給出的起始位置,都需要進行一次搜尋。為了提高效率,可以採用一種“啟發學習法”的搜尋方式對於dijkstra演算法進行改進。方法如下:
啟發學習法搜尋的思想是:設計一個評估函數F,使用評估函數對於每一個點評估,然後在搜尋的過程中優先考慮評估結果較好的點。這樣就可以有更高的機率優先找到通向終點的最短路徑。
評估函數F由兩部分組成:F = G + H,其中G表示從起點到當前點的最短路徑長度,初始狀態為0,在搜尋過程中逐步對G進行賦值;H則表示從當前點到終點的最短距離路徑,函數H需要在搜尋前進行預估。
搜尋過程中維護兩個集合:openSet和closeSet,初始狀態時,兩個集合均為空白。搜尋步驟如下:
① 將起點的G值設定為0,並設定為當前點。
② 將當前點鍵入到closeSet中。
③ 搜尋所有和當前點鄰接的點,如果該點已經在closeSet中,則不做操作,如果該點在openSet中,則更新其G值,比較當前點的G值加1和該臨界點已有的G值,取較小者;如果該臨界點不在openSet中,則設定該點的G值為當前點G值加1,然後將其加入到openSet中。
④ 選擇openSet中F值最小的點作為當前點。
⑤ 反覆執行步驟②-④,直到openSet為空白或重點已經加入到了closeSet,此時就找到了起點到終點的最短路徑。
注意為了保證演算法的正確性,評估函數H對於每一個點的函數值一定不能大於其到終點的實際距離。否則在從openSet選擇當前搜尋點時,就可能會出錯,導致無法得到最優解。對於本題,我們則可以定義函數H為:目前狀態中的1-8的8個數位當前位置到目標狀態中的8個對應位置的曼哈頓距離之和。由於每次移動會改變一個棋子的位置,導致該棋子的位置到目標位置的曼哈頓距離增大或者減小,如果每次移動都是減小移動棋子到目標位置的曼哈頓距離,那麼總的移動次數就是8個棋子到目標位置的曼哈頓距離之和。如果在移動的過程中有些操作會使得移動的棋子的曼哈頓距離變大,那麼總的移動次數還要更多。因此這樣定義函數H是正確的。
下面的代碼採用的是方法一。
輸入輸出格式:
輸入:第1行:1個正整數t,表示資料群組數;接下來有t組資料,每組資料有3行,每行3個整數,包含0-8,每個數字只出現一次,其中0表示空位。
輸出:第1..t行:每行1個整數,表示該組資料解的步數。若無解輸出"No Solution!"
資料範圍:
1≤t≤8
程式碼:
/****************************************************//* File : Hiho_Week_100 *//* Author : Zhang Yufei *//* Date : 2016-05-30 *//* Description : HihoCoder ACM program. (submit:g++)*//****************************************************/#include<stdio.h>#include<stdlib.h>#define NUM 362880typedef struct node1 vertex;typedef struct node2 edge;/* * Define structure to store the node of graph. * Parameters: *@distance: The distance between the current node and the start node. *@edges: The link list of all the nodes which are adjacent with this node. *@tag: Mark if the node has been visited. */typedef struct node1 {int status;int index;int distance;edge *edges;int tag;} vertex;/* * Define structure to store the edges of the graph. * Parameters: *@index: The end point of this edge. * @next: The next pointer. */typedef struct node2 {vertex* v;struct node2 *next;} edge;// Record the graph.vertex graph[NUM];// Record the multiply value.int multiply[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};// Record map.int map[9];// Record the Set.vertex *heap[NUM + 1];// The size of heap.int heap_cnt;/* * This function compare the distance of the 2 elements in heap. * Parameters: *@a & @b: The 2 elements to compare. * Returns: *If @a is greater than @b, returns 1; or if @a is equal to @b, returns 0; *or returns -1; */int cmp(int a, int b) {if(heap[b]->distance == -1) {return -1;} else if(heap[a]->distance == -1) {return 1;}else if(heap[a]->distance > heap[b]->distance) {return 1;}else if(heap[a]->distance == heap[b]->distance) {return 0;} else {return -1;}}/* * This function adjusts the array into a heap. * Parameters: *@root: The root node of heap. * Returns: *None. */void min_heapify(int root) {int min_index = root;if(root * 2 <= heap_cnt) {if(cmp(min_index, 2 * root) > 0) {min_index = 2 * root;}}if(root * 2 + 1 <= heap_cnt) {if(cmp(min_index, 2 * root + 1) > 0) {min_index = 2 * root + 1;}}if(min_index != root) {vertex *swap = heap[root];heap[root] = heap[min_index];heap[min_index] = swap;heap[root]->index = root;heap[min_index]->index = min_index;min_heapify(min_index);}}/* * This node adjust the heap after update the distance of node. * Parameters: *@index: The element to update. * Returns: *None. */void update(int index) {while(index > 1) {if(cmp(index, index / 2) < 0) {vertex *swap = heap[index];heap[index] = heap[index / 2];heap[index / 2] = swap;heap[index]->index = index;heap[index / 2]->index = index / 2;index /= 2; } else {break;}}}/* * This function removes the top element from heap. * Parameters: *None. * Returns: *The element removed. */vertex* remove(void) {vertex *r = heap[1];heap[1] = heap[heap_cnt];heap[1]->index = 1;heap_cnt--;min_heapify(1);return r;} /* * This function transports the map into status number. * Parameters£º *None. * Returns: *The status number. */int get_status_from_map(void) {int status = 0;int tag[10] = {0};for(int i = 0; i < 9; i++) {int cnt = 0;for(int j = 1; j < map[i]; j++) {if(tag[j] == 0) {cnt++;}}status += cnt * multiply[8 - i];tag[map[i]] = 1;}return status;} /* * This function transports the status into map. * Parameters: *@status: The current status. * Returns: *None. */void get_map_from_status(int status) {int tag[10] = {0};for(int i = 0; i < 9; i++) {int s = status / multiply[8 - i];int cnt = 0;for(int j = 1; j < 10; j++) {if(tag[j] == 0) {cnt++;if(cnt >