算法设计与分析第六章回溯算法(一)

算法设计与分析第六章回溯算法(一)

一、回溯算法概述

(1)、以深度优先的方式系统地搜索问题的解的方法称为回溯法。可以系统地搜索一个问题的所有解或任意解。
(2)、回溯法的基本做法是搜索,或是一种组织得井井有条的,能避免不必要搜索的穷举式搜索法。
(3)、应用回溯法求解时,需要明确定义问题的解空间。问题的解空间应至少包含问题的一个(最优)解。
解空间的特点:
(完全)二叉树.
问题的解是一棵子树(一条路)
通过深度优先搜索获得最优解
在这里插入图片描述
(4)、回溯算法基本思想:
在回溯法搜索解空间树时,通常采用两种策略(剪枝函数)避免无效搜索以提高回溯法的搜索效率:
用约束函数在扩展结点处剪去不满足约束条件的子树;
用限界函数剪去不能得到最优解的子树。

二、回溯算法有关例题
1、素数环

问题描述:
素数环:从1到20这20个数摆成一个环,要求相邻的两个数的和是一个素数。

问题分析:
从1开始,每个空位有20种可能,只要填进去的数合法:
(1)、与前面的数不相同;
(2)、与左边相邻的数的和是一个素数。
(3)、第20个数还要判断和第1个数的和是否素数。

素数环(3个数)的解空间(搜索空间)
在这里插入图片描述
分析流程:
1、数据初始化;
2、递归填数:判断第i个数填入是否合法;
A、如果合法:填数;判断是否到达目标(20个已填完):
是,打印结果;不是,递归填下一个;(剪枝条件)
B、如果不合法:选择下一种可能;

代码:

#include <iostream>
#include<cmath>
#include<cstdlib>
#include<cstdio>

using namespace std;
bool b[21]={0};
int total=0,a[21]={0};
int print();             //输出答案
bool pd(int x,int y);    //判断素数
int search(int x);       //回溯方案
int main()
{

   search(1);
   cout<<total<<endl;    //这是总方案数
   return 0;
}
int search(int x)
{
    int i;
    for(i=1;i<=20;i++)
        if(pd(a[x-1],i)&&!b[i])//和是否为素数  该数是否可用
    {
        a[x]=i;
        b[i]=1;
        if(x==20)
        {
            pd(a[20],a[1]);
            print();
        }
        else search(x+1);
        b[i]=0;              //可以选择不用这个数,保证每种方案
    }
}
bool pd(int x,int y)
{
    int i=x+y,j=2;
    while(j<=sqrt(i)&&i%j!=0)
        j++;
        if(j>sqrt(i))  return 1;
        else       return 0;
}
int print()
{
   total++;
   cout<<"<"<<total<<">";    //第几种方案
   for(int j=1;j<=20;j++)    //素数圈
     cout<<a[j]<<" ";
   cout<<endl;
}

2、n位数任取r个数进行全排列

问题描述:
设有n个整数的集合{1,2,…,n},从中取出任意r个数进行排列(r<n),试列出所有的排列。

问题分析:
3个数全排列搜索空间
在这里插入图片描述

代码:

#include<cstdio>
#include<iostream>
#include<iomanip>
using namespace std;
int num=0,a[10001]={0},n,r;
bool b[10001]={0};
int search(int);      //回溯过程
int print();              //输出方案
int main(){
   cout<<"input n,r:";
   cin>>n>>r;
   search(1);
   cout<<"number="<<num<<endl;    //输出方案总数
}
int search(int k){
    int i;
    for (i=1;i<=n;i++)
     if  (!b[i]) {                                //判断i是否可用
         a[k]=i;                               //保存结果
         b[i]=1;
         if (k==r)
	print();
         else
	search(k+1);
       b[i]=0;
      }
}
int print(){
  num++;
  for (int i=1;i<=r;i++)
    cout<<setw(3)<<a[i];
  cout<<endl;
}

运行结果:
在这里插入图片描述

3、自然数拆分问题

