---
# System prepended metadata

title: 15-Puzzle Solver
tags: [IDA*, A*, 15-Puzzle, Searching Algorithm]

---

# 15-Puzzle Solver
由於在過去課程曾經學到如何使用A\*演算法來尋找8-Puzzle的最佳解，因此在機緣巧合下決定將原有的程式升級，改成能夠解15-Puzzle的程式。  
以下將使用C++作爲範例，完整程式碼可以在 https://github.com/lemtea8/15puzzle 看到。  

## 15-Puzzle
![15 Puzzle](https://upload.wikimedia.org/wikipedia/commons/thumb/9/91/15-puzzle.svg/330px-15-puzzle.svg.png)  
*15-Puzzle的範例 (圖片來源: 維基百科)*  

## Puzzle Implementation
首先需要一個負責對Puzzle進行移動等等功能的class，15-Puzzle的輸入是一個16個字元的16進位字串，例如: 2348170c56ab9def(空格視爲0)。  
  
Puzzle狀態的儲存可以使用一個字元陣列 `char arr[16];`  
但是由於15-Puzzle中每個方格的數值皆小於16，因此可以只使用4個bits來表達每個方格，相比之下char使用了1個byte，有一半的空間都浪費了。  
16個方格總共只需要16x4=64個bits，因此剛好可以用一個uint64來儲存。  
這個技巧叫做bit board，使用bit board有諸多好處，主要是能夠節省記憶體來加速程式。但缺點是bitwise的操作比較不好理解。  
以下使用bit board的技巧來建構Puzzle的功能  
  

先定義Puzzle這個class:  
```cpp
class Puzzle {
  public:
    Puzzle();
    Puzzle(const std::string &input);
    Puzzle(uint64_t bits);

  private:
    uint64_t bits;
};
```

建構子(爲了實作方便，方格儲存的順序是從低位元到高位元):  
```cpp
// "0" stands for the blank
Puzzle::Puzzle(const std::string &input) {
    this->bits = 0;
    for (int i = 0; i < input.size(); i++) {
        char c = input[i];
        uint64_t num = hex_to_int(c);
        // each cell takes four bits (0~F)
        this->bits |= num << (i * 4);
    }
}
```

讀取某個特定位置的數值(使用mask將uint64中特定位置的4個bit讀取出來):  
```cpp
uint8_t Puzzle::at(int n) {
    uint64_t mask = uint64_t(0b1111) << (n * 4);
    return (this->bits & mask) >> (n * 4);
}
```

設定某個特定位置的數值(先將該位置重設爲0000，再使用"or"將新的數值存入):  
```cpp
void Puzzle::set(int n, uint8_t val) {
    uint64_t mask = ~(uint64_t(0b1111) << (n * 4));
    this->bits &= mask; // set the target area to zero
    uint64_t valmask = uint64_t(val) << (n * 4);
    this->bits |= valmask;
}
```

檢查目前是否是目標狀態(goal):  
```cpp
bool Puzzle::is_goal() {
    return this->bits == uint64_t(0x0FEDCBA987654321);
}
```

接下來要實作移動方格的功能，爲了要移動方格首先要先確定0在puzzle中的哪個位置，因此先實作尋找0位置的funcition:
```cpp
int Puzzle::find_zero_position() {
    for (int i = 0; i < 16; i++) {
        if (this->at(i) == 0) {
            return i;
        }
    }
    return -1;
}
```

互換兩個位置的值(15-Puzzle中，移動一次就相當於將該方格與0互換位置):  
```cpp
void Puzzle::swap(int i, int j) {
    uint64_t tmp = this->at(i);
    this->set(i, this->at(j));
    this->set(j, tmp);
}
```

實作移動的功能，向上移動表示0下面的方格往上移動(如果已經在最邊邊因此不能再移動了，則回傳false表示無法移動):  

```cpp
// returns false if the move is invalid
bool Puzzle::move(Direction dir) {
    int p0 = this->find_zero_position();
    int new_p0;

    switch (dir) {
    case UP:
        new_p0 = p0 + 4;
        if (new_p0 >= 16) {
            return false;
        }
        break;
    case DOWN:
        new_p0 = p0 - 4;
        if (new_p0 < 0) {
            return false;
        }
        break;
    case LEFT:
        new_p0 = p0 + 1;
        // not movable if zero is at the right most column
        if ((new_p0 >= 16) || (new_p0 % 4 == 0)) {
            return false;
        }
        break;
    case RIGHT:
        new_p0 = p0 - 1;
        // not movable if zero is at the left most column
        if ((new_p0 < 0) || ((new_p0 + 1) % 4 == 0)) {
            return false;
        }
        break;
    default:
        return false;
    }

    this->swap(p0, new_p0);
    return true;
}
```

## Algorithms

### Solvability
首先，需要先確認輸入的15-Puzzle是有解的(https://en.wikipedia.org/wiki/15_puzzle#Solvability)，實作solvable function:  
```cpp
bool Puzzle::solvable() {
    int inversion_count = 0;
    for (uint32_t i = 0; i < 15; i++) {
        if (at(i) == 0) {
            continue;
        }
        for (uint32_t j = i + 1; j < 16; j++) {
            if (at(j) != 0 && at(i) > at(j)) {
                inversion_count++;
            }
        }
    }
    int zero_row = this->zero_position / 4;
    return ((zero_row + inversion_count) % 2 != 0);
}
```

### BFS(Breadth-first search)
爲了找出最佳解，第一個可能的方法就是使用BFS來搜尋，但是由於每步會有2~4種選擇性(角落的有2種，邊邊的有3種，中間的有4種)，單純使用BFS會導致狀態數量快速增長直到記憶體無法負荷爲止(平均在第17步時就需要儲存超過1億個狀態，而最難的15-Puzzle需要80步才能解開)。  

### A*
爲了解決BFS的不足，A\*引入了一個heuristic function(啟發函數)，這個函數可以用來評估當前狀態距離終點還需要多少成本，如果成本是0則表示已經抵達終點。  
首先我們需要一個priority queue來儲存目前訪問過的狀態，並根據他們的優先順序來尋找下一個應該訪問的狀態。  
這個優先順序表示爲 $f(n)$ ，其值越小越好，而 $f(n)=g(n)+h(n)$ ，其中$g(n)$表示從起始點到目前狀態所花費的成本，$h(n)$則表示heuristic function計算出的值。  

### Heuristic
我們使用Manhattan distance作爲我們的heuristic function，他的計算方式是$h(n)=|x_n - x_g|+|y_n-y_g|$，其中$(x_n, y_n)$表示此方格現在的位置，$(x_g, y_g)$表示方格的終點位置。  
將所有方格的值相加，就可以得到heuristic function的值。  
*並非所有heuristic都可以找到最佳解，要找到最佳解需要滿足一定的條件( https://en.wikipedia.org/wiki/Admissible_heuristic )  

在Puzzle中實作計算heuristic的function:  
```cpp
int manhattan_distance(int i, int j) {
    int ix = i % 4, iy = i / 4;
    int jx = j % 4, jy = j / 4;
    return abs(ix - jx) + abs(iy - jy);
}

int Puzzle::heuristic() {
    int val = 0;
    for (int i = 0; i < 16; i++) {
        uint8_t c = this->at(i);
        // does not include 0
        if (c != 0) {
            val += manhattan_distance(c - 1, i);
        }
    }
    return val;
}
```

## Solver
Solver中包含兩個關鍵元件: 一個是用來表示當前狀態的class，另一個是priority queue。  

### State
State中需要保存:  
1. 當前的Puzzle，以用來往下一個狀態走。
2. 目前走了多遠，也就是$g(n)$。這對找出最佳解至關重要。
3. 當前Puzzle的heuristic。
4. 上一個State的指標，能夠在抵達終點後回溯到起始狀態，找出最佳解的路徑。  
5. 上一個State到目前這個State是採取了什麼動作，用途同4.
```cpp
class State {
  public:
    State(State *parent, uint64_t bits, int walked, int heuristic,
          Direction dir) {
        this->parent = parent;
        this->bits = bits;
        this->walked = walked;
        this->heuristic = heuristic;
        this->dir = dir;
    }
    uint64_t get_bits() { return this->bits; }
    int heuristic;
    int walked;
    State *parent;
    Direction dir;

  private:
    uint64_t bits;
};

bool state_compare(State *i, State *j) {
    return i->walked + i->heuristic < j->walked + j->heuristic;
}
```

### Priority Queue
使用binary heap實作一個簡單的priority queue(想要更好的效能可以使用fibonacci heap):  
```cpp
template <typename T> class PriorityQueue {
  public:
    PriorityQueue(bool (*compare)(T i, T j)) {
        container = std::vector<T>(1);
        this->compare = compare;
    }

    PriorityQueue(bool (*compare)(T i, T j), int capacity) {
        container = std::vector<T>(1);
        container.reserve(capacity);
        this->compare = compare;
    }

    bool empty() { return container.size() == 1; }

    void push(T t) {
        container.push_back(t);
        heapify_bottomup(container.size() - 1);
    }

    void clear() {
        container.clear();
    }

    T top() { return container[1]; }

    void pop() {
        if (empty()) {
            return;
        }
        container[1] = container[container.size() - 1];
        container.pop_back();
        heapify_topdown(1);
    }

    int size() { return container.size() - 1; }

  private:
    std::vector<T> container;
    bool (*compare)(T i, T j);

    void heapify_bottomup(int curr) {
        T bottom = container[curr];
        while (curr != 1 && compare(bottom, container[curr / 2])) {
            container[curr] = container[curr / 2];
            curr /= 2;
        }
        container[curr] = bottom;
    }

    void heapify_topdown(int curr) {
        int size = container.size();
        T root = container[curr];
        int child;

        while (curr * 2 < size) {
            // left
            child = curr * 2;
            // right
            if (child < size && 
                compare(container[child + 1], container[child])) {
                child += 1;
            }
            if (compare(container[child], root)) {
                container[curr] = container[child];
            } else {
                break;
            }
            curr = child;
        }
        container[curr] = root;
    }
};
```

### A* Implementation
藉由上述兩個元件，使用A\*實作Solver class:  
```cpp
Solver::Solver() {}

std::vector<Direction> Solver::solve(Puzzle puzzle) {
    PriorityQueue<State *> pq(state_compare);
    std::vector<State *> used;

    int walked = 0;

    State *root =
        new State(nullptr, puzzle.get_bits(), walked, puzzle.heuristic(), UP);
    pq.push(root);
    State *last = nullptr;

    while (!pq.empty()) {
        State *curr = pq.top();
        last = curr;
        walked = curr->walked + 1;
        pq.pop();
        used.push_back(curr);

        Puzzle p(curr->get_bits());
        if (p.is_goal()) {
            break;
        }

        for (auto dir : {UP, DOWN, LEFT, RIGHT}) {
            Puzzle tmp = p;
            if (tmp.move(dir)) {
                State *next = new State(curr,
                    tmp.get_bits(), walked, tmp.heuristic(), dir);
                pq.push(next);
            }
        }
    }

    std::vector<Direction> answer;
    while (true) {
        if (last->parent == nullptr) {
            break;
        }
        answer.push_back(last->dir);
        last = last->parent;
    }
    std::reverse(answer.begin(), answer.end());

    while (!pq.empty()) {
        State *s = pq.top();
        pq.pop();
        delete s;
    }
    for (auto s : used) {
        delete s;
    }

    return answer;
}
```

至此基本的15-Puzzle solver已經完成了，能夠用來解步數較少的puzzle。  

## Optimizations
整個程式還有一些可以最佳化的地方，用來加速程式。  

### Caching 0 Position
在Puzzle class中可以看到，每次要移動的時候都需要重新找一次0的位置，這無疑是做白用工  
將0的位置保存起來重複使用，可以節省不少時間。  
但需要記得在建構子中初始化0的位置，以及每次進行move之後要更新0的位置。  
```cpp
class Puzzle {
  public:
    Puzzle();
    Puzzle(const std::string &input);
    Puzzle(uint64_t bits);

  private:
    uint64_t bits;
    int zero_position;
};

// "0" stands for the blank
Puzzle::Puzzle(const std::string &input) {
    this->bits = 0;
    ...
    this->zero_position = this->find_zero_position();
}

// returns false if the move is invalid
bool Puzzle::move(Direction dir) {
    int p0 = this->zero_position;
    int new_p0;

    ...

    this->swap(p0, new_p0);
    this->zero_position = new_p0;
    return true;
}
```

### Partially Update Heuristic
另外一個可以減少計算量的是heuristic，因爲每次移動一格只會影響到該格的Manhattan distance，完全可以不用重新計算其他格子的值:

```cpp
class Puzzle {
  public:
    Puzzle();
    Puzzle(const std::string &input);
    Puzzle(uint64_t bits);

  private:
    uint64_t bits;
    int zero_position;
    int heuristic_v;
};

// "0" stands for the blank
Puzzle::Puzzle(const std::string &input) {
    this->bits = 0;
    ...
    this->zero_position = this->find_zero_position();
    this->heuristic_v = this->calculate_heuristic();
}

int Puzzle::heuristic() { return this->heuristic_v; }

// returns false if the move is invalid
bool Puzzle::move(Direction dir) {
    int p0 = this->zero_position;
    int new_p0;

    ...

    int tile = at(new_p0) - 1;
    this->heuristic_v -= manhattan_distance(tile, new_p0);
    this->swap(p0, new_p0);
    this->zero_position = new_p0;
    this->heuristic_v += manhattan_distance(tile, p0);
    return true;
}
```

### Avoid Duplicated States
如果檢查在A*過程中走到的State，會發現他其實常常重複訪問同一個Puzzle，這其實是非常沒有效率的。如果使用一個hashmap來記錄已經訪問過的Puzzle，可以大量減少不必要的計算。  
這個hashmap的key是Puzzle，剛好Puzzle是用一個uint64來存，可以不需要重新寫一個hash function。而value則是儲存這個Puzzle是走多遠(walked)，因爲可能會發現一個比較短的路徑到達當前這個Puzzle，爲了避免找到次佳解，當遇到重複的Puzzle時且路徑較短時，需要更新他的值。

```cpp
std::vector<Direction> Solver::solve(Puzzle puzzle) {
    PriorityQueue<State *> pq(state_compare);
    std::vector<State *> used;
    // record visited states with its priority
    std::unordered_map<uint64_t, int> visited;
    int walked = 0;

    State *root =
        new State(nullptr, puzzle.get_bits(), walked, puzzle.heuristic(), UP);
    visited[puzzle.get_bits()] = 0;
    pq.push(root);
    State *last = nullptr;

    while (!pq.empty()) {
        State *curr = pq.top();
        last = curr;
        walked = curr->walked + 1;
        pq.pop();
        used.push_back(curr);

        Puzzle p(curr->get_bits());
        if (p.is_goal()) {
            break;
        }

        for (auto dir : {UP, DOWN, LEFT, RIGHT}) {
            Puzzle tmp = p;
            tmp.move(dir);
            State *next = new State(curr,
                tmp.get_bits(), walked, tmp.heuristic(), dir);

            // if not visited before
            if (visited.count(tmp.get_bits()) == 0) {
                pq.push(next);
                visited[tmp.get_bits()] = walked;
            } else {
                // update if the path is shorter
                if (curr->walked < visited[tmp.get_bits()]) {
                    visited[tmp.get_bits()] = walked;
                    pq.push(next);
                }
            }
        }
    }

    std::vector<Direction> answer;
    while (true) {
        if (last->parent == nullptr) {
            break;
        }
        answer.push_back(last->dir);
        last = last->parent;
    }
    std::reverse(answer.begin(), answer.end());

    while (!pq.empty()) {
        State *s = pq.top();
        pq.pop();
        delete s;
    }
    for (auto s : used) {
        delete s;
    }

    return answer;
}
```

## Limitations
A\*能比BFS更有效率的找出解答，但是在問題比較困難的時候，A\*依然會因爲需要記憶太多狀態導致記憶體不足。例如：`0291ca8d6574feb3`的解需要60步，這導致A*在解的過程中需要使用超過8GiB的記憶體，因而成爲一個主要的限制。爲了解決這個問題，我們改使用IDA\*演算法。  

## IDA\* (Iterative Deepening A\*)
IDA\*的本質其實比較偏向DFS(Depth First Search)，他主要的概念是給與一個預算，並使用DFS往下搜尋，直到超出預算則返回繼續從上一個狀態開始。當遍歷完成後，根據這次遍歷的結果給出一個新的預算，重新使用這個預算用DFS搜尋。IDA\*會重複這個過程直到找到解爲止。  

IDA\*的優點: 不需要記憶大量狀態，因此不會有記憶體爆炸的問題。  
缺點: 會大量重複訪問曾經訪問過的狀態。  

雖然IDA\*相較於A\*而言會大量訪問重複的狀態，但是在實務上由於A\*需要維護一個龐大的priority queue(動輒幾百、千萬)，消耗了大量的運算成本。最終的結果是IDA\*有更好的效率以及更短的執行時間，同時記憶體使用量甚少。  

IDA\*的程式碼如下:  
```cpp
const int FOUND = -1;

int Solver::dfs(Puzzle p, int walked, int limit) {
    this->count++;
    int h = p.heuristic();
    int f = walked + h;

    if (f > limit) {
        return f;
    }
    if (p.is_goal()) {
        return FOUND;
    }

    int min = INT_MAX;
    for (auto dir : {UP, DOWN, LEFT, RIGHT}) {
        Puzzle tmp = p;
        if (!tmp.move(dir)) {
            continue;
        }
        auto iter = std::find(this->visited.begin(), this->visited.end(),
            tmp.get_bits());
        if (iter != this->visited.end()) {
            continue;
        }
        this->visited.push_back(tmp.get_bits());
        int res = dfs(tmp, walked + 1, limit);
        if (res == FOUND) {
            this->path.push_back(dir);
            return FOUND;
        }
        this->visited.pop_back();
        if (res < min) {
            min = res;
        }
    }
    return min;
}

std::vector<Direction> Solver::solve(Puzzle puzzle) {
    this->path.clear();
    this->visited.clear();
    this->count = 0;

    int budget = puzzle.heuristic();

    // IDA*
    while (true) {
        int new_budget = dfs(puzzle, 0, budget);
        if (new_budget == FOUND) {
            break;
        }
        budget = new_budget;
    }
    std::reverse(this->path.begin(), this->path.end());

    return this->path;
}
```
其中，Solver.visited是一個vector，用來記錄目前DFS路上訪問過的狀態，避免重複訪問。  
不使用unordered_set的原因是由於這些狀態的數量通常很小，使用set反而會減速。  
Solver.path則是在找到終點狀態時，在遞迴返回時將找到的路徑一一填入解答中(但最後要reverse才是答案)。  

至此，整個15-puzzle solver就算是完成了。雖然他在解較難的問題時(>60幾步)需要花費大量時間(成指數增長)，但不會吃記憶體導致甚至沒辦法解開。  
還有其他的改進方法就是找一個更強大的heuristic function(在搜尋演算法中，好的heuristic是相當重要的)，讓程式在搜尋中少走許多歪路，才能快速找到解答。  

## Test Data
- 2348170c56ab9def (15 moves)
- 51309af4d7b826ec (25 moves)
- 1f3452d8a70b96ec (30 moves)
- 12345dc8796eba0f (33 moves)
- 1f345dc8796eab20 (44 moves)
- 0bd45c329671af8e (50 moves)
- 58d9ebc1324a70f6 (56 moves)
- 38abfe7951024c6d (58 moves)
- cbfed67a13248095 (64 moves)