状态压缩DP + 优先队列BFS
题意:给一个矩阵图,图中只有一个*表示起点,#表示不能到达的点,从A开始有k个大写字母(一定是连续的前k个,但是每个字母的个数不一定),一种大写字母表示一种矿石,一个矿石有两个值ai,bi,表示收集这个矿石需要ai电量,另外,在一个矿石点,可以选择收集或者不收集直接走掉,收集后每走一步,需要多加bi的电量,一开始没有任何矿石,每走一步需要1电量,另外,一开始给你P电量。走的方向是上下左右四个方向。你的任务是在电量用完之前,收集到每种矿石(每种矿石只需要1个),然后回到起点,但是注意一点,在没有收集完之前,不能回到起点,因为一旦回到起点就会瞬间传送回基地。如果有的矿石收集不到,或者没足够的电量回到起点,输出Impossible,否则输出最小花费
一看这题,没什么特别的方法,那就只好搜索了,因为要求最小花费,所以用优先队列可以达到这个效果,另外搜索过程需要剪枝。
队列中元素的定义
1.当前位置
2.当前收集了哪些矿石
3.当前花费
对于当前收集了哪些矿石,因为矿石种类最大是10,所以可以用状态压缩,用一个长度为k的01串来表示哪些矿石被搜集了哪些没有,然后压缩为一个十进制数
状态的转移就是枚举四个方向,然后计算新的位置和花费,以及新的收集情况
关于收集和不收集矿石
收集的话比较容易,就是当前点有矿石,并且还没有收集到这个矿石,就可以考虑收集(但不是一定要收集)
不收集矿石有几种情况
当前点是个自由地.,不存在矿石,当然不能收集;当前地有矿石,但是已经有这种矿石了,那肯定不收集;当前点有矿石,而且还没收集到这个矿石,也是可以选择不收集的,因为可能这种矿石不止一个,可以去别的地方再收集
但是这些情况也没什么特别要注意的地方,只是说,要有这个意识,要考虑到这些情况
最重要的是剪枝
BFS搜索,要达到剪枝效果,最好就是入队时设置较严格的条件,防止元素重复入队。
剪枝主要靠一个数组vis[x][y][s],表示点(x,y)的收集情况为s,这个状态是否被搜索过,如果搜索给,不用再入队,每个这样的状态只会入队一次
为什么这样剪枝是对的。因为我们用的是优先队列,是按电量花费最小这种悠闲级出队的,那么可以知道,对于一个状态[x,y,s],如果第一次被搜索到,那么它的电量花费是最少的,以后再搜索到这个状态,电量花费不会比第一次的更少,所以没必要进来。
另外仿照无限搜索下去的关键是,对于一个新的状态,如果要达到这个状态需要的电量超过了总电量,那么这个状态不应该入队
再者就是,在得到一个合法的新状态后,先看看它是不是我们的目标状态,是的话直接返回答案
最后一点,就是注意题意,不能起点不能随便走的,一旦回到起点就会传送回基地相当于一切都终止了,所以如果一个新状态的位置是起点,但是还没有搜集到全部矿石,那么这个状态是要丢弃的,因为这个状态如果入队了,说明允许了这个操作,允许在没有收集完矿石的条件下回到起点,这和题意是相矛盾的
#include <iostream> #include <cstdio> #include <cstring> #include <queue> #include <vector> using namespace std; #define N 25 #define M 15 #define NS 2510 const int dx[4] = {-1,1,0,0}; const int dy[4] = {0,0,-1,1}; char map[N][N]; int row,col,kind,power,sx,sy,tot; int A[M],B[M],per[NS]; bool vis[N][N][NS]; struct node //bfs搜索时的状态 { int x,y; //当前所在的坐标 int s; //携带的物品的状态,压缩回来的十进制数 int cost; //花费了多少电量 void func(int xx ,int yy ,int ss ,int cc) { x=xx; y=yy; s=ss; cost=cc; } bool operator<(const struct node &a) const{ return cost > a.cost; } }; priority_queue<struct node>que; void input() { cin >> row >> col >> kind >> power; for(int r=0; r<row; r++) cin >> map[r]; for(int i=0; i<kind; i++) cin >> A[i] >> B[i]; for(int r=0; r<row; r++) for(int c=0; c<col; c++) if(map[r][c] == '*') { sx=r; sy=c; break; } } void cal_per() {//计算在每种状态下,走一步需要的花费,先预先计算,不必一边搜索一边计算 // memset(per,0,sizeof(per)); tot = (1<<kind); for(int s=0; s<tot; s++) { per[s] = 1; for(int k=0; k<kind; k++) if( s & (1<<k) ) per[s] += B[k]; } } int solve() { struct node a,b; memset(vis,0,sizeof(vis)); while(!que.empty()) que.pop(); a.func(sx,sy,0,0); que.push(a); vis[sx][sy][0] = true; //剪枝:用vis数组标记,另外,电量>power的不能入队 //得到一个新状态,先判断它是否是目标状态,是的话直接返回答案,不用入队 while(!que.empty()) { a = que.top(); que.pop(); if(a.cost + per[a.s] > power) continue; //当前电量,无论怎么走都会超过power,那么直接丢弃 for(int k=0; k<4; k++) //四个方向 { int xx = a.x + dx[k]; int yy = a.y + dy[k]; int ss = a.s; int cc = a.cost + per[a.s]; if(xx<0 || xx>=row || yy<0 || yy>=col || map[xx][yy]=='#') continue; //不是合法的位置 if(xx==sx && yy==sy) if(ss == tot-1) return cc;//目标状态 else continue; //回到起点又没有收集完矿石,肯定是多余的(走回头路),直接抛弃 if(!vis[xx][yy][ss]) //这个状态没搜索过 {//在这个地方不采集矿石,而不采集是有很多可能的 b.func(xx,yy,ss,cc); que.push(b); vis[xx][yy][ss] = true; } if(map[xx][yy]>='A' && map[xx][yy]<='Z') //有矿石 { int index = map[xx][yy] - 'A'; if( !(ss & (1<<index)) ) //没有这个矿石,考虑采集 { ss |= (1<<index); //采集后的状态 cc += A[index]; //采集的花费 if(cc+A[index] > power) continue; //超过电量,放弃 if(xx==sx && yy==sy && ss==tot-1) return cc; //目标状态 if(!vis[xx][yy][ss]) { b.func(xx,yy,ss,cc); que.push(b); vis[xx][yy][ss] = true; } } } } } return -1; } int main() { int cas; cin >> cas; while(cas--) { input(); cal_per(); int res = solve(); if(res == -1) cout << "Impossible" << endl; else cout << res << endl; } return 0; }