hdu3681 Prison Break (bfs + 二分 + 状态压缩)

题目: http://acm.hdu.edu.cn/showproblem.php?pid=3681

题意:一个机器人要越狱,输入一个字符矩阵,'F'代表出发点,机器人从这个点出发,电池的电量是满的;'S'是空地,机器人可以走;'G'表示充电站,经过充电站,机器人可以选择给自己的电池充满电,充完电之后,该充电站就等同于'S‘,下次经过不再充电,机器人也可以选择只经过,不充电,那么它还是‘G’ ; 'Y'表示一个开关,机器人必须关掉所有的开关,才能逃出监狱;'D'表示该位置有监测器,机器人不能走这个点,除了'D'点,其他点可以重复走。

问:机器人的电池的容量min至少是多大 才能关闭所有'Y'之后 逃出监狱?如果可以逃出来,输出min,逃不出来,输出-1;

围观大神的解题报告: hdu3681解题报告

题解:首先,我们要明确,题目就是要求我们必须经过F和所有的Y至少一次,这有点类似tsp,于是想到可以状态压缩,但是G怎么处理,这很头疼,因为经过'G',我们可以充电 也可以不充电。所以我们决定把'G'点也进行压缩,'F' ‘G’ ‘Y’ 我称为关键点,我们要对这三类点进行状态压缩,具体分为三步:

第一步,把所有的F,Y,G 视为关键点,记录下来, 用bfs算出这些关键点两两之间的最短距离dis[i][j],我们把'G' ‘Y’ ‘F’ 全部视为普通的'S'。也就是说,经过'G',我们不充电 ;经过'Y'点,我们也不管关闭开关的事情,那什么时候充电和关闭开关呢 ?等到我们自己在状态转移时,再进行 充电 / 关闭开关,这样就保证了'G'点我们只充一次电 ,  但是这样对于一个'Y',我们就可能重复走了两次,因为dis[i][j]可能经过了其他的'Y',但没有标记,我们还要去再关闭那个‘Y',这显然不是最优,不过没关系,状态转移的时候,我们都会进行比较,最后取到最优的情况。

第二步,二分,答案明显 在 0 ~ 矩阵的行*矩阵的列  之间,二分出一个mid,然后对mid检查,看是否符合条件

第三步,写一个检查函数 bool ok(int mid); 检查mid是否合法;函数体:

用 类似 tsp的转移方程,我们把'Y' 'G' 'F' 压缩成一个状态,如果某个开关‘Y’还没有明确关闭,则Y相应位置0;如果某个充电站还有点,该充电站G相应位置为0;因为题目指出'Y'和'G'的总数不会超过15,所以状态s在1到1<<16之内

状态 : dp[s][i] 表示 到达状态s,位于关键点i,剩余的最大电量;

边界 : dp[1<<start][start] = mid;

当((s & en) == en) && dp[en][i] >=0 即可返回 true, 其中 en表示经过F和Y一次的状态,可以在统计关键点的时候,得到en。