问题描述:
任何一个大于1的自然数n,总可以拆分成若干个小于n的自然数之和
当n=7共14种拆分方法:
7=1+1+1+1+1+1+1
7=1+1+1+1+1+2
7=1+1+1+1+3
7=1+1+1+2+2
7=1+1+1+4
7=1+1+2+3
7=1+1+5
7=1+2+2+2
7=1+2+4
7=1+3+3
7=1+6
7=2+2+3
7=2+5
7=3+4
total=14

问题分析:
n=4 的解空间
在这里插入图片描述

代码:

#include<cstdio>
#include<iostream>
#include<cstdlib>
using namespace std;
int a[10001]={1},n,total;
int search(int  s ,int  t); // s 代表待拆分的数; 序列中t 代表拆分序列中的数的编号
int print(int);
int main(){
    cin>>n;
    search(n,1);          //将要拆分的数n传递给s
    cout<<"total="<<total<<endl;        //输出拆分的方案数
}
 int search(int s,int t){
    int i;
    for (i=a[t-1];i<=s;i++)//后面的加数不小于前面的加数
     if (i<n) { //当前数i要大于等于前1位数,且不过n
       a[t]=i;  //保存当前拆分的数i
       s-=i; 	//s减去数i, s的值将继续拆分
       if (s==0)
	print(t);             //当s=0时,拆分结束输出结果
         else
	search(s,t+1);       //当s>0时,继续递归
       s+=i;    //回溯:加上拆分的数,以便产生所有可能的拆分
      }
}
int print(int t){
    cout<<n<<"=";
    for (int i=1;i<=t-1;i++)                  //输出一种拆分方案
      cout<<a[i]<<"+";
    cout<<a[t]<<endl;
    total++;                                  //方案数累加1
}

运行结果:
在这里插入图片描述

4、装载问题

问题描述:
给定n个集装箱要装上一艘载重量为c的轮船,其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下,将尽可能多的集装箱装上轮船(贪心算法中的装载问题讨论的是装载件数;本题讨论的是最大装载重量。)
输入样例
34 3
21 10 5
输出(考虑最大装载量的最优解)
31(重量)
1 2

考虑最大装载件数的最优解
2(件)
5 10

问题分析:
由于集装箱问题是从n个集装箱里选择一部分集装箱,假设解向量为X(x1, x2, …, xn),其中xi∈{0, 1}, xi =1表示集装箱i装上轮船, xi =0表示集装箱i不装上轮船。
输入
每组测试数据:第1行有2个整数c和n。C是轮船的载重量(0<c<30000),n是集装箱的个数(n≤20)。第2行有n个整数w1, w2, …, wn,分别表示n个集装箱的重量。
输出
对每个测试例,输出两行:第1行是装载到轮船的最大载重量,第2行是集装箱的编号。

该问题的形式化描述为:在这里插入图片描述
用回溯法解装载问题时,其解空间是一棵子集树,与0 - 1背包问题的解空间树相同。
在这里插入图片描述
可行性约束函数可剪去不满足约束条件的子树:
在这里插入图片描述
令cw(t)表示从根结点到第t层结点为止装入轮船的重量,即部分解(x1, x2 , …, xt)的重量:
在这里插入图片描述
当cw(t)>c时,表示该子树中所有结点都不满足约束条件,可将该子树剪去。

代码:

#include <iostream>
using namespace std;
class goods{
	int weight;
public:
	goods(int w=0):weight(w)
	{}
	int get_w(){
		return weight;
	}
	void set(int w){
		weight=w;
	}

};
void load(goods *g, int *x, int t, int n,int cw, int &bestcw ,int *best,int r,int c){
	if(t>n) {     //已经遍历的到叶子结点,得到了一个解决方案
	    if(cw>bestcw)	{
		for(int i=0;i<n;i++)
	       	    best[i]=x[i];
		    bestcw=cw;
		}
	}
	   else{ //每个结点可以有两个分支,分别利用约束规则和限界规则进行剪枝
	r=r-g[t].get_w();//剩余未处理的物品的重量和,与是否选取当前物品无关
	if(cw+g[t].get_w()<=c){ //  根据题意中的约束条件进行剪枝
	  	x[t]=1;
		cw=cw+g[t].get_w(); //当前装入的物品的重量和
		load(g,x,t+1,n,cw,bestcw,best,r,c);
		cw=cw-g[t].get_w(); //回溯的需要
	}
	if(cw+r>bestcw) {    //限界规则
		x[t]=0;
		load(g,x,t+1,n,cw,bestcw,best,r,c);
	}
	r=r+g[t].get_w(); //回溯的需要
      }
   }
