最小步數模型
一、簡介
最小步數模型和最短路模型的區別?
最短路模型:某一個點到另一個點的最短距離(坐標與坐標之間)
最小步數模型:不再是點(坐標),而是狀態到另一個狀態的轉變
BFS難點所在(最短路問題):
存盤的資料結構:佇列
狀態如何存盤到佇列里邊(以什么形式)?
狀態怎么表示,怎么轉移?
3.
dist如何記錄每一個狀態的距離
技巧:在最小步數模型中狀態和狀態的距離通常用哈希表來進行存盤(存在key-value的映射關系!),如map,unordered_map,
思路:將初始狀態加入佇列,然后去搜索擴展,直到搜索到目標狀態為止,
注:
? 在搜索程序中可能由狀態的切換,如一維坐標切換到二維坐標,字串切換到二標坐標形式的狀態等等!
二、練習
1. 八數碼
【題目鏈接】845. 八數碼 - AcWing題庫
在一個 3×3 的網格中,1~8 這 8 個數字和一個
x恰好不重不漏地分布在這 3×3 的網格中,例如:
1 2 3 x 4 6 7 5 8在游戲程序中,可以把
x與其上、下、左、右四個方向之一的數字交換(如果存在),我們的目的是通過交換,使得網格變為如下排列(稱為正確排列):
1 2 3 4 5 6 7 8 x例如,示例中圖形就可以通過讓
x先后與右、下、右三個方向的數字交換成功得到正確排列,交換程序如下:
1 2 3 1 2 3 1 2 3 1 2 3 x 4 6 4 x 6 4 5 6 4 5 6 7 5 8 7 5 8 7 x 8 7 8 x現在,給你一個初始網格,請你求出得到正確排列至少需要進行多少次交換,
輸入格式
輸入占一行,將 3×33×3 的初始網格描繪出來,
例如,如果初始網格如下所示:
1 2 3 x 4 6 7 5 8則輸入為:
1 2 3 x 4 6 7 5 8輸出格式
輸出占一行,包含一個整數,表示最少交換次數,
如果不存在解決方案,則輸出 ?1?1,
輸入樣例:
2 3 4 1 5 x 7 6 8輸出樣例
19
1、題目的目標

求最小步數 -> 用BFS
2、移動情況

移動方式:
轉以后:a = x + dx[i], b = y + dy[i].
思想:把每一個狀態看作圖論的一個節點(節點a到節點b的距離為1——狀態能轉移)——> 起點到終點最少需要多少步
從初始狀況移動到目標情況 —> 求最短路
3、問題
第一點:狀態如何存盤到佇列里?
第二點:如何記錄每一個狀態的“距離”(即需要移動的次數)?
第三點:佇列怎么定義(佇列存盤的是狀態,狀態可以用字串表示),dist陣列怎么定義(每一個狀態到起始狀態的距離——兩個關鍵字:狀態(字串)、距離(int))——key-value?——哈希表
4、解決方案
將 “3*3矩陣” 轉化為 “字串”
如:

所以:
佇列可以用 queue<string>
//直接存轉化后的字串
dist陣列用 unordered_map<string, int>
//將字串和數字聯系在一起,字串表示狀態,數字表示距離
5、矩陣與字串的轉換方式

【參考代碼】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include<unordered_map>
using namespace std;
//狀態轉移
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
int bfs(string strat)
{
//定義目標狀態、
string end = "12345678x";
//定義佇列和dist陣列
queue<string> q;
unordered_map<string, int> d;
//初始化佇列和dist陣列
q.push(strat);
d[strat] = 0;
while(q.size())
{
// 獲取隊頭元素,彈出佇列
auto t = q.front();
q.pop();
//記錄當前狀態的距離,如果為最終狀態則回傳距離結果
int distance = d[t];
if(t == end) return d[t];
//查詢x在一位陣列中的下標,進行狀態轉換
int k = t.find('x');
int x = k / 3, y = k % 3;
//擴展隊頭元素(鄰接節點入隊)
for(int i = 0; i < 4; i ++)
{
//轉移后的x的坐標(鄰接節點)
int a = x + dx[i], b = y + dy[i];
//沒有越界
if(a >= 0 && b >= 0 && a < 3 && b < 3)
{
//轉移x
swap(t[k], t[a * 3 + b]);
//如果當前狀態是第一次遍歷,記錄距離,入隊
if(!d.count(t))
{
d[t] = distance + 1;
q.push(t);
}
//還原狀態,為下一種轉換情況做準備!
swap(t[k], t[a * 3 + b]);
}
}
}
//無法轉換到目標狀態,回傳-1
return -1;
}
int main()
{
string strat;
// 輸入起始狀態
for (int i = 0; i < 9; i ++ )
{
char c;
cin >> c;
strat += c;
}
cout << bfs(strat);
}
2.魔板
【題目鏈接】1107. 魔板 - AcWing題庫
Rubik 先生在發明了風靡全球的魔方之后,又發明了它的二維版本——魔板,
這是一張有 8 個大小相同的格子的魔板:
1 2 3 4 8 7 6 5我們知道魔板的每一個方格都有一種顏色,
這 8 種顏色用前 8 個正整數來表示,
可以用顏色的序列來表示一種魔板狀態,規定從魔板的左上角開始,沿順時針方向依次取出整數,構成一個顏色序列,
對于上圖的魔板狀態,我們用序列 (1,2,3,4,5,6,7,8) 來表示,這是基本狀態,
這里提供三種基本操作,分別用大寫字母 A,B,C 來表示(可以通過這些操作改變魔板的狀態):
A:交換上下兩行;
B:將最右邊的一列插入到最左邊;
C:魔板中央對的4個數作順時針旋轉,下面是對基本狀態進行操作的示范:
A:
8 7 6 5 1 2 3 4B:
4 1 2 3 5 8 7 6C:
1 7 2 4 8 6 3 5對于每種可能的狀態,這三種基本操作都可以使用,
你要編程計算用最少的基本操作完成基本狀態到特殊狀態的轉換,輸出基本操作序列,
注意:資料保證一定有解,
輸入格式
輸入僅一行,包括 8 個整數,用空格分開,表示目標狀態,
輸出格式
輸出檔案的第一行包括一個整數,表示最短操作序列的長度,
如果操作序列的長度大于0,則在第二行輸出字典序最小的操作序列,
資料范圍
輸入資料中的所有數字均為 1 到 88 之間的整數,
輸入樣例:
2 6 8 4 5 7 3 1輸出樣例:
7
BCABCCB
思路:
-
將初始狀態加入佇列,然后去搜索擴展,直到搜索到目標狀態為止,每一次擴展ABC三種操作后,如果該狀態沒被遍歷過,更新最短距離并加入佇列,
-
由于要存盤路徑,因此我們開一個
pre陣列,記錄當前狀態的前驅狀態,最終由終點逆推回去即可獲得整個路徑(上一篇博客的迷宮問題也是存盤路徑),在記錄前驅狀態的同時也把操作方式記錄下來 -
狀態的存盤:看代碼解釋
-
狀態的切換:字串與一個
2行4列的一個二維表的相互切換,以這個二維表為橋梁具體實作A、B、C三種操作
我們只需要按照先進行操作A,再B后C的操作順序,最終得到的結果就會滿足字典序最小,
【代碼實作】
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#include <unordered_map>
using namespace std;
char g[2][4];//狀態圖(狀態圖作為操作狀態轉換的一個橋梁)
// key-value
unordered_map<string, int> dist;// 記錄到當前狀態的最小步數
unordered_map<string, pair<char, string>> pre;// 存盤當前狀態的前驅,和操作方式
queue<string>q;
//在操作前 先把字串變化到 2行4列的圖表狀態,方便我們操作的實作
void set(string state)
{
for(int i = 0; i < 4; i ++) g[0][i] = state[i];
for(int i = 7, j = 0; j < 4; i --, j ++) g[1][j] = state[i];
}
//在set得到的圖基礎上 在進行了ABC操作后得到新的狀態圖 將改圖轉換為狀態字串,最為操作后得到的字串結果回傳
string get()
{
string res;
for(int i = 0; i < 4; i ++) res += g[0][i];
for(int j = 3; j >= 0; j --) res += g[1][j];
return res;
}
string move0(string state)//操作A:交換上下兩行
{
set(state);
for(int i = 0; i < 4; i ++) swap(g[0][i], g[1][i]);
return get();
}
string move1(string state)//操作B:將最右邊的一列插入到最左邊
{
set(state);
//(先將最后一列存下來,將前3列后移一列,最后一列移到最左邊)
char v0 = g[0][3], v1 = g[1][3];
for(int i = 3; i >= 0; i --)
{
g[0][i] = g[0][i - 1];
g[1][i] = g[1][i - 1];
}
g[0][0] = v0, g[1][0] = v1;
return get();
}
string move2(string state)//操作C:魔板中央對的4個數作順時針旋轉
{
set(state);
char v = g[0][1];
g[0][1] = g[1][1];
g[1][1] = g[1][2];
g[1][2] = g[0][2];
g[0][2] = v;
return get();
}
int bfs(string start, string end)
{
if(start == end) return 0;//如果一開始的狀態即為目標狀態
q.push(start);
dist[start] = 0;
//擴展隊頭元素
while(q.size())
{
auto t = q.front();
q.pop();
//每種狀態有三種操作方式,每次操作后得到對應的字串結果
string m[3];
m[0] = move0(t);
m[1] = move1(t);
m[2] = move2(t);
//遍歷三種狀態方式,看看選哪個(按照先A后B再C操作得到的結果滿足字典序從小到大)
for(int i = 0; i < 3; i ++)
{
if(!dist.count(m[i]))//如果這個狀態沒有被遍歷過
{
q.push(m[i]);
dist[m[i]] = dist[t] + 1;//更新步數
pre[m[i]] = {'A' + i ,t};//將操作方式,是該鄰接點的前驅記錄下來
if(m[i] == end) return dist[end];// 如果在遍歷中發現已經到達目標狀態直接回傳結果
}
}
}
return -1;
}
int main()
{
string start, end;
start = "12345678";
for(int i = 0; i < 8; i ++)
{
int x;
cin >> x;
end += (x + '0');
}
// cout << end << endl;
int step = bfs(start, end);
cout << step << endl;
//通過終點的前驅逐一反推獲取操作的方式
string res;
while (end != start)
{
res += pre[end].first;//操作
end = pre[end].second;//狀態字串
}
reverse(res.begin(), res.end());//由于是逆推求前驅,因此結果要翻轉
if (step > 0) cout << res << endl;
return 0;
}
三、總結
最小步數模型的難點在于狀態的表示、切換還有存盤,理清它們之間的邏輯關系代碼就好實作了!
技巧:一維陣列與二維陣列坐標的轉換
設[一維陣列]下標為index(從0開始),二維陣列長度為m * n,則:
一維陣列轉換為二維陣列
x = index / n
y = index % n
二維陣列轉換為一維陣列
index = x + y * n
技巧:在最小步數模型中狀態和狀態的距離通常用哈希表來進行存盤(存在key-value的映射關系!),如map,unordered_map,
技巧:BFS存盤路徑問題,通常通過設定一個前驅pre陣列來記錄當前節點或者狀態的前驅,最后再逆推找出路徑
學習內容參考:
acwing演算法基礎課、提高課
注:如果文章有任何錯誤或不足,請各位大佬盡情指出,評論留言留下您寶貴的建議!如果這篇文章對你有些許幫助,希望可愛親切的您點個贊推薦一手,非常感謝啦

轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/418108.html
標籤:其他
上一篇:演算法競賽——多源BFS
