广度优先搜索
- 树上的广度优先搜索实际上就是层次遍历。
分支界限法
- 分支界限法通常以广度优先或以最小耗费(最大效益)优先的方式搜索问题的解空间树。
- 首先将根节点加入活节点表中,接着从活节点表中取出根结点,使其称为当前扩展节点,一次性生成其所有孩子节点,判断对孩子节点是舍弃还是保留,舍弃那些得不到可行解或最优解的节点。
- 活节点表的实现通常有两种形式:一种是普通的队列,即先进先出队列;另一种是优先级队列,按照某种优先级决定哪个节点为当前扩展节点。
队列式广度优先搜索
- 有一个背包和4个物品,每个物品的重量分别为2、5、4、2,价值分别为:6、3、5、4,背包的容量
W = 10
。求在不超过背包容量的前提下,把哪些物品放入背包才能获得最大价值。
算法设计
- 定义节点结构体
struct Node{ //定义节点,记录当前节点的解信息
int cp,rp; //cp背包的物品总价值,rp剩余物品的总价值
int rw; //剩余容量
int id; //物品号
bool x[N]; //解向量
Node() {memset(x,0,sizfof(x));} //将解向量初始化为0
Node(int _cp, int _rp, int _rw, int _id){
cp = _cp;
rp = _rp;
rw = _rw;
id = _id;
}
};
- 定义物品结构体
struct Goods{ //物品
int wight; //重量
int value; //价值
}goods[N]
- 搜索解空间。首先创建一个普通队列(先进先出)然后将根节点加入队列中,如果队列不空,则取出队头元素livenode,得到当前处理的物品序号,如果当前处理的物品序号大于n,则说明搜到最后一个物品了,不需要往下搜索。如果当前的背包没有剩余容量(已经装满)了,则不再扩展。如果当前放入背包的物品价值大于或等于最优值(livenode.cp ≥ \geq ≥ bestp),则更新最优解和最优值。判断是否满足约束条件,满足则生成左孩子,判断是否更新最优值,左孩子入队,不满足约束条件则舍弃左孩子;判断是否满足限界条件,满足则生成右孩子,右孩子入队,不满足限界条件则舍弃右孩子。
算法实现
#include<iostream>
#include<cstring>
#include<queue>
using namespace std;
const int N=10;
bool bestx[N];
//定义结点。每个节点来记录当前的解。
struct Node{
int cp,rp; //cp为当前装入背包的物品总价值,rp为剩余物品的总价值
int rw; //剩余容量
int id; //物品号
bool x[N];//解向量
Node(){}
Node(int _cp,int _rp,int _rw,int _id){
cp=_cp;
rp=_rp;
rw=_rw;
id=_id;
memset(x,0,sizeof(x));//解向量初始化为0
}
};
struct Goods{//物品
int weight;//重量
int value;//价值
}goods[N];
int bestp,W,n,sumw,sumv;
/*
bestp用来记录最优解
W为背包最大容量。
n为物品的个数。
sumw为所有物品的总重量。
sumv为所有物品的总价值。
*/
int bfs();//队列式分支限界法
int main(){
cin>>n>>W;//输入物品的个数和背包的容量
bestp=0; //bestv用来记录最优解
sumw=0; //sumw为所有物品的总重量。
sumv=0; //sumv为所有物品的总价值
for(int i=1;i<=n;i++){//输入每个物品的重量和价值,用空格分开
cin>>goods[i].weight>>goods[i].value;//输入第i件物品的重量和价值。
sumw+=goods[i].weight;
sumv+=goods[i].value;
}
if(sumw<=W){
bestp=sumv;
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
return 0;
}
bfs();
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"放入背包的物品序号为: ";
for(int i=1;i<=n;i++){// 输出最优解
if(bestx[i])
cout<<i<<" ";
}
return 0;
}
int bfs(){//队列式分支限界法
int t,tcp,trp,trw;//当前处理的物品序号t,装入背包物品价值tcp,剩余容量trw
queue<Node> q; //创建一个普通队列(先进先出)
q.push(Node(0,sumv,W,1)); //压入一个初始节点
while(!q.empty()){
Node livenode,lchild,rchild;//定义三个结点型变量
livenode=q.front();//取出队头元素作为当前扩展结点livenode
q.pop(); //队头元素出队
t=livenode.id;//当前处理的物品序号
// 搜到最后一个物品的时候不需要往下搜索
// 如果当前的背包没有剩余容量(已经装满)了,不再扩展
if(t>n||livenode.rw==0){
if(livenode.cp>=bestp){//更新最优解和最优值
for(int i=1;i<=n;i++)
bestx[i]=livenode.x[i];
bestp=livenode.cp;
}
continue;
}
if(livenode.cp+livenode.rp<bestp)//判断当前结点是否满足限界条件,如果不满足不再扩展
continue;
tcp=livenode.cp; //当前背包中的价值
trp=livenode.rp-goods[t].value; //不管当前物品装入与否,剩余价值都会减少。
trw=livenode.rw; //背包剩余容量
if(trw>=goods[t].weight){ //扩展左孩子,满足约束条件,可以放入背包
lchild.rw=trw-goods[t].weight;
lchild.cp=tcp+goods[t].value;
lchild=Node(lchild.cp,trp,lchild.rw,t+1);
for(int i=1;i<t;i++)//复制以前的解向量
lchild.x[i]=livenode.x[i];
lchild.x[t]=true;
if(lchild.cp>bestp)//比最优值大才更新
bestp=lchild.cp;
q.push(lchild);//左孩子入队
}
if(tcp+trp>=bestp){//扩展右孩子,满足限界条件,不放入背包
rchild=Node(tcp,trp,trw,t+1);
for(int i=1;i<t;i++)//复制以前的解向量
rchild.x[i]=livenode.x[i];
rchild.x[t]=false;
q.push(rchild);//右孩子入队
}
}
return bestp;//返回最优值
}
输入:
4 10
2 6 5 3 4 5 2 4
输出:
放入背包的物品最大价值为: 15
放入背包的物品序号为: 1 3 4
优先队列式广度优先搜索
- 优先队列优化以当前节点的上界为优先值,把普通队列改成优先队列,这样就得到了优先队列分支限界法。
算法设计
- 优先级的定义为活节点代表的部分解所描述的已装入物品的价值上界,上界越大,优先级越高。活节点的价值上界up = 活节点的cp+剩余物品装满背包剩余容量的最大价值rp’。
- 约束条件:
cw+w[i]
≤ \leq ≤W
- 限界条件:
up = cp + rp'
≥ \geq ≥bestp
算法实现
- 定义节点和物品结构体
struct Node{ //定义节点,记录当前节点的解信息
int cp; //cp背包的物品总价值,rp剩余物品的总价值
double up; //价值上界
int rw; //剩余容量
int id; //物品号
bool x[N]; //解向量
Node() {}
Node(int _cp, double _up, int _rw, int _id){
cp = _cp;
up = _up;
rw = _rw;
id = _id;
memset(x,0,sizfof(x));
}
};
struct Goods{//物品
int weight;//重量
int value;//价值
}goods[N];
- 定义辅助结构体和排序优先级(从大到小排序)
struct Object{ //辅助物品结构体,用于按单位重量价值(价值/重量比)排序
int id; //序号
double d; //单位重量价值
}S[N];
bool cmp(Object a1,Object a2){ //排序优先级,按照物品的单位重量价值由大到小排序
return a1.d>a2.d;
}
- 定义队列的优先级
bool operator <(const Node &a, const Node &b){ //队列优先级,up越大越优先
return a.up < b.up;
}
- 计算节点的上界
double Bound(Node tnode){
double maxvalue = tnode.cp; //已装入背包的物品价值
int t = tnode.id; //排序后序号
double left = tnode.rw; //剩余容量
while(t <= n && w[t] <= left){
maxvalue+=v[t];
left-=w[t++];
}
if(t <= n){
maxvalue+=double(v[t])/w[t]*left;
}
return maxvalue;
}
- 优先队列分支限界法。
算法实现
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
using namespace std;
const int N=10;
bool bestx[N]; //记录最优解
int w[N],v[N];//辅助数组,用于存储排序后的重量和价值
struct Node{//定义结点,记录当前结点的解信息
int cp; //已装入背包的物品价值
double up; //价值上界
int rw; //背包剩余容量
int id; //物品号
bool x[N];
Node() {}
Node(int _cp,double _up,int _rw,int _id){
cp=_cp;
up=_up;
rw=_rw;
id=_id;
memset(x, 0, sizeof(x));
}
};
struct Goods{ //物品结构体
int weight;//重量
int value;//价值
}goods[N];
struct Object{//辅助物品结构体,用于按单位重量价值(价值/重量比)排序
int id; //序号
double d;//单位重量价值
}S[N];
bool cmp(Object a1,Object a2){//排序优先级,按照物品单位重量价值由大到小排序
return a1.d>a2.d;
}
bool operator <(const Node &a, const Node &b){//队列优先级。up越大越优先
return a.up<b.up;
}
int bestp,W,n,sumw,sumv;
/*
bestv 用来记录最优解。
W为背包的最大容量。
n为物品的个数。
sumw为所有物品的总重量。
sumv为所有物品的总价值。
*/
double Bound(Node tnode){//计算节点的上界
double maxvalue=tnode.cp;//已装入背包物品价值
int t=tnode.id;//排序后序号
double left=tnode.rw;//剩余容量
while(t<=n&&w[t]<=left){
maxvalue+=v[t];
left-=w[t++];
}
if(t<=n)
maxvalue+=double(v[t])/w[t]*left;
return maxvalue;
}
int priorbfs(){//优先队列式分支限界法
int t,tcp,trw;//当前处理的物品序号t,当前装入背包物品价值tcp,当前剩余容量trw
double tup; //当前价值上界tup
priority_queue<Node> q; //创建一个优先队列
q.push(Node(0,sumv,W,1));//初始化,根结点加入优先队列
while(!q.empty()){
Node livenode, lchild, rchild;//定义三个结点型变量
livenode=q.top();//取出队头元素作为当前扩展结点livenode
q.pop(); //队头元素出队
t=livenode.id;//当前处理的物品序号
// 搜到最后一个物品的时候不需要往下搜索。
// 如果当前的背包没有剩余容量(已经装满)了,不再扩展。
if(t>n||livenode.rw==0){
if(livenode.cp>=bestp){//更新最优解和最优值
for(int i=1;i<=n;i++)
bestx[i]=livenode.x[i];
bestp=livenode.cp;
}
continue;
}
if(livenode.up<bestp)//如果不满足不再扩展
continue;
tcp=livenode.cp; //当前背包中的价值
trw=livenode.rw; //背包剩余容量
if(trw>=w[t]){ //扩展左孩子,满足约束条件,可以放入背包
lchild.cp=tcp+v[t];
lchild.rw=trw-w[t];
lchild.id=t+1;
tup=Bound(lchild); //计算左孩子上界
lchild=Node(lchild.cp,tup,lchild.rw,lchild.id);
for(int i=1;i<=n;i++)//复制以前的解向量
lchild.x[i]=livenode.x[i];
lchild.x[t]=true;
if(lchild.cp>bestp)//比最优值大才更新
bestp=lchild.cp;
q.push(lchild);//左孩子入队
}
rchild.cp=tcp;
rchild.rw=trw;
rchild.id=t+1;
tup=Bound(rchild);//计算右孩子上界
if(tup>=bestp){//扩展右孩子,满足限界条件,不放入
rchild=Node(tcp,tup,trw,t+1);
for(int i=1;i<=n;i++)//复制以前的解向量
rchild.x[i]=livenode.x[i];
rchild.x[t]=false;
q.push(rchild);//右孩子入队
}
}
return bestp;//返回最优值。
}
int main(){
bestp=0; //bestv用来记录最优解
sumw=0; //sumw为所有物品的总重量。
sumv=0; //sumv为所有物品的总价值
cin>>n>>W;
for(int i=1;i<=n;i++){
cin>>goods[i].weight>>goods[i].value;//输入第i件物品的重量和价值。
sumw+=goods[i].weight;
sumv+=goods[i].value;
S[i-1].id=i;
S[i-1].d=1.0*goods[i].value/goods[i].weight;
}
if(sumw<=W){
bestp=sumv;
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
return 0;
}
sort(S,S+n,cmp);//按价值重量比非递增排序
for(int i=1;i<=n;i++){//把排序后的数据传递给辅助数组
w[i]=goods[S[i-1].id].weight;
v[i]=goods[S[i-1].id].value;
}
priorbfs();//优先队列分支限界法
cout<<"放入背包的物品最大价值为: "<<bestp<<endl;
cout<<"放入背包的物品序号为: ";
for(int i=1;i<=n;i++){ //输出最优解
if(bestx[i])
cout<<S[i-1].id<<" ";//输出原物品序号(排序前的)
}
return 0;
}
输入:
4 10
2 6 5 3 4 5 2 4
输出:
放入背包的物品最大价值为: 15
放入背包的物品序号为: 1 3 4
训练1:迷宫问题
题目描述
用一个二维数组表示一个迷宫,其中1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,编写程序,找出从左上角到右下角的最短路线。
输入:一个 5 × 5 5 \times 5 5×5的二维数组,表示一个迷宫。数据保证有唯一解。
输出:从左上角到右下角的最短路径,格式如以下输出样例所示。
算法设计
- 定义一个队列,将起点(0,0)入队,标记已走过。
- 如果队列不为空,则队头出队。
- 如果队头正好是目标(4,4),则退出。
- 沿着4个方向搜索,如果该节点未出边界,未走过且不说墙壁,则标记走过并入队,用前驱数组记录该节点。
- 转向步骤2
- 根据前驱数组输出从起点到终点的最短路径。
算法实现
#include<cstdio>
#include<queue>
using namespace std;
int mp[5][5],vis[5][5];
int dir[4][2]={{1,0},{-1,0},{0,1},{0,-1}};
struct node{
int x,y;
};
node pre[10][10];
void bfs(); //广度搜索
void print(node cur); //打印结果
int main(){
for(int i=0;i<5;i++)
for(int j=0;j<5;j++)
scanf("%d",&mp[i][j]);
bfs();
node ed;
ed.x=ed.y=4;
print(ed);
return 0;
}
void bfs(){
queue<node> que; //创建node格式的队列
node st;
st.x=st.y=0; //从左上角出发
que.push(st);
vis[0][0]=1; //标记起点已经被走过
while(!que.empty()){ //如果队列不为空
node now=que.front(); //取队头
que.pop(); //出队
if(now.x==4&&now.y==4) //如果到达终点,结束
return;
for(int i=0;i<4;i++){ //向4个方向扩展
node next; //新建node型结构
next.x=now.x+dir[i][0];
next.y=now.y+dir[i][1];
if(next.x>=0&&next.x<5&&next.y>=0&&next.y<5&&!mp[next.x][next.y]&&!vis[next.x][next.y]){ //边界检查,墙体检查,路径检查
vis[next.x][next.y]=1;
que.push(next); //入队
pre[next.x][next.y]=now; //
}
}
}
}
void print(node cur){
if(cur.x==0&&cur.y==0){
printf("(0, 0)\n");
return;
}
print(pre[cur.x][cur.y]);//逆序输出(递归)
printf("(%d, %d)\n",cur.x,cur.y);
}
输入:
0 1 0 0 0
0 1 0 1 0
0 0 0 0 0
0 1 1 1 0
0 0 0 1 0
输出:
(0, 0)
(1, 0)
(2, 0)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 4)
(4, 4)
训练2:加满邮箱
题目描述
城市之间的油价是不同的,编写程序,寻找最便宜的城市间旅行方式。在旅行途中可以加满油箱。假设汽车每单位距离使用以单位燃料,从一个空油箱开始。
输入:输入的第1行包含 n n n( 1 ≤ n ≤ 1000 1 \leq n \leq 1000 1≤n≤1000)和m( 0 ≤ m ≤ 10000 0 \leq m \leq 10000 0≤m≤10000),表示城市和道路的数量。下一行包含n个整数 p i p_{i} pi( 1 ≤ p i ≤ 100 1 \leq p_{i} \leq 100 1≤pi≤100),其中 p i p_{i} pi表示第 i i i个城市的燃油价格。接下来的 m m m行,每行都包含3个整数 u u u、 v v v( 0 ≤ u 0 \leq u 0≤u)和 d d d,表示在 u u u和 v v v之间有一条路,长度为 d d d。接下来一行是查询数 q q q( 1 ≤ q ≤ 100 1 \leq q \leq 100 1≤q≤100)。再接下来的 q q q行,每行都包含3个整数 c c c( 1 ≤ c ≤ 100 1 \leq c \leq 100 1≤c≤100)、 s s s和 e e e,其中 c c c是车辆的油箱容量, s s s是起点城市, e e e是终点城市。
输出:对于每个查询,都输出给定容量的汽车从 s s s到 e e e的最便宜旅程的价格,如果无法从 s s s到 e e e,则输出“impossible”
算法设计
- 定义一个优先队列,将起点及当前油量、花费作为一个节点 ( s t , 0 , 0 ) (st,0,0) (st,0,0)入队。
- 如果队列不空,则队头
(
u
,
v
o
l
,
c
o
s
t
)
(u,vol,cost)
(u,vol,cost)出队,并标记该节点油量已扩展,
vis[u][vol] = 1
。 - 如果 u u u正好是目标 e d ed ed,则返回花费 c o s t cost cost。
- 如果当前油量小于油箱容量,且
u
u
u的油量
vol+1
未扩展,则将该节点(u,vol+1,cost+price[u])
入队。 - 检查
u
u
u所有邻接点
v
v
v,如果当前油量大于或等于边权
w
w
w,且
v
v
v节点的油量
vol-w
未扩展,则将该节点(v,vol-w,cost)
入队
算法实现
#include<cstdio>//加dp[][]优化
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
const int maxn=1005;
const int maxm=20005;
struct edge{
int v,w,next;
}edge[maxm];
struct node{
int u,vol,cost;
node(int u_,int vol_,int cost_){u=u_,vol=vol_,cost=cost_;}
bool operator < (const node &a) const{
return cost>a.cost;
}
};
int head[maxn];
bool vis[maxn][105];
int dp[maxn][105];//dp[i][j]表示在顶点i,当前油量为j时的最小花费。
int n,m,V,st,ed,cnt,price[maxn];
void add(int u,int v,int w){
edge[cnt].v=v;
edge[cnt].w=w;
edge[cnt].next=head[u];
head[u]=cnt++;
}
int bfs(){
memset(vis,0,sizeof(vis));
memset(dp,0x3f,sizeof(dp));
priority_queue<node>Q;
Q.push(node(st,0,0));
dp[st][0]=0;
while(!Q.empty()){
node cur=Q.top();
Q.pop();
int u=cur.u,vol=cur.vol,cost=cur.cost;
vis[u][vol]=1;
if(u==ed) return cost;
if(vol<V&&!vis[u][vol+1]&&dp[u][vol]+price[u]<dp[u][vol+1]){
dp[u][vol+1]=dp[u][vol]+price[u];
Q.push(node(u,vol+1,cost+price[u]));
}
for(int i=head[u];~i;i=edge[i].next){
int v=edge[i].v,w=edge[i].w;
if(vol>=w&&!vis[v][vol-w]&&cost<dp[v][vol-w]){
dp[v][vol-w]=cost;
Q.push(node(v,vol-w,cost));
}
}
}
return -1;
}
int main(){
int u,v,w;
while(~scanf("%d%d",&n,&m)){
for(int i=0;i<n;i++) scanf("%d",&price[i]);
cnt=0;
memset(head,-1,sizeof(head));
while(m--){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w),add(v,u,w);
}
int q;
scanf("%d",&q);
while(q--){
scanf("%d%d%d",&V,&st,&ed);
int ans=bfs();
if(ans==-1) puts("impossible");
else printf("%d\n", ans);
}
}
return 0;
}
输入:
5 5
10 10 20 12 13
0 1 9
0 2 8
1 2 1
1 3 11
2 3 7
2
10 0 3
20 1 4
输出:
170
impossible