int main(){
	int n,c,bestcw=0;
	int *x,*best, r=0;
	cout<<"请输入物品的件数和轮船的装载重量:";
	cin>>n>>c;
	goods *g;
	g=new goods[n];
	x=new int [n];
	best=new int[n];
	cout<<"请输入每件物品的重量:";
	for(int i=0;i<n;i++)	{
		int w;	cin>>w;  g[i].set(w);r=r+w;
	}
	load(g,x,0,n,0,bestcw,best,r,c);
	cout<<bestcw<<endl;
for(int i=0;i<n;i++){
		cout<<best[i]<<"  ";
	}
	cout<<endl;
	return 0;
}

运行结果:
在这里插入图片描述

5、0-1背包问题

问题描述:
给定一个物品集合s={1,2,3,…,n},物品i的重量是wi,其价值是vi,背包的容量为W,即最大载重量不超过W。在限定的总重量W内,我们如何选择物品,才能使得物品的总价值最大。
在这里插入图片描述
问题分析:
令cw(i)表示目前搜索到第i层已经装入背包的物品总重量,即部分解(x1, x2 , …, xi)的重量:
在这里插入图片描述
对于左子树, xi =1 ,其约束函数为:
在这里插入图片描述
若constraint(i)>W,则停止搜索左子树,否则继续搜索。
假设背包容量C=30,w={16,15,15},v={45,25,25}
在这里插入图片描述
代码:

#include <stdio.h>
#include <conio.h>

int n;//物品数量
double c;//背包容量
double v[100];//各个物品的价值
double w[100];//各个物品的重量
double cw = 0.0;//当前背包重量
double cp = 0.0;//当前背包中物品价值
double bestp = 0.0;//当前最优价值
double perp[100];//单位物品价值排序后
int order[100];//物品编号
int put[100];//设置是否装入

//按单位价值排序
void knapsack()
{
    int i,j;
    int temporder = 0;
    double temp = 0.0;

    for(i=1;i<=n;i++)
        perp[i]=v[i]/w[i];
    for(i=1;i<=n-1;i++)
{
        for(j=i+1;j<=n;j++)
            if(perp[i]<perp[j])//冒泡排序perp[],order[],sortv[],sortw[]
        {
            temp = perp[i];
            perp[i]=perp[i];
            perp[j]=temp;

            temporder=order[i];
            order[i]=order[j];
            order[j]=temporder;
            temp = v[i];
            v[i]=v[j];
            v[j]=temp;

            temp=w[i];
            w[i]=w[j];
            w[j]=temp;
        }
    }
}
//回溯函数
void backtrack(int i)
{
    double bound(int i);
    if(i>n)
    {
        bestp = cp;
        return;
    }
    if(cw+w[i]<=c)
    {
        cw+=w[i];
        cp+=v[i];
        put[i]=1;
        backtrack(i+1);
        cw-=w[i];
        cp-=v[i];
    }
    if(bound(i+1)>bestp)//符合条件搜索右子数
        backtrack(i+1);
}
//计算上界函数
double bound(int i)
{
    double leftw= c-cw;
    double b = cp;
    while(i<=n&&w[i]<=leftw)
    {
        leftw-=w[i];
        b+=v[i];
        i++;
    }
    if(i<=n)
        b+=v[i]/w[i]*leftw;
    return b;

}
int main()
{
    int i;
    printf("请输入物品的数量和容量:");
    scanf("%d %lf",&n,&c);
    printf("请输入物品的重量和价值:");
    for(i=1;i<=n;i++)
    {
        printf("第%d个物品的重量:",i);
        scanf("%lf",&w[i]);
        printf("价值是:");
        scanf("%lf",&v[i]);
        order[i]=i;
    }
    knapsack();
    backtrack(1);
    printf("最有价值为:%lf\n",bestp);
    printf("需要装入的物品编号是:");
    for(i=1;i<=n;i++)
    {
        if(put[i]==1)
            printf("%d ",order[i]);
    }
 return 0;
}

运行结果:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值