[状压DP][BFS][哈希]JZOJ 3243 Cube

本文介绍了一种解决密室逃脱游戏中立方体颜色转移问题的算法。通过状压DP结合哈希和邻接表,实现对立方体状态和地图颜色状态的有效追踪,最终找到使立方体所有面着色并逃出密室的最小步数。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Description
你被困在一个密室里。经过一轮摸索,你在密室里有所发现:

1.密室是一个呈m×n网格的长方形,地面有六个格子被上了色;

2.密室地面部分格子可能有障碍物;

3.密室的某一格有一个六面都没上色的立方体;

4.当立方体滚动到相邻无障碍物的格子,如果立方体接触地面的一面没有颜色而地面有颜色,则该颜色会从地面转移到立方体上;如果立方体接触地面的一面有颜色而地面没有颜色,则该颜色会从立方体转移到地面上;如果立方体接触地面的一面和地面都有颜色,则两者的颜色都不会转移。

5.当立方体滚动到密室的指定地点,如果立方体六面都被涂上了颜色,则传送门就会开启,让你逃离这个密室。

由于时间紧迫,你必须借助计算机的力量,算出立方体最少滚动几次就可以让你逃离这个密室。
 
Input
输入的第一行是用空格分隔的两个正整数m和n(2<=m<=20,2<=n<=20),分别代表密室的高和宽。接下来的m行,每行有n个字符,含义如下:

‘.’:该格子是空白的;

‘#’:该格子有障碍物;

‘P’:该格子是上色的;

‘C’:立方体就在这个格子;

‘G’:能让你逃离密室的指定地点。

整个密室保证正好有6个格子是‘P’,1个格子是‘C’,1个格子是‘G’,‘.’的格子不超过12个。
Output
输出仅一个整数,代表立方体最少滚动的次数。我们保证每个密室场景都有解。

对以下第一个样例,立方体最少只需要滚动10次(下右右上右右下左右左)。
 
Sample Input
输入1:
2 5
C.PPP
PPPG.

输入2:
4 5
C....
G##.P
.##PP
..PPP

输入3:
3 3
PPP
PCP
PG.

输入4:
2 10
.PPPCPPP..
....G.....
Sample Output
输出1:
10

输出2:
23

输出3:
15

输出4:
21
 
Data Constraint
2<=m<=20,2<=n<=20

分析

今日最难,一个旗鼓相当的对手

首先别被总点数迷惑了,总点数400个,但输入要求告诉我们,能走的点包括起点和终点及颜色,加起来不超过20个

那么我们可以考虑状压,给每个点标号,状态表示哪些点有颜色

这仅仅是图上的状态而已!因为立方体会带走颜色,所以另设一个6位状态,表示立方体当前每个面是否有颜色,建议在写的时候用记事本记着,忘掉了的话转移转死了

两种状态需要放在一起,所以整个状态26位,后6位位立方体状态,前20位为图上状态

然后我们考虑到,会有重复的状态,可是光是这个状态重复不够,有时图上和立方体一样,但是立方体位置不一样!

重复的状态可用哈希加邻接表(或指针)解决

最恶心的部分就是当你的立方体移动时,你的6位颜色状态全部要变一波,一坨位运算堆在一起

同时注意判断一下,当颜色状态中的下面状态与当前立方体在图上位置的状态不一样(一个有颜色一个没颜色)时,需要给他们变状态

空间别开爆了,我12Wkb刚好卡过去

 

#include <iostream>
#include <cstdio>
#include <cstring>
#include <memory.h>
#include <queue>
#include <cstdlib>
using namespace std;
const int N=4e2+10;
const int P=388211;
struct SS {
    int u,S;
};
struct QS {
    SS s;
    int dep;
};
struct Graph {
    int v,nx;
}e[10000000];
int cnt;
int n,m;
char c[N][N];
int id[N][N],icnt,g[21][4];
int sx,sy,tx,ty,sS;
queue<QS> q;
int p[21][517619];

bool Query(SS a) {
    int i=a.S%P;
    for (int j=p[a.u][i];j;j=e[j].nx)
        if (e[j].v==a.S) return 1;
    return 0;
}

void Insert(SS a) {
    int i=a.S%P;
    e[++cnt]=(Graph){a.S,p[a.u][i]};p[a.u][i]=cnt;
}

int Roll(int S,int type) {
    switch (type) {
        case 0:return (S&48)|((S&3)<<2)|((S&4)>>1)|((S&8)>>3);
        case 1:return (S&48)|((S&1)<<3)|((S&2)<<1)|((S&12)>>2);
        case 2:return ((S&3)<<4)|(S&12)|((S&16)>>3)|((S&32)>>5);
        case 3:return ((S&1)<<5)|((S&2)<<3)|(S&12)|((S&48)>>4);
    }
}

void Solve(int u,int bS,int cS,int dep) {
    if ((cS&1)^((bS>>u-1)&1)) cS^=1,bS^=(1<<u-1);
    if (!bS&&u==id[tx][ty]) {
        printf("%d",dep-1);
        exit(0);
    }
    int S=bS|(cS<<icnt);
    if (Query((SS){u,S})) return;
    q.push((QS){(SS){u,S},dep});
    Insert((SS){u,S});
}

