【图论】最小步数(双向广搜与A*算法)

这篇具有很好参考价值的文章主要介绍了【图论】最小步数(双向广搜与A*算法)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

算法提高课笔记

最小步数

魔板

原题链接

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 到 8 之间的整数。

输入样例

2 6 8 4 5 7 3 1

输出样例

7
BCABCCB

题意

给出一个原始状态,问怎么经过最少步骤转换成特殊状态,以及转换步骤是什么

思路

思路比较简单但是代码实现有难度,依然是把原始状态先入队,然后每次更新队头序列可以进行的操作
每个操作直接打表更简单

代码

#include <bits/stdc++.h>

using namespace std;

char a[10];
string endd;
map<string, int> dist; // 存储每一个状态到最终状态的距离
map<string, pair<char, string> > pre; // 存储从后一个状态到前一个状态的具体操作

string get(string t, int op) // 模拟每个操作
{
    string k;
    if (op == 0) k = {t[4], t[5], t[6], t[7], t[0], t[1], t[2], t[3]};
    if (op == 1) k = {t[3], t[0], t[1], t[2], t[7], t[4], t[5], t[6]};
    if (op == 2) k = {t[0], t[5], t[1], t[3], t[4], t[6], t[2], t[7]};
    return k;
}

int bfs()
{
    string s = "12348765";
    queue<string> q;
    dist[s] = 0;
    q.push(s);

    while (q.size())
    {
        auto t = q.front();
        q.pop();

        if (t == endd) return dist[t];

        for (int i = 0; i < 3; i ++ ) // 更新每个操作后的新序列
        {
            string s = get(t, i);
            if (!dist.count(s))
            {
                dist[s] = dist[t] + 1;
                pre[s] = {'A' + i, t};
                q.push(s);
            }
        }
    }
}

int main()
{
    string start = "12348765", ans;
    for (int i = 1; i <= 8; i ++ ) cin >> a[i];
    reverse(a + 5, a + 9);
    for (int i = 1; i <= 8; i ++ ) endd.push_back(a[i]);

    cout << bfs() << '\n';

    while (endd != start)
    {
        ans += pre[endd].first;
        endd = pre[endd].second;
    }
    reverse(ans.begin(), ans.end());
    cout << ans;
}

双向广搜

因为BFS越到后面,每一层可以扩展的情况越多(会成指数型增长),为了防止TLE / MLE,可以用双向广搜对其进行优化

从两端开始搜索,到中间相遇,使得搜索次数降低,避免了中间大部分不必要的搜索环节

字串变换

原题链接

已知有两个字串 A, B 及一组字串变换的规则(至多 6 个规则):

A1→B1
A2→B2

规则的含义为:在 A 中的子串 A1 可以变换为 B1、A2 可以变换为 B2…。

例如:A=abcd B=xyz

变换规则为:
abc → xu ud → y y → yz

则此时,A 可以经过一系列的变换变为 B,其变换的过程为:

abcd → xud → xy → xyz

共进行了三次变换,使得 A 变换为 B。

注意,一次变换只能变换一个子串,例如 A=aa B=bb

变换规则为:

a → b

此时,不能将两个 a 在一步中全部转换为 b,而应当分两步完成。

输入格式

输入格式如下:
A B
A1 B1
A2 B2
… …

第一行是两个给定的字符串 A 和 B。

接下来若干行,每行描述一组字串变换的规则。

所有字符串长度的上限为 20。

输出格式

若在 10 步(包含 10 步)以内能将 A 变换为 B ,则输出最少的变换步数;否则输出 NO ANSWER!。

输入样例

abcd xyz
abc xu
ud y
y yz

输出样例

3

题意

一个字符串想转变成另一个字符串,现在给出若干个变换规则(某个字串可以转换成另一个字串),问最少的步骤数目是多少

思路

从开始和结尾两个字符串同时往中间搜索,搜索过程中相遇就直接输出,具体看代码注释

代码

#include <bits/stdc++.h>

using namespace std;

const int N = 6;