#include<stdio.h>
#include<memory.h>
#include<queue>
using namespace std;
int r,c;//字符矩阵的行和列
const int maxn = 16;
char map[maxn][maxn];//存放输入的字符矩阵
int sign[maxn][maxn];//标记数组,把关键点标记起来
struct Point
{
    int x,y;
    int val;    
    Point(){}
    Point(int xx,int yy){x = xx ; y =yy;val=0;}
}p[maxn+1];
int cnt = 0;//关键点的个数  编号 1~cnt
int en = 0;//终止状态,即 经过F和Y一次的状态
int start = 0;//出发点
int dis[maxn][maxn];//关键点两两之间的最短距离
int tmp[maxn][maxn];//临时数组,计算最短距离用到
int dir[4][2]
{
    {-1,0},{1,0},{0,-1},{0,1}
};
bool check(int x,int y)
{
    if(x<0||y<0) return false;
    if(x>=r||y>=c) return false;
    if(map[x][y]=='D') return false;
    return true;
}
void bfs(int i) //统计关键i到其他关键点 之间的最短距离
{
    memset(tmp,-1,sizeof(tmp)); // tmp[i][j] 表示点(i,j) 距离 关键点 p[i] 的最短距离
    tmp[p[i].x][p[i].y] = 0;
    
    //开始bfs
    queue<Point>q; //存放有更新的点
    q.push(p[i]);
    
    while(q.size())  
    {
        Point top = q.front();
        q.pop();
        for(int ii=0;ii<4;ii++)
        {
            int newX = top.x + dir[ii][0];
            int newY = top.y + dir[ii][1];
            bool flag = false; //该点是否被更新
            if(check(newX,newY))//合法
            {
                if(tmp[newX][newY]==-1) //还没求出来,直接赋值
                {
                    tmp[newX][newY] = tmp[top.x][top.y] + 1;
                    flag = true;
                }else if(tmp[newX][newY] > tmp[top.x][top.y] + 1){//更新
                    tmp[newX][newY] = tmp[top.x][top.y] + 1;
                    flag = true;
                }
            }
            if(flag)//有更新,将该点压入队列
            {
                Point pp(newX,newY);
                q.push(pp);
            }
        }
    }
    //统计i到其他关键点的最短距离
    for(int ii=0;ii<r;ii++)
    {
        for(int jj=0;jj<c;jj++)
        {
            if(sign[ii][jj]>=0)//是关键点
            {
                dis[i][sign[ii][jj]] = tmp[ii][jj];
            }
        }
    }
}
int dp[1<<maxn][maxn];
int max(int a,int b)
{
    return a>b?a:b;
}
bool ok(int all)
{
    memset(dp,-1,sizeof(dp));
    
    dp[1<<start][start] = all;//边界,出发点是满电量
    for(int s=1;s<(1<<cnt);s++) //到达s状态
    {
        for(int i=0;i<cnt;i++)//当前位于i
        {
            if(dp[s][i]<0 || !(s&(1<<i))) continue; //这个状态合法,且s包含i
            
            if((s&en)==en) return true; //如果已经满足了条件,直接返回true
            for(int j=0;j<cnt;j++)//转移到j
            {
                if(i==j || dis[i][j]==-1) continue;//如果i和j不相通,或者j==i
                if(dp[s][i] < dis[i][j]) continue;//如果剩余的能量不够我们转移到j
                if(s&(1<<j)) continue;//如果s已经包含了j
                
                int tmp = (s|(1<<j));//推出下一个状态
                
                int x = p[j].x;
                int y = p[j].y;
                if(map[x][y]=='G')//明确地指出 我要充电
                {
                    dp[tmp][j] = all;
                }else { //或者 明确地关闭某个 开关'Y'
                    dp[tmp][j] = max(dp[tmp][j],dp[s][i]-dis[i][j]);
                }
            }
        }
    }    
    return false;
}
int main()
{
    while(true)
    {
        scanf("%d%d",&r,&c);
        if(r==0 && c==0)break;
        
        memset(sign,-1,sizeof(sign));
        memset(dis,-1,sizeof(dis));
        cnt = en = 0;
        
        //1.记录所有的非'S'点,'G' 'Y' 'F'他们是关键点,我们要让机器人至少经过Y和F点 一次
        for(int i=0;i<r;i++)
        {
            scanf("%s",map[i]);
            for(int j=0;j<c;j++)
            {
                if(map[i][j]=='G')
                {
                    p[cnt].x = i;
                    p[cnt].y = j;
                    sign[i][j] = cnt++;//标记
                }else if(map[i][j]=='F')
                {
                    en = en + (1<<cnt);
                    p[cnt].x = i;
                    p[cnt].y = j;
                    start = cnt;
                    sign[i][j] = cnt++;//标记
                }else if(map[i][j]=='Y')
                {
                    en = en + (1<<cnt);
                    p[cnt].x = i;
                    p[cnt].y = j;
                    sign[i][j] = cnt++;//标记
                }
            }
        }
        
        //2.计算关键点 之间的最短距离
        for(int i=0;i<cnt;i++)
        {
            dis[i][i] = 0;
            bfs(i);
        }
            
        //3.二分枚举每一个解 然后用 状态压缩dp 判断该解是否可行
        int ll=0,rr=r*c;
        int rs = -1;
        while(ll<=rr)
        {
            int mid = (ll+rr)/2;
            if(ok(mid))
            {
                rs = mid;
                rr = mid - 1;
//                printf("ok....mid == %d\n",mid);    
            }else{
                ll = mid + 1;
//                printf("no..\n");
            }
        }
        printf("%d\n",rs);
    }    
    return 0;
}//187ms

...蒟蒻渣科看到这题掉下了眼泪

猜你喜欢

转载自blog.csdn.net/zark721/article/details/81145582