void BFS() {
    q.push((QS){(SS){id[sx][sy],sS},1});
    Insert((SS){id[sx][sy],sS});
    while (!q.empty()) {
        QS a=q.front();q.pop();
        int u=a.s.u,bS=a.s.S&((1<<icnt)-1),cS=a.s.S>>icnt,dep=a.dep;
        for (int i=0;i<4;i++)
            if (g[u][i]) {
                int scS=Roll(cS,i);
                Solve(g[u][i],bS,scS,dep+1);
            }
    }
}

int main() {
    scanf("%d%d",&m,&n);
    for (int i=1;i<=m;i++) scanf("%s",c[i]+1);
    for (int i=1;i<=m;i++)
        for (int j=1;j<=n;j++) {
            if (c[i][j]!='#') id[i][j]=++icnt;
            if (c[i][j]=='C') sx=i,sy=j;
            if (c[i][j]=='G') tx=i,ty=j;
            if (c[i][j]=='P') sS|=(1<<icnt-1);
        }
    for (int i=1;i<=m;i++)
        for (int j=1;j<=n;j++)
            if (c[i][j]!='#') {
                if (i>1&&c[i-1][j]!='#') g[id[i][j]][0]=id[i-1][j];
                if (i<m&&c[i+1][j]!='#') g[id[i][j]][1]=id[i+1][j];
                if (j>1&&c[i][j-1]!='#') g[id[i][j]][2]=id[i][j-1];
                if (j<n&&c[i][j+1]!='#') g[id[i][j]][3]=id[i][j+1];
            }
    BFS();
}
View Code

 

转载于:https://www.cnblogs.com/mastervan/p/11117931.html

在C++中实现缩动态规划(DP)通常适用于态空间较小且可以用位表示的问题。DP的核心思想是将态用一个整数表示,其中每一位代表某种特定的含义,从而利用位运算进行态转移。 ### 缩动态规划的基本步骤 1. **态定义**:将问题的态用一个整数表示,每一位代表某种态。 2. **态转移**:通过位运算实现态的更新。 3. **初始化与边界条件**:设置初始态和边界条件。 4. **结果提取**:从态中提取最终结果。 ### 示例:访问所有节点的最短路径 以下是一个使用缩动态规划解决“访问所有节点的最短路径”问题的示例。该问题要求找到一条路径,使得从某个起点出发,经过所有节点至少一次的最短路径。 ```cpp #include <iostream> #include <vector> #include <queue> #include <cstring> #include <algorithm> using namespace std; const int MAXN = 12; // 最多节点数 int shortestPathAllKeys(vector<string>& grid) { int m = grid.size(); int n = grid[0].size(); int keys = 0; int start_x = -1, start_y = -1; // 找到起点和钥匙的数量 for (int i = 0; i < m; ++i) { for (int j = 0; j < n; ++j) { if (grid[i][j] == '@') { start_x = i; start_y = j; } else if (grid[i][j] >= 'a' && grid[i][j] <= 'f') { keys++; } } } // 态总数:位置 + 拥有的钥匙 int target = (1 << keys) - 1; int visited[MAXN][MAXN][1 << keys]; memset(visited, -1, sizeof(visited)); queue<tuple<int, int, int, int>> q; q.push({start_x, start_y, 0, 0}); visited[start_x][start_y][0] = 0; int dirs[4][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; while (!q.empty()) { auto [x, y, mask, steps] = q.front(); q.pop(); // 如果已经收集了所有钥匙 if (mask == target) { return steps; } for (auto& dir : dirs) { int nx = x + dir[0]; int ny = y + dir[1]; if (nx >= 0 && nx < m && ny >= 0 && ny < n) { char cell = grid[nx][ny]; // 如果是墙,跳过 if (cell == '#') continue; // 如果是锁,检查是否有对应的钥匙 if (cell >= 'A' && cell <= 'F') { int key_bit = 1 << (cell - 'A'); if ((mask & key_bit) == 0) continue; } // 如果是钥匙,更新mask int new_mask = mask; if (cell >= 'a' && cell <= 'f') { int key_bit = 1 << (cell - 'a'); new_mask |= key_bit; } // 如果该态未被访问过 if (visited[nx][ny][new_mask] == -1) { visited[nx][ny][new_mask] = steps + 1; q.push({nx, ny, new_mask, steps + 1}); } } } } return -1; // 无法收集所有钥匙 } ``` ### 代码解析 1. **态表示**:`mask` 表示当前已收集到的钥匙集合,每一位代表一种钥匙。 2. **广度优先搜索(BFS)**:使用队列进行BFS,每次从队列中取出一个态,并尝试向四个方向扩展。 3. **态转移**:通过位运算更新`mask`,表示收集到新的钥匙。 4. **终止条件**:当`mask`等于所有钥匙的集合时,表示已经收集了所有钥匙,返回当前的步数。 ### 缩动态规划的优势 - **空间效率**:通过位运算态,减少了存储空间的需求。 - **时间效率**:态转移通常通过位运算实现,计算效率高。 - **适用性**:特别适合处理组合态问题,例如路径规划、任务分配等[^3]。 ### 注意事项 - **态数量**:态数量通常为 $2^n$,其中 $n$ 是态的位数。因此,DP适用于 $n$ 较小的问题。 - **位运算熟练度**:需要熟练掌握位运算技巧,如按位与(`&`)、按位或(`|`)、左移(`<<`)等。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值