1. 程式人生 > 實用技巧 >P1379 八數碼難題 ( A* 演算法 與 IDA_star 演算法)

P1379 八數碼難題 ( A* 演算法 與 IDA_star 演算法)

P1379 八數碼難題

題目描述

在3×3的棋盤上,擺有八個棋子,每個棋子上標有1至8的某一數字。棋盤中留有一個空格,空格用0來表示。空格周圍的棋子可以移到空格中。要求解的問題是:給出一種初始佈局(初始狀態)和目標佈局(為了使題目簡單,設目標狀態為123804765),找到一種最少步驟的移動方法,實現從初始佈局到目標佈局的轉變。

輸入格式

輸入初始狀態,一行九個數字,空格用0表示

輸出格式

只有一行,該行只有一個數字,表示從初始狀態到目標狀態需要的最少移動次數(測試資料中無特殊無法到達目標狀態資料)

輸入輸出樣例

輸入 #1

283104765

輸出 #1

4

分析:

利用A-star(A*演算法),設目標狀態為

123
804
765

\(h\) 函式可以定義為,不在應該在的位置的數字個數。

容易發現 \(h\) 滿足以上兩個性質,此題可以使用 A*演算法求解。

程式碼實現:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <queue>
#include <set>
using namespace std;
const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, 1, -1};
int fx, fy;
char ch;
struct matrix {
  int a[5][5];
  bool operator<(matrix x) const {
    for (int i = 1; i <= 3; i++)
      for (int j = 1; j <= 3; j++)
        if (a[i][j] != x.a[i][j]) return a[i][j] < x.a[i][j];
    return false;
  }
} f, st;
int h(matrix a) {
  int ret = 0;
  for (int i = 1; i <= 3; i++)
    for (int j = 1; j <= 3; j++)
      if (a.a[i][j] != st.a[i][j]) ret++;
  return ret;
}
struct node {
  matrix a;
  int t;
  bool operator<(node x) const { return t + h(a) > x.t + h(x.a); }
} x;
priority_queue<node> q;
set<matrix> s;
int main() {
  st.a[1][1] = 1;
  st.a[1][2] = 2;
  st.a[1][3] = 3;
  st.a[2][1] = 8;
  st.a[2][2] = 0;
  st.a[2][3] = 4;
  st.a[3][1] = 7;
  st.a[3][2] = 6;
  st.a[3][3] = 5;
  for (int i = 1; i <= 3; i++)
    for (int j = 1; j <= 3; j++) {
      scanf(" %c", &ch);
      f.a[i][j] = ch - '0';
    }
  q.push({f, 0});
  while (!q.empty()) {
    x = q.top();
    q.pop();
    if (!h(x.a)) {
      printf("%d\n", x.t);
      return 0;
    }
    for (int i = 1; i <= 3; i++)
      for (int j = 1; j <= 3; j++)
        if (!x.a.a[i][j]) fx = i, fy = j;
    for (int i = 0; i < 4; i++) {
      int xx = fx + dx[i], yy = fy + dy[i];
      if (1 <= xx && xx <= 3 && 1 <= yy && yy <= 3) {
        swap(x.a.a[fx][fy], x.a.a[xx][yy]);
        if (!s.count(x.a)) s.insert(x.a), q.push({x.a, x.t + 1});
        swap(x.a.a[fx][fy], x.a.a[xx][yy]);
      }
    }
  }
  return 0;
}

繼續想想還有沒有其他方法,

假設每一步都是有意義的,那麼一個數字成功對上也要移動他們的曼哈頓距離次

所以,我們將估價函式設計為所有數字與目標狀態中數字的曼哈頓距離之和(當然0不算,否則你就可以高興的WA了)

還有一個顯然的優化,就是記錄上一次的操作

而且我們不一定要用二維陣列,可以用一個string來儲存狀態。對於一個string中的下標i,縱座標為i/3, 橫座標為i%3。對於二維陣列下標x, y, string中的下標便為x * 3 + y

還有不明白的,請看程式碼

IDA_star

除了估價函式,還有一個顯然的優化是記錄上一次的操作

#include<bits/stdc++.h>
using namespace std;
string st, ed;//起始狀態和目標 
int depth;//搜尋深度 
//估價函式,為每個數字的曼哈頓距離之和 
int hfunc(string st) {
	int ans = 0;
	for (register int i = 0; i < st.size(); ++i) {
		if (st[i] == '0') continue;//0不算,否則會WA 
		int j = ed.find(st[i]), r = i / 3, c = i % 3;
		int x = j / 3, y = j % 3;
		//橫座標為/3, 縱座標為%3 
		ans += abs(r - x) + abs(c - y);
	}
	return ans;
}
const int dx[4] = {1, -1, 0, 0}, dy[4] = {0, 0, -1, 1};
//IDA_star
//除了估價函式,還有一個顯然的優化是記錄上一次的操作 
bool dfs(int now, int pre) {
	int cnt = hfunc(st);
	if (!cnt) return 1;
	if (now + cnt > depth) return 0;
	//當前步數+估價>深度限制,立即回溯 
	int pos = st.find('0'), x = pos / 3, y = pos % 3;
	for (register int i = 0; i < 4; ++i) {
		int nx = x + dx[i], ny = y + dy[i];
		if (nx < 0 || nx > 2 || ny < 0 || ny > 2 || nx * 3 + ny == pre) continue;
		//陣列中的下標為橫座標*3+縱座標 
		swap(st[pos], st[nx * 3 + ny]);
		if (dfs(now + 1, pos)) return 1;
		swap(st[pos], st[nx * 3 + ny]);
	}
	return 0;
}
int main() {
	cin >> st;
	ed = "123804765";
	depth = hfunc(st);
	while (depth <= 27 && !dfs(0, -1)) ++depth;
	cout << depth << endl;
}

從AC速度上看 IDA_star 利用了儲存的結果明顯加快了速度

同樣這道題也可以使用BFS做,畢竟當估值函式 h = 1時就是BFS嘛 ^ v ^