1. 程式人生 > 其它 >LuoguP4289 [HAOI2008]移動玩具 題解

LuoguP4289 [HAOI2008]移動玩具 題解

LuoguP4289 [HAOI2008]移動玩具 題解

主要參照這篇題解(第一頁第一篇),作者為本人教練。

講解在註釋裡都有,自己看看吧。

#include <cstdio>
#include <cmath>
#include <algorithm>
#include <queue>
using namespace std;

int pre[6][6], tar[6][6], vis[65539], f[65539], dec1, dec2;
queue<int> q;
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
	/*
		變數:
		pre陣列表示初始狀態,dec1表示初始狀態轉化的十進位制數。
		tar陣列表示目標狀態,dec2表示目標狀態轉換的十進位制數。
		vis陣列表示是否被訪問過。 
		q佇列用來儲存玩具擺放的狀態。 
		dx和dy陣列是為了方便表示擴充套件而使用的。 
		有關玩具放置的狀態的轉化請移步至函式1。 
	*/
int BintoDec(int a[6][6]) { 
	/*	
		函式1:int BintoDec(int a[6][6])
		此函式的目的:
			根據a陣列,將玩具擺放的狀態轉化為十進位制數。 
		變數解釋: 
			a陣列表示要轉換的玩具擺放的狀態。 
		具體思想: 
			由於每種玩具擺放的狀態都是唯一的, 
			所以我們嘗試著將其轉化為一個數字。
			大家可以發現,每種玩具擺放的狀態其實是二進位制的表示。
			所以我們可以將其轉化為十進位制的數。
			例如:樣例中,初始狀態可以轉化為:1111000011100010。 
			所以可以將其轉化為十進位制的數。 
			可能你會考慮爆long long或爆空間的問題。
			放心,玩具擺放的狀態用十進位制表示必不會超過2^16-1。
			所以,我們可以將樣例中的起始狀態和目標狀態
			分別表示為61666和42405。
			具體怎麼轉化呢?
			我們先畫一個玩具擺放的狀態圖:	 
			  1     2     3     4
			1 15	14    13    12
			2 11    10    9     8
			3 7     6     5     4
			4 3     2     1     0
			可以發現,每個格子對應的乘方有如下的規律:
			設格子(i,j)對應的乘方為G(i,j),
			則有:G(i,j) = 16 - (i - 1) * 4 - j。
			這個稍微想一下就可以證明出來,這裡不再贅述。 
	*/
	int res = 0;
	for(int i = 4; i >= 1; --i)
		for(int j = 4; j >= 1; --j)
			res += a[i][j] * pow(2, 16 - (i - 1) * 4 - j);
	return res;
}
	
void DectoBin(int x, int a[6][6]) {
	/*
		函式2:void DectoBin(int x, int a[6][6])
		此函式的目的:
			根據x,將十進位制狀態轉換回一開始二進位制的狀態。
		變數解釋: 
			x表示要轉換回的十進位制數,
			a陣列表示轉換後的二進位制狀態。 
		具體思想:
			呃。。。這就不用解釋了吧。
			這個大家應該很明白吧。
			不明白就去看有關二進位制的內容吧。 
	*/
	while(x) {
		for(int i = 4; i >= 1; --i)
			for(int j = 4; j >= 1; --j) {
				a[i][j] = x % 2;
				x /= 2;
			}
	} 
	//轉換完畢 
}
int judge(int x0, int y0, int xx, int yy) {
	/*
		函式3:int judge(int x0, int y0, int xx, int yy)
		此函式的目的:
			判斷是否越過邊界且點所表示的數是否相同,是返回1,否表示0
		變數解釋:
			xx,yy分別是點(x0,y0)經過擴充套件後得到的點的橫座標和縱座標。
		具體思想:\ 
	*/
	return (xx >= 1) && (xx <= 4) && (yy >= 1) && (yy <= 4) && (pre[x0][y0] != pre[xx][yy]);
}
void bfs() {
	/*
		函式4:void bfs()
		此函式的目的:
			通過bfs求得最少步數。 
		變數解釋:\
		具體思想:
			基本的bfs(STL<queue>)操作:
			1. 將初始狀態入隊,並標記在佇列中(vis[x]=1)
			2. 在佇列不為空時迴圈執行以下操作:
				(1)取出隊首,將其轉化為原來的二進位制狀態,然後出隊。
				(2)向上下左右擴充套件。 
				(3)判斷是否越過邊界且兩數相等。 
				if No then執行以下操作(最後需要還原a陣列狀態!):
					1_ 提取擴充套件節點的狀態,並將其轉化為十進位制數。 
					2_ 將擴充套件節點與原數交換
					3_ 記錄交換後的陣列對應的十進位制數
					4_ 判斷是否訪問過
					5_ 若沒訪問過,則標記已訪問過,並記錄交換後的陣列對應的十進位制數的父節點,即交換前的陣列對應的十進位制數。 
					6_ 否則直接跳過。 
					7_ 如果已經達到了目標狀態,直接返回。
				else then直接跳過 
	*/
	q.push(dec1);
	vis[dec1] = 1;/*注意!一定要加!!!*/ 
	while(q.size()/*或者可以寫!q.empty()*/) {
		int now = q.front();
		DectoBin(now, pre);
		q.pop();
		for(int i = 1; i <= 4; ++i)
			for(int j = 1; j <= 4; ++j) {
				int x0 = i, y0 = j;
				for(int k = 0; k < 4; ++k) {
					int xx = x0 + dx[k], yy = y0 + dy[k], flag = 0;
					if(judge(x0, y0, xx, yy)) {
						flag = 1;
						int tmpdec1 = BintoDec(pre);
						swap(pre[x0][y0], pre[xx][yy]);
						int tmpdec2 = BintoDec(pre);
						if(!vis[tmpdec2]) {
							vis[tmpdec2] = 1;
							f[tmpdec2] = tmpdec1;
							q.push(tmpdec2); 
						}
						if(tmpdec2 == dec2)	return;
					}
					if(flag)
						swap(pre[x0][y0], pre[xx][yy]);
				}
			} 
	}
}
int main() {
	/*
		Step 1:輸入&轉化為二進位制 
		由於輸入形式中,數字都是連在一起的。 
		所以,大多數人都會想到用字串或字元陣列輸入每一行, 
		然後將其轉化為數字。但其實可以不用這麼麻煩。
		大家都知道,佔x位輸入是這麼寫的:
		scanf("%xd", &a);(假定x是具體數字)
		所以,對於一行連在一起的數字,我們可以佔一位輸入。
		就是上面所寫的那樣。這樣就可以使程式碼簡潔明瞭。
		然後再呼叫上面的BintoDec函式得到初始和目標狀態的十進位制數。 
	*/ 
	for(int i = 1; i <= 4; ++i)
		for(int j = 1; j <= 4; ++j)
			scanf("%1d", &pre[i][j]);
	for(int i = 1; i <= 4; ++i)
		for(int j = 1; j <= 4; ++j)
			scanf("%1d", &tar[i][j]);
	dec1 = BintoDec(pre), dec2 = BintoDec(tar);
	/*
		Step 2:bfs一波走著!
		根據上面的函式,保證會有解。 
	*/ 
	bfs();
	f[dec1] = 0;
	int ans = 0;
	/*
		Step 3:遞進求答案、輸出&程式結束 
	*/
	while(f[dec2])	ans++, dec2 = f[dec2];
	printf("%d", ans);
	return 0; 
}