int n;
string A, B;
string a[N], b[N];

int extend(queue<string>& q, unordered_map<string, int>& da, unordered_map<string, int>& db, string a[N], string b[N]) // 要处理da 从a->b
{
    int d = da[q.front()];
    while (q.size() && da[q.front()] == d)
    {
        auto t = q.front();
        q.pop();

        for (int i = 0; i < n; i ++ ) // 遍历每个规则
            for (int j = 0; j < t.size(); j ++ ) // 遍历每个元素作为开头
                if (t.substr(j, a[i].size()) == a[i])
                {
                    string r = t.substr(0, j) + b[i] + t.substr(j + a[i].size()); // 记录新字符串
                    if (db.count(r)) return da[t] + db[r] + 1; // 在另一个里找到了,直接输出
                    if (da.count(r)) continue; // 在自己里找到了,说明重复了直接开始下一次循环
                    da[r] = da[t] + 1; // 更新距离
                    q.push(r); // 新字符串入队
                }
    }
    return 11;
}

int bfs()
{
    if (A == B) return 0;
    queue<string> qa, qb; // qa qb两个队列同时搜索
    unordered_map<string, int> da, db; // 不同字符串到起始字符串的步骤数

    qa.push(A), qb.push(B); // 队头入队
    da[A] = db[B] = 0; // 初始化

    int step = 0;
    while (qa.size() && qb.size())
    {
        int t;
        // 每次对队列长的那边进行操作
        if (qa.size() < qb.size()) t = extend(qa, da, db, a, b);
        else t = extend(qb, db, da, b, a);

        if (t <= 10) return t;
        if ( ++ step == 10) return -1;
    }
    return -1;
}

int main()
{
    cin >> A >> B;
    while (cin >> a[n] >> b[n]) n ++ ;

    int t = bfs();
    if (t == -1) cout << "NO ANSWER!\n";
    else cout << t << '\n';
}

A*

A*与双向广搜解决的问题类似,利用估价函数,在非常少的步骤内从一个状态到达另一个状态

可以解决任何边权的问题,只要没有负权回路

步骤:

  1. 把BFS中的队列换成优先队列(小根堆),队列中存的是从起点到当前点的真实距离(不一定是最短距离)和从当前点到终点的估计距离(算法的尽头是玄学),优先队列排序的依据是存储的两个距离相加,也就是从起点到终点的估计距离
  2. 取出优先队列队头(也就是估计距离最短的点)
  3. for循环 t 的所有邻边,能更新的话就更新并将邻边入队
  4. 当终点第一次出队时break

(和Dijkstra非常像,可以看为Dijkstra中所有从当前点到终点的估计距离都取0((但本质上没什么关系

必须要保证当前点到终点的估计距离一定要小于等于真实距离,并且要保证有解(否则效率很低)

A*只能保证终点距离起点是最优的,不能保证其他点第一次出队时也是最优的,除了终点外,每个点不一定只扩展一次

八数码

原题链接

在一个 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

把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。

现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。

输入格式

输入占一行,将 3×3 的初始网格描绘出来。

例如,如果初始网格如下所示:

1 2 3 
x 4 6 
7 5 8 

则输入为:1 2 3 x 4 6 7 5 8

输出格式

输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。

如果答案不唯一,输出任意一种合法方案即可。

如果不存在解决方案,则输出 unsolvable

输入样例

2  3  4  1  5  x  7  6  8 

输出样例

ullddrurdllurdruldr

题意

每次可以修改一个数字上下左右移动,输出一个从给定状态转化到标准状态的最少步骤

思路

首先,怎么判断是否有解?

我们将九宫格中的数字转换成字符串,每次将数字左右移动时,字符串中的逆序对数量不变,上下移动时,逆序对数量总是改变2,所以奇偶性不变
我们知道最终要求的状态的逆序对是偶数,所以看初始状态的逆序对数量,是偶数就一定有解,是奇数就一定无解

然后是最关键的问题:怎么设计估计函数?

注意到每次移动,最好的情况是将移动的数字向目标位置移动一格,因此估价函数可以取当前的每一个数字和最终目标位置之间的曼哈顿距离之和(这样可以保证估计值一定小于等于真实值)

代码(+详细注释)

#include <bits/stdc++.h>

using namespace std;

typedef pair<int, string> PIS;
#define ft first
#define sd second

int f(string state) // 估价函数(曼哈顿距离)
{
    int res = 0;
    for (int i = 0; i < state.size(); i ++ )
        if (state[i] != 'x')
        {
            int t = state[i] - '1'; // 转换成数字
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3); // 计算曼哈顿距离
        }
    return res;
}

string bfs(string start)
{
    string end = "12345678x";
    char op[] = "urdl";
    int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};

    unordered_map<string, int> dist; // 记录每个状态距离初始状态的步数
    unordered_map<string, pair<char, string> > prev; // 前一个字符串是由后一个字符串经过什么操作得来的
    priority_queue<PIS, vector<PIS>, greater<PIS> > heap; // 第一个参数是估计距离 第二个参数是当前状态

    dist[start] = 0; // 初始化
    heap.push({f(start), start});

    while (heap.size())
    {

        auto t = heap.top(); // 取出队头元素
        heap.pop();

        string state = t.sd; // 当前状态
        if (state == end) break;

        int x, y; // xy表示空位的坐标
        for (int i = 0; i < state.size(); i ++ )
            if (state[i] == 'x')
            {
                x = i / 3, y = i % 3;
                break;
            }

        string source = state; // 记录当前状态
        for (int i = 0; i < 4; i ++ ) // 遍历四个操作
        {
            int a = x + dx[i], b = y + dy[i]; // 操作后坐标
            if (a < 0 || a >= 3 || b < 0 || b >= 3) continue; // 位置不合法
            state = source; // 将state在每个操作前恢复至初始状态
            swap(state[x * 3 + y], state[a * 3 + b]); // 修改操作后字符窜
            if (!dist.count(state) || dist[state] > dist[source] + 1) // 没更新过或者有更优解
            {
                dist[state] = dist[source] + 1; // 更新距离
                prev[state] = {op[i], source}; // 记录操作
                heap.push({dist[state] + f(state), state}); // 存进优先队列中
            }
        }
    }

    string res; // 记录操作字符串
    while (end != start)
    {
        res += prev[end].ft;
        end = prev[end].sd;
    }
    reverse(res.begin(), res.end());
    return res;
}

int main()
{
    string start, seq;
    char c;

    while (cin >> c)
    {
        start += c;
        if (c != 'x') seq += c;
    }

    int cnt = 0; // 逆序对数量
    for (int i = 0; i < 8; i ++ )
        for (int j = i; j < 8; j ++ )
            if (seq[i] > seq[j]) cnt ++ ;

    if (cnt & 1) cout << "unsolvable\n";
    else cout << bfs(start) << '\n';
}

第K短路

原题链接

给定一张 N 个点(编号 1,2…N),M 条边的有向图,求从起点 S 到终点 T 的第 K 短路的长度,路径允许重复经过点或边。

注意: 每条最短路中至少要包含一条边。

输入格式

第一行包含两个整数 N 和 M。

接下来 M 行,每行包含三个整数 A,B 和 L,表示点 A 与点 B 之间存在有向边,且边长为 L。

最后一行包含三个整数 S,T 和 K,分别表示起点 S,终点 T 和第 K 短路。

输出格式

输出占一行,包含一个整数,表示第 K 短路的长度,如果第 K 短路不存在,则输出 −1。

数据范围

1 ≤ S , T ≤ N ≤ 1000,
0 ≤ M ≤ 104,
1 ≤ K ≤ 1000,
1 ≤ L ≤ 100

输入样例

2 2
1 2 5
2 1 4
1 2 2

输出样例

14

题意

找到起点到终点第k短的路径

思路

这一题和之前不同的地方在于,之前只有需要更新距离的时候才将点存进队列,现在不管是否需要更新距离,都要存进队列

估价函数:从起点到终点的最短路(用Dijkstra求)(这样就可以保证估计距离小于等于真实距离)

之前求最短路的时候我们认为终点第一次从队列中弹出时是最小值,在这里第k次从队列中弹出时是第k小值(因为每个点最多只能走不超过K次)(可以证明)文章来源地址https://www.toymoban.com/news/detail-621484.html

代码( + 详细注释)

#include <bits/stdc++.h>

using namespace std;

const int N = 1010, M = 200010;

typedef pair<int, int> PII;
typedef pair<int, PII> PIII;
#define ft first 
#define sd second 

int n, m, S, T, K;
int h[N], rh[N], e[M], w[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N]; // 判重

void add(int h[], int a, int b, int c)
{
    e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}

void dijkstra()
{
    priority_queue<PII, vector<PII>, greater<PII> > heap; // 第二个参数是点 第一个参数是第二个参数到终点的距离
    heap.push({0, T}); // 终点入队

    memset(dist, 0x3f, sizeof dist);
    dist[T] = 0; // 初始化

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.sd; // 记录当前遍历的点
        if (st[ver]) continue;
        st[ver] = true;

        for (int i = rh[ver]; ~i; i = ne[i])
        {
            int j = e[i];
            if (dist[j] > dist[ver] + w[i]) // 不是最优解就更新距离并入队
            {
                dist[j] = dist[ver] + w[i];
                heap.push({dist[j], j});
            }
        }
    }
}

int astar()
{
    priority_queue<PIII, vector<PIII>, greater<PIII> > heap; // 第一个参数是估计距离 第二个参数中 ft:到起始点的距离 sd:当前点
    heap.push({dist[S], {0, S}});

    while (heap.size())
    {
        auto t = heap.top();
        heap.pop();

        int ver = t.sd.sd, distance = t.sd.ft; // distance是到起点的真实距离
        cnt[ver] ++ ; // 第几次遍历该点
        if (cnt[T] == K) return distance;

        for (int i = h[ver]; ~i; i = ne[i]) // 遍历所有邻接点
        {
            int j = e[i];
            if (cnt[j] < K) // 没遍历k次以上的就入队
                heap.push({distance + w[i] + dist[j], {distance + w[i], j}});
        }
    }
    return -1;
}

int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);

    cin >> n >> m;
    memset(h, -1, sizeof h);
    memset(rh, -1, sizeof rh);

    for (int i = 0; i < m; i ++ )
    {
        int a, b, c;
        cin >> a >> b >> c;
        add(h, a, b, c); // 存正向边
        add(rh, b, a, c); // 存反向边(因为dijkstra里需要计算所有点到终点的最短路
    }
    cin >> S >> T >> K;
    if (S == T) K ++ ;

    dijkstra();

    cout << astar() << '\n';
}

到了这里,关于【图论】最小步数(双向广搜与A*算法)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 图论与算法(遍历、最小生成树、最短路径)

    图是由顶点集合及顶点间的关系组成的一种数据结构:G = (V, E),其中:顶点集合V = {x|x属于某个数据对象集}是有穷非空集合;E = {(x,y)|x,y属于V}或者E = {x, y|x,y属于V Path(x, y)}是顶点间关系的有穷集合,也叫做边的集合。(x, y)表示x到y的一条双向通路,即(x, y)是无方向的;Path

    2024年04月14日
    浏览(45)
  • 【算法基础:搜索与图论】3.5 求最小生成树算法(Prim&Kruskal)

    最小生成树 有关树的定义 生成子图 :生成子图是从原图中选取部分节点以及这些节点之间的边所组成的图。生成子图中的所有节点和边都必须在原图中存在。 生成树 :一个连通 无向图 的 生成子图 ,同时要求是树。也即在图的边集中选择 n - 1 条,将所有顶点连通。 我们

    2024年02月16日
    浏览(40)
  • 华为OD机试 - 求最小步数(Java & JS & Python)

    题目描述 求从坐标零点到坐标点n的最小步数,一次只能沿横坐标轴向左或向右移动 2 或 3。 注意:途径的坐标点可以为负数 输入描述 坐标点n 输出描述 输出从坐标零点移动到坐标点n的最小步数 备注 1 = n = 10^9 用例 输入 4 输出 2 说明 从坐标零点移动到4,最小需要两步,即

    2024年02月13日
    浏览(45)
  • acwing算法提高之图论--最小生成树的扩展应用

    本专题用来记录使用最小生成树算法(prim或kruskal)解决的扩展题目。 题目1 :1146新的开始 C++代码如下, 题目2 :1145北极通讯网络 C++代码如下, 题目3 :346走廊泼水节 C++代码如下, 题目4 :1148秘密的牛奶运输 C++代码如下,

    2024年04月16日
    浏览(38)
  • acwing算法提高之图论--最小生成树的典型应用

    本专题用来记录使用prim算法或kruskal算法求解的题目。 题目1 :1140最短网络 C++代码如下, 题目2 :1141局域网 C++代码如下, 题目3 :1142繁忙的都市 C++代码如下, 题目4 :1143联络员 C++代码如下, 题目5 :1144连接格点 C++代码如下,

    2024年04月27日
    浏览(41)
  • 【算法入门&图论】【模板】拓扑排序|【模板】单源最短路2 |最小生成树

    ✅作者简介:热爱后端语言的大学生,CSDN内容合伙人 ✨精品专栏:C++面向对象 🔥系列专栏:算法百炼成神 本专栏收录的均为牛客网的算法题目,内含链表、双指针、递归、动态规划、基本数据结构等算法思想的具体运用。牛客网不仅有大量的经典算法题目,也有大厂的面

    2024年02月03日
    浏览(46)
  • 数学建模十大算法04—图论算法(最短路径、最小生成树、最大流问题、二分图)

    一、最短路径问题 从图中的某个顶点出发,到达另一个顶点的 所经过的边的权重之和最小 的一条路径。 1.1 两个指定顶点之间的最短路径 问题如下:给出了一个连接若干个城镇的铁路网络,在这个网络的两个指定城镇间,求一条最短铁路线。 1.1.1 Dijkstra算法 迪杰斯特拉(D

    2024年02月02日
    浏览(73)
  • 【算法每日一练]-图论(保姆级教程篇7 最小生成树 ,并查集模板篇)#村村通 #最小生成树

    目录 题目:村村通 并查集  题目:最小生成树 kruskal算法 prim算法          先引入问题: 要在n个城市之间铺设光缆,主要目标是要使这 n 个城市的 任意两个之间都可以通信 ,但铺设光缆的费用很高,且各个城市之间铺设光缆的费用不同,因此另一个目标是要 使铺设光

    2024年02月04日
    浏览(78)
  • 【华为OD统一考试B卷 | 100分】求最小步数(C++ Java JavaScript Python)

    在线OJ 已购买本专栏用户,请私信博主开通账号,在线刷题!!! 运行出现 Runtime Error 0Aborted,请忽略 华为OD统一考试A卷+B卷 新题库说明 2023年5月份,华为官方已经将的 2022/0223Q(1/2/3/4)统一修改为OD统一考试(A卷)和OD统一考试(B卷)。 你收到的链接上面会标注A卷还是B卷。

    2024年02月15日
    浏览(43)
  • 【算法导论】图论(图的基本概念,图上的深度优先搜索(DFS),广度优先搜索(BFS),最小生成树(MST)及Prim,Kruskal算法)

    图(Graph)是一种包含节点与节点的边的集合,记作G=(V,E),V是节点的集合,E是边的集合。 有向图 一个有向图G=(V,E),E中每个元素是V上的一个二值关系:一条从a出发的连向b的边e可以记作一个 有序 对e = (a,b) 。 无向图 一个无向图G=(V,E),E的每个元素e可以表示V上的一个 无序 对,记

    2024年02月03日
    浏览(52)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包