免责声明
不想打字了。。
题目
在某博物馆中摆放了非常重要的文物,为了节省人力,该博物馆专门购买了警卫机器人来看管这些文物。该博物馆的房间排列整齐,房间的大小相同。每个警卫机器人能够巡查的范围除本身所在房间外,还包括其起始安放的房间的上下左右四个房间。为了减少摆放的机器人的数量,请你设计一种最佳的摆放方案,使得摆放的机器人数量最少。
输入:
输入一行,有两个整数m,n,分别表示该博物馆每行的房间数和每列的房间数。博物馆总房间数即为m*n。
输出:
输出的第一行表示需要的机器人的数量,其后m行,每行有n个元素,每个元素的值为0或1,分别表示对应的房间是否摆放机器人。0表示不摆放,1表示需要摆放。
样例输入:
4 4
样例输出:
4
0 0 1 0
1 0 0 0
0 0 0 1
0 1 0 0
要求
1、写出采用分支限界法求解样例输入时的求解过程。
2、写出采用分支限界法算法分析过程,编写程序求解上述问题,并分析算法的时间复杂度。
参考文章
虽然是参考文章但是先不要看。
分析
首先要介绍一下快照这个词。比如你给你的电脑系统保存一个快照,那么在你把系统删了之后,拿快照过来又可以恢复原样。
下面要讲的代码就是这样的。
(1)我们对初始化的博物馆(二维数组)保存“快照”(后面都不打引号了,快照其实是对结构体的比喻)。
(2)遍历第一个点,在3种可能的位置放置机器人(不要问为什么是3种,俺也不知道T_T),它们会改变二维数组,把它们存为3张快照,放入优先队列进行排序,它会把监控房间最多的那个放到队列顶端。
(3)继续下一个点的遍历。pop队列,取出最优的那个快照继续重复(2)的操作,直到遍历完所有点。
所以,分支界限法的特点就是要用到广度优先遍历,广度优先遍历通常需要队列。每次广度遍历完,会取出最优解,在此基础上进行下一次广度遍历。这个代码里面是没有递归的!
注意上图有三个分支,这对于画解空间树和搜索空间树很重要。
现在开始讲代码。如何构造这个快照(结构体)呢?
struct Node{
// 机器人位置
int robot_position[30][30];
// 被监视的房间位置
int room_watched[30][30];
// (i,j)为当前遍历到的坐标
int i,j;
// 机器人数 被监视的房间个数
int robotNum,roomNum;
};
接下来对它进行初始化。
Node init(Node node)
{
// 初始化robot_position数组全为0
memset(node.robot_position,0,sizeof(node.robot_position));
// 初始化room_watched数组全为0
memset(node.room_watched,0,sizeof(node.room_watched));
// 当前点在i=1,j=1
node.i=1;node.j=1;
// 当前的机器人数;当前的监控房间数
node.robotNum=0;node.roomNum=0;
// 在博物馆的四边扩充1个单位的行列
for(int i=0;i<=n+1;i++)
node.room_watched[i][0]=node.room_watched[i][m+1]=1;
for(int i=0;i<=m+1;i++)
node.room_watched[0][i]=node.room_watched[n+1][i]=1;
return node;
}
我把这个结构体大致画了一下,初始化的时候,博物馆的二维数组外层一圈灰的是特意增加的,因为放置机器人的时候,会把机器人上下左右的点都设置成被监控的状态,为了边界不进行越界判断,就添加了一圈外围的行列。 所以遍历的点从(1,1)开始,到(m,n)结束(图里的n写错了,是m)。
有了初始的结构体,我们把它放到队列中,从while循环开始遍历了。
// m*n的房间
int m,n;
//最优结果 ans为机器人个数 ans_arr为每个机器人的位置
int ans_arr[30][30],ans;
int main()
{
// 输入行列
scanf("%d%d",&m,&n);
// 机器人最多的数量
ans=m*n/3+2;
// 初始化
Node node;
node=init(node);
// 快照放入队列
q.push(node);
// 如果队列不空
while(!q.empty())
{
// 返回队列第一个
Node p=q.top();
// 队列把第一个弹走
q.pop();
// 如果房间没有全被监控,则分别在当前遍历点的下方、本身、右方放置机器人
// 注意这三种情况是互不干扰的,它们会生成三种快照,判断出用机器人最少的一个
if(p.roomNum < m*n)
{
// 1、在下方放置
// 判断条件就是下方有位置可放,不能在最后一行)
if(p.i<m)
setRobot(p,p.i+1,p.j);
// 2、在本身放置。
// 第一个判断条件是在它已经没有下方和右方的点的情况下,只能选择自身
// 第二个判断条件是它的右边没有被监控
if((p.i==m && p.j==n) || p.room_watched[p.i][p.j+1]==0)
setRobot(p,p.i,p.j);
// 3、在右方放置
// 第一个判断条件是遍历点右边是没被监控的点
// 第二个判断条件是遍历点右边的右边是没有监控的点
if(p.j<n && (p.room_watched[p.i][p.j+1]==0 || p.room_watched[p.i][p.j+2]==0))
setRobot(p,p.i,p.j+1);
}
// 如果房间全被监控了
else(p.roomNum>=m*n)
{
// 如果已安置的机器人数是目前最少的,更新结果ans
if(p.robotNum<ans)
{
ans=p.robotNum;
// 把这种安置方法保存到结果数组ans_arr里面
memcpy(ans_arr, p.robot_position, sizeof(p.robot_position));
}
}
}
// 打印结果和结果数组
printf("%d\n",ans);
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
printf("%d ",ans_arr[i][j]);
printf("\n");
}
}
为什么那三个判断条件是那样的,俺也不太懂T_T。
我们调用了setRobot函数,传入的参数是当前快照,要放置的坐标。放置完之后,这新的三个快照被放入优先队列,选择最优那个到队列顶端。
// 自身和上下左右5个方位
int position[5][2]={
{0, 0}, //自身
{0, 1}, //右方
{0,-1}, //左方
{1, 0}, //下方
{-1,0} //上方
};
// 重写优先队列的比较函数
struct cmp
{
bool operator() (Node a, Node b)
{
// 比较哪个快照的被监控房间多(小顶堆)
return a.roomNum > b.roomNum;
}
};
//C++的优先队列,存入快照时,cmp函数会自动执行
priority_queue<Node, vector<Node>, cmp> q;
void setRobot(Node p,int x,int y)
{
// 以下几行都是在复制一份快照p
Node node;
node=init(node);
node.i=p.i;
node.j=p.j;
node.roomNum=p.roomNum;
memcpy(node.robot_position, p.robot_position, sizeof(p.robot_position));
memcpy(node.room_watched, p.room_watched, sizeof(p.room_watched));
// 在(x,y)点新增机器人,机器人数量要+1
node.robot_position[x][y]=1;
node.robotNum=p.robotNum+1;
// 对这个新增机器人的上下左右和自身标记被监控
for(int d=0;d<5;d++)
{
// pos_x,pos_y表示机器人上下左右位置,我们标记这些位置的房间被监控
int pos_x=x+position[d][0];
int pos_y=y+position[d][1];
node.room_watched[pos_x][pos_y]++;
// 标记一个房间,roomNum就加一。
// 一定要等于1,因为有的房间会被重复监控,那就是2了
if(node.room_watched[pos_x][pos_y]==1)
{
node.roomNum++;
}
}
// 如果行数不越界 且 当前点被监控了
while(node.i<=m && node.room_watched[node.i][node.j])
{
// 当前点的列右移一个单位
node.j++;
// 如果右移之后越界了,就换行
if(node.j>n)
node.i++,node.j=1;
}
// 把当前快照存到优先队列里,会调用cmp排序,保证最顶端的是最优的快照
q.push(node);
return;
}
完整代码
#include <stdio.h>
#include <queue>
// m*n的房间
int m,n;
// 自身和上下左右5个方位
int position[5][2]={
{0, 0},
{0, 1},
{0,-1},
{1, 0},
{-1,0}
};
//最优结果 ans为机器人个数 ans_arr为每个机器人的位置
int ans_arr[30][30],ans;
struct Node{
// 机器人位置
int robot_position[30][30];
// 被监视的房间位置
int room_watched[30][30];
// (i,j)为当前遍历到的坐标
int i,j;
// 机器人数 被监视的房间个数
int robotNum,roomNum;
};
// 优先队列的比较函数重写
struct cmp
{
bool operator() (Node a, Node b)
{
// 比较哪个快照被监视房间多(小顶堆)
return a.roomNum > b.roomNum;
}
};
//优先队列,cmp函数会自动执行
priority_queue<Node, vector<Node>, cmp> q;
Node init(Node node)
{
// 初始化robot_position数组全为0
memset(node.robot_position,0,sizeof(node.robot_position));
// 初始化room_watched数组全为0
memset(node.room_watched,0,sizeof(node.room_watched));
// 当前点在i=1,j=1
node.i=1;node.j=1;
// 当前的机器人数;当前的监控房间数
node.robotNum=0;node.roomNum=0;
// 在博物馆的上下扩充两行
for(int i=0;i<=m+1;i++)
node.room_watched[i][0]=node.room_watched[i][m+1]=1;
// 在博物馆的左右扩充两列
for(int i=0;i<=n+1;i++)
node.room_watched[0][i]=node.room_watched[n+1][i]=1;
return node;
}
void setRobot(Node p,int x,int y)
{
// 以下几行都是在复制一份快照p
Node node;
node=init(node);
node.i=p.i;
node.j=p.j;
node.roomNum=p.roomNum;
memcpy(node.robot_position, p.robot_position, sizeof(p.robot_position));
memcpy(node.room_watched, p.room_watched, sizeof(p.room_watched));
// 在(x,y)点新增机器人,机器人数量要+1
node.robot_position[x][y]=1;
node.robotNum=p.robotNum+1;
// 对这个新增机器人的上下左右和自身标记被监控
for(int d=0;d<5;d++)
{
// pos_x,pos_y表示机器人上下左右位置,我们标记这些位置的房间被监控
int pos_x=x+position[d][0];
int pos_y=y+position[d][1];
node.room_watched[pos_x][pos_y]++;
// 标记一个房间,roomNum就加一。
// 一定要等于1,因为有的房间会被重复监控,那就是2了
if(node.room_watched[pos_x][pos_y]==1)
{
node.roomNum++;
}
}
// 如果行数不越界 且 当前点被监控了
while(node.i<=m && node.room_watched[node.i][node.j])
{
// 当前点的列右移一个单位
node.j++;
// 如果右移之后越界了,就换行
if(node.j>n)
node.i++,node.j=1;
}
// 把当前快照存到优先队列里,会调用cmp排序,保证最顶端的是最优的快照
q.push(node);
return;
}
int main()
{
// 输入行列
scanf("%d%d",&m,&n);
// 机器人最多的数量
ans=m*n/3+2;
// 初始化
Node node;
node=init(node);
// 快照放入队列
q.push(node);
// 如果队列不空
while(!q.empty())
{
// 返回队列第一个
Node p=q.top();
// 队列把第一个弹走
q.pop();
// 如果房间没有全被监控,则分别在当前遍历点的下方、本身、右方放置机器人
// 注意这三种情况是互不干扰的,它们会生成三种快照,判断出用机器人最少的一个
if(p.roomNum < m*n)
{
// 1、在下方放置
// 判断条件就是下方有位置可放,不能在最后一行)
if(p.i<m)
setRobot(p,p.i+1,p.j);
// 2、在本身放置。
// 第一个判断条件是在它已经没有下方和右方的点的情况下,只能选择自身
// 第二个判断条件是它的右边没有被监控
if((p.i==m && p.j==n) || p.room_watched[p.i][p.j+1]==0)
setRobot(p,p.i,p.j);
// 3、在右方放置
// 第一个判断条件是遍历点右边是没被监控的点
// 第二个判断条件是遍历点右边的右边是没有监控的点
if(p.j<n && (p.room_watched[p.i][p.j+1]==0 || p.room_watched[p.i][p.j+2]==0))
setRobot(p,p.i,p.j+1);
}
// 如果房间全被监控了
else(p.roomNum>=m*n)
{
// 如果已安置的机器人数是目前最少的,更新结果ans
if(p.robotNum<ans)
{
ans=p.robotNum;
// 把这种安置方法保存到结果数组ans_arr里面
memcpy(ans_arr, p.robot_position, sizeof(p.robot_position));
}
}
}
// 打印结果和结果数组
printf("%d\n",ans);
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
printf("%d ",ans_arr[i][j]);
printf("\n");
}
}
解空间树
我不太会,应该是个三叉数,把每种情况都画出来。
搜索空间树
它应该是在解空间的基础上剪枝。