最小步数模型

一、简介

最小步数模型和最短路模型的区别?

最短路模型:某一个点到另一个点的最短距离(坐标与坐标之间)

最小步数模型:不再是点(坐标),而是状态到另一个状态的转变

BFS难点所在(最短路问题):

  1. 存储的数据结构:队列

    状态如何存储到队列里边(以什么形式)?

  2. 状态怎么表示,怎么转移?

3. dist

如何记录每一个状态的距离

技巧:在最小步数模型中状态和状态的距离通常用哈希表来进行存储(存在key-value的映射关系!),如mapunordered_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 4

B:

4 1 2 3
5 8 7 6

C:

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的映射关系!),如mapunordered_map

技巧:BFS存储路径问题,通常通过设置一个前驱pre数组来记录当前节点或者状态的前驱,最后再逆推找出路径

学习内容参考:

acwing算法基础课、提高课

注:如果文章有任何错误或不足,请各位大佬尽情指出,评论留言留下您宝贵的建议!如果这篇文章对你有些许帮助,希望可爱亲切的您点个赞推荐一手,非常感谢啦

带你学习BFS最小步数模型的更多相关文章

  1. 第十四届华中科技大学程序设计竞赛 J Various Tree【数值型一维BFS/最小步数】

    链接:https://www.nowcoder.com/acm/contest/106/J 来源:牛客网 题目描述 It's universally acknowledged that there'r ...

  2. yzoi2226最小步数的详细解法

    Description - 问题描述 在各种棋中,棋子的走法总是一定的,如中国象棋中马走“日”.有一位小学生就想如果马能有两种走法将增加其趣味性,因此,他规定马既能按“日”走,也能如象一样走“田”字. ...

  3. 1到n的最小步数

    1到n的最小步数 Time Limit: 1 Sec  Memory Limit: 128 MB 给你一个数n,让你求从1到n的最小步数是多少. 对于当前的数x有三种操作: 1:  x+1 2:  x ...

  4. 从零开始一起学习SLAM | 相机成像模型

    上一篇文章<从零开始一起学习SLAM | 为啥需要李群与李代数?>以小白和师兄的对话展开,受到了很多读者的好评.本文继续采用对话的方式来学习一下相机成像模型,这个是SLAM中极其重要的内容 ...

  5. 时间序列深度学习:状态 LSTM 模型预测太阳黑子

    目录 时间序列深度学习:状态 LSTM 模型预测太阳黑子 教程概览 商业应用 长短期记忆(LSTM)模型 太阳黑子数据集 构建 LSTM 模型预测太阳黑子 1 若干相关包 2 数据 3 探索性数据分析 ...

  6. ny58 最小步数

    最少步数 时间限制:3000 ms  |  内存限制:65535 KB 难度:4 描述 这有一个迷宫,有0~8行和0~8列: 1,1,1,1,1,1,1,1,1 1,0,0,1,0,0,1,0,1 1 ...

  7. JavaScript学习系列之内存模型篇

    一个热爱技术的菜鸟...用点滴的积累铸就明日的达人 正文 如果真的想学好一门语言,那么一定要了解它内存模型,本篇文章就带你走进JavaScript的内存模型,由于本人才疏学浅,若有什么表述有误的地方, ...

  8. bzoj 2039 最小割模型

    比较明显的网络流最小割模型,对于这种模型我们需要先求获利的和,然后减去代价即可. 我们对于第i个人来说, 如果选他,会耗费A[I]的代价,那么(source,i,a[i])代表选他之后的代价,如果不选 ...

  9. 2019 HDU 多校赛第二场 HDU 6598 Harmonious Army 构造最小割模型

    题意: 有n个士兵,你可以选择让它成为战士还是法师. 有m对关系,u和v 如果同时为战士那么你可以获得a的权值 如果同时为法师,你可以获得c的权值, 如果一个为战士一个是法师,你可以获得b的权值 问你 ...

随机推荐

  1. Spring实现自定义注解并且配置拦截器进行拦截

    有时候我们会自定义注解,并且需要配置拦截器对请求方法含有该自定义注解的方法进行拦截操作 自定义注解类 NeedToken.java import java.lang.annotation.Docume ...

  2. cmake之生成动态库

    演示源码下载地址: https://github.com/mohistH/demo_cmake_dylib 把文仅为参考. 以实际情况为准 1.目录结构 │ CMakeLists.txt │ inde ...

  3. 【LeetCode】312. Burst Balloons 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/burst-ba ...

  4. 【LeetCode】761. Special Binary String 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/special- ...

  5. 【LeetCode】659. Split Array into Consecutive Subsequences 解题报告(Python)

    [LeetCode]659. Split Array into Consecutive Subsequences 解题报告(Python) 标签(空格分隔): LeetCode 作者: 负雪明烛 id ...

  6. A. Toda 2

    A. Toda 2 time limit per test 2 seconds memory limit per test 512 megabytes input standard input out ...

  7. golang 数组的一些自问自答

    所有代码基于Go-1.17.一些研究Go数组的自问自答,可以考虑作为面试题. 问题:静态存储区是什么?和堆/栈有什么区别? 回答: 可以参考下列图 堆上存放new产生的大块内存 栈上存放的是程序运行的 ...

  8. Codeforces 931C:Laboratory Work(构造)

    C. Laboratory Work time limit per test : 1 second memory limit per test : 256 megabytes input : stan ...

  9. 基于Spring MVC + Spring + MyBatis的【图书资源管理系统】

    资源下载:https://download.csdn.net/download/weixin_44893902/45598347 练习点设计:模糊查询.删除.新增 一.语言和环境 实现语言:JAVA语 ...

  10. Vue的安装及使用(Vue的三种安装使用方式)

    vue是一个JavaMVVM库,是一套用于构建用户界面的渐进式框架,是初创项目的首选前端框架.它是以数据驱动和组件化的思想构建的,采用自底向上增量开发的设计.它是轻量级的,它有很多独立的功能或库,我们 ...