洛谷题单指南-进阶搜索-P5507 机关

原题链接:https://www.luogu.com.cn/problem/P5507

题意解读:有12个旋钮,每个旋钮的值是1~4,每个旋钮旋转后值加1,如果是4旋转则变成1,另外每个旋钮在1~4不同状态下旋转,可以带动另一个旋钮旋转,被带动旋转的不能再带动下一级旋转,给出每个旋钮的起始状态,求所有旋钮都变成1最少需要多少步。

解题思路:与八数码问题类似,也是一个最小步数模型,直觉上可以使用BFS求解。

分析一下复杂度,每一次操作有12种选择,对应12种状态,如果扩展8层,将达到12^8=429981696,状态爆炸了,超时是必然。

对于BFS问题,如果状态空间巨大,主要有两种优化手段:A*算法、双向BFS算法,下面介绍用两种方法实现本题。

方法一:A*算法。

A*算法也是在BFS的基础之上,每次取最有可能更快到达终点的状态进行扩展,如何实现选取最有可能更快到达终点的状态呢?

我们定义dist[stat]表示状态stat距离起始状态的步数,f[stat]表示状态stat到目标终点的最小步数(通常约定f[stat]<=stat到终点的真实最优步数)。

那么每次状态扩展后,将状态stat加入队列时,同时要记录dist[stat] + f[stat],未来要优先扩展dist[stat] + f[stat]最小的状态,要实现优先扩展最小的状态,就不能用一般的queue,可以借助priority_queue。

如何计算f[stat]?对于本题,可以将所有旋钮到1的步数累加,结果除以2。

如何表示状态呢?首先想到的是直接用string就好了。

90分代码:

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, string> PIS;

string s = "0"; //初始状态,开头补0是为了方便下标从1开始
string target = "0111111111111"; //目标状态,开头补0是为了方便下标从1开始
int mp[13][5]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
unordered_map<string, int> dist; //dist[stat]表示从初始状态到stat状态的最短步数
unordered_map<string, PIS> pre; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号
priority_queue<PIS, vector<PIS>, greater<PIS> > q;

int f(string stat) //估价函数最小值是所有旋钮到1的距离之和的一半
{
    int res = 0;
    for(int i = 1; i <= 12; i++)
    {
        if(stat[i] != '1') res += 5 - (stat[i] - '0');
    }
    return res / 2.0; 
}

void astar(string start)
{
    q.push({f(start), start});
    dist[start] = 0;
    while(q.size())
    {
        PIS p = q.top(); q.pop();
        string stat = p.second;
        if(stat == target)
        {
            cout << dist[stat] << endl;
            vector<int> ans;
            while(stat != start)
            {
                ans.push_back(pre[stat].first);
                stat = pre[stat].second;
            }
            for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
            return;
        }
        
        for(int i = 1; i <= 12; i++)
        {
            string tmp = stat;
            int ii = mp[i][tmp[i] - '0']; // 旋转第i个旋钮会引起共同旋转的旋钮编号
            tmp[ii] = '0' + (tmp[ii] - '0') % 4 + 1; // 旋转第ii个旋钮
            tmp[i] = '0' + (tmp[i] - '0') % 4 + 1; // 旋转第i个旋钮
           
            if(!dist.count(tmp) || dist[tmp] > dist[stat] + 1) //如果没有到达过或者到达的步数更少
            {
                dist[tmp] = dist[stat] + 1;
                pre[tmp] = {i, stat};
                q.push({dist[tmp] + f(tmp), tmp});
            }
        }
    }
}

int main()
{
    int a, b, c, d, e;
    for(int i = 1; i <= 12; i++)
    {
        cin >> a >> b >> c >> d >> e;
        s += a + '0';
        mp[i][1] = b;
        mp[i][2] = c;
        mp[i][3] = d;
        mp[i][4] = e;
    }
    astar(s);
    return 0;
}

卡在了最后一个测试点,状态表示用string时,采用map导致的hash计算会产生开销,可以直接通过一个24位的二进制表示状态,每2位表示一个旋钮,旋钮的状态1~4调整为0~3,这样00表示0,01表示1,10表示2,11表示3,通过位运算即可实现状态的操作和变换。

100分代码:

#include <bits/stdc++.h>
using namespace std;
//为便于计算,将旋钮状态1~4分别用0~3表示
//状态用24位二进制表示,每两位表示一个旋钮的状态,00,01,10,11分别表示0,1,2,3
//旋钮编号1~12分别用0~11表示
typedef pair<int, int> PIS;

int start; //初始状态,输入时填充
int target = 0; //目标状态, 24位全0
int mp[12][4]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
int dist[1 << 24]; //dist[stat]表示从初始状态到stat状态的最短步数
PIS pre[1 << 24]; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号
priority_queue<PIS, vector<PIS>, greater<PIS> > q;

int f(int stat) //估价函数最小值是所有旋钮到0的距离之和的一半
{
    int res = 0;
    for(int i = 0; i < 12; i++)
    {
        if(stat >> (i * 2) & 3 != 0) res += 5 - (stat >> (i * 2) & 3);
    }
    return res / 2.0; 
}

void astar()
{
    memset(dist, 0x3f, sizeof(dist));
    q.push({f(start), start});
    dist[start] = 0;
    while(q.size())
    {
        PIS p = q.top(); q.pop();
        int stat = p.second;
        if(stat == target)
        {
            cout << dist[stat] << endl;
            vector<int> ans;
            while(stat != start)
            {
                ans.push_back(pre[stat].first);
                stat = pre[stat].second;
            }
            for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
            return;
        }
        
        for(int i = 0; i < 12; i++)
        {
            int tmp = stat;
            int ii = mp[i][tmp >> (i * 2) & 3]; // 旋转第i个旋钮会引起共同旋转的旋钮编号
            int iadd = ((tmp >> (i * 2) & 3) + 1) % 4; // 旋转第i个旋钮
            int iiadd = ((tmp >> (ii * 2) & 3) + 1) % 4; // 旋转第ii个旋钮
            tmp &= ~(3 << (i * 2));
            tmp &= ~(3 << (ii * 2));
            tmp |= iadd << (i * 2);
            tmp |= iiadd << (ii * 2);
           
            if(dist[tmp] > dist[stat] + 1) //如果没有到达过这个状态或者到达这个状态的步数更少
            {
                dist[tmp] = dist[stat] + 1;
                pre[tmp] = {i + 1, stat}; //旋钮编号加1
                q.push({dist[tmp] + f(tmp), tmp});
            }
        }
    }
}

int main()
{
    int a, b, c, d, e;
    for(int i = 0; i < 12; i++)
    {
        cin >> a >> b >> c >> d >> e;
        start |= (a - 1) << (i * 2);
        mp[i][0] = b - 1;
        mp[i][1] = c - 1;
        mp[i][2] = d - 1;
        mp[i][3] = e - 1;
    }
    astar();
    return 0;
}

方法二:双向BFS

所谓双向BFS,是同时从状态起点、终点进行BFS,用两个队列分别进行扩展状态,当其中一个队列的状态扩展时,如果发现已经在另一个方向处理过了,那么双向搜索就相遇,这样从相遇的状态点到起点、终点的距离之和就行总的步数,同样可以保存相遇点到起点、终点的路径。

实际执行时,可以根据两个队列的长度,优先扩展长度较少的。

100分代码:

#include <bits/stdc++.h>
using namespace std;

typedef pair<int, int> PIS;

int start; //初始状态,输入时填充
int target = 0; //目标状态, 24位全0
int mp[12][4]; //mp[i][j]表示第i个旋钮在状态j时旋转会引起共同旋转的旋钮编号
int dist1[1 << 24], dist2[1 << 24]; //dist[stat]表示从初始状态到stat状态的最短步数
PIS pre1[1 << 24], pre2[1 << 24]; //pre[stat]表示stat状态的上一个状态以及旋转的旋钮编号

void showpath(int stat)
{
    cout << dist1[stat] + dist2[stat]<< endl;
    int tmp = stat;
    vector<int> ans;
    int cnt = 0;
    while(tmp != start)
    {
        ans.push_back(pre1[tmp].first);
        tmp = pre1[tmp].second;
    }
    for(int i = ans.size() - 1; i >= 0; i--) cout << ans[i] << " ";
    tmp = stat;
    while(tmp != target)
    {
        cout << pre2[tmp].first << " ";
        tmp = pre2[tmp].second;
    }                 
}

void bfs()
{
    queue<int> q1, q2;
    q1.push(start);
    q2.push(target);
    dist1[start] = 0;
    dist2[target] = 0;
    while(q1.size() && q2.size())
    {
        if(q1.size() < q2.size()) //双向BFS,每次选择节点少的一端进行扩展
        {
            int stat = q1.front(); q1.pop();
            if(dist2[stat])
            {
                showpath(stat);    
                return;
            }
            for(int i = 0; i < 12; i++)
            {
                int nstat = stat;
                int ii = mp[i][nstat >> (i * 2) & 3]; // 旋转第i个旋钮会引起共同旋转的旋钮编号
                int iadd = ((nstat >> (i * 2) & 3) + 1) % 4; // 旋转第i个旋钮
                int iiadd = ((nstat >> (ii * 2) & 3) + 1) % 4; // 旋转第ii个旋钮
                nstat &= ~(3 << (i * 2));
                nstat &= ~(3 << (ii * 2));
                nstat |= iadd << (i * 2);
                nstat |= iiadd << (ii * 2);
                if(dist1[nstat]) continue; //如果已经到达过这个状态就不用再次到达了
                dist1[nstat] = dist1[stat] + 1;
                pre1[nstat] = {i + 1, stat}; //旋钮编号加1
                q1.push(nstat);
            }
        }
        else
        {
            int stat = q2.front(); q2.pop();
            if(dist1[stat])
            {
                showpath(stat);
                return;
            }
            for(int i = 0; i < 12; i++)
            {
                int nstat = stat;
                int last = ((nstat >> (i * 2) & 3) - 1 + 4) % 4; //第i个旋钮的上一个状态
                int ii = mp[i][last]; //第i个旋钮会引起共同旋转的旋钮编号
                int iadd = last; //旋转第i个旋钮
                int iiadd = ((nstat >> (ii * 2) & 3) - 1 + 4) % 4; //旋转第ii个旋钮
                nstat &= ~(3 << (i * 2));
                nstat &= ~(3 << (ii * 2));
                nstat |= iadd << (i * 2);
                nstat |= iiadd << (ii * 2);
                if(dist2[nstat]) continue; //如果已经到达过这个状态就不用再次到达了
                dist2[nstat] = dist2[stat] + 1;
                pre2[nstat] = {i + 1, stat}; //旋钮编号加1
                q2.push(nstat);
            }
        }
    }
}

int main()
{
    int a, b, c, d, e;
    for(int i = 0; i < 12; i++)
    {
        cin >> a >> b >> c >> d >> e;
        start |= (a - 1) << (i * 2);
        mp[i][0] = b - 1;
        mp[i][1] = c - 1;
        mp[i][2] = d - 1;
        mp[i][3] = e - 1;
    }
    bfs();
    return 0;
}

 

posted @   五月江城  阅读(7)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
历史上的今天:
2024-02-18 洛谷题单指南-递推与递归-P2437 蜜蜂路线
2024-02-18 洛谷题单指南-递推与递归-P1928 外星密码
点击右上角即可分享
微信分享提示