实验五、回溯法
一 实验目的与要求
1、 理解回溯法的概念。
2、 掌握回溯法纠结问题基本步骤。
3、 了解回溯算法效率的分析方法
二 实验内容
1、求解组合问题回溯求法
2、0/1背包问题分支求法
三、实验题
1、编写一个实验程序,采用回溯法输出自然数1~n中任取r个数的所有组合
实验报告使用
/*
找n个数中r个数的组合
例如:当 n=5, r=3 时 , 所有组合为:
1 2 3
1 2 4
1 2 5
1 3 4
1 3 5
1 4 5
2 3 4
2 3 5
2 4 5
3 4 5 total=10 { 组合数 }
分析1:
每组3个数的特点:
1)互不相同;
2)前面的数小于后面的数;
将上述两条作为约束条件。
3) 当 r =3时,可用三重循环对
每组中的3个数进行 枚举 。
用递归法设计该问题:
每个组合中的数据必须从大到小排列,因为递归算法设计是要找出大规模问题域
小规模问题之间的关系
分析2:
分析n=5,r=3时10个组合数
1)首先固定第一个数5,然后就是n=4,r=2的组合数,共6个组合
2)其次固定第一个数4,其后就是n=3,r=2的组合数,共3个
3)最够固定第一个数3,后面就是n=2,r=2的组合数,共1个
至此,找到了“5个数中3个数的组合”与"4个数中2个数的组合"
,3个数中2个数的组合,2个数中2个数的组合的递归关系
递归算法的三个步骤:
1)n个数中r各数组合递推到n-1个数r-1个数有组合,n-2个书中r-1个数有组合
,…,r-1个数中有r-1个数有组合,共n-(r-1)次递归
2)递归地边界条件是r=1
3)函数主要操作是输出,每当低轨道r=1时,就有一个新的组合产生,输出他们和
一个换行符,
先固定5,然后进行多次递归,数字5要多次输出,所以要用数组存储以备每次
递归到r=l时输出。
同样每次向下递归都要用到数组,所以将数组设置为全局变量
输入:
5 3
输出:
10
5 4 3
5 4 2
5 4 1
5 3 2
5 3 1
5 2 1
4 3 2
4 3 1
4 2 1
3 2 1
*/
#include <iostream>
#include <string.h>
using namespace std;
const int MAXSIZE = 100;
int g_iArr[MAXSIZE];
bool isOk()
{
int iTemp;
int iNext;
int iCur = 100000;
for(int j = g_iArr[0]; j >= 1; j--)
{
iNext = g_iArr[j];//第一个数
//判断前面的数是否比自己大,正常的顺序应该是前面大,后面小,如果不符合就是前面<=后面。现在拿到的第一个是最前面的
if( iCur <= iNext)
{
return false;
}
iCur = iNext;
}
return true;
}
void dfs(int n,int r)
{
for(int i = n ; i >= r ; i--)
{
//设定当前选中的元素为第一个,因为元素的选取是从大到小,因此为i
g_iArr[r] = i;
//递归基,当r减为1,说明成功了
if(r == 1)
{
if(isOk())
{
for(int j = g_iArr[0]; j >= 1; j--)
{
cout << g_iArr[j];
}
cout << endl;
}
}
//递归步,从剩余n-1个数中挑选r-1个数
else
{
//添加限制条件,后面的大于前面,不能重复
dfs(n-1,r-1);
}
}
}
void process()
{
int n,r;
while(EOF != scanf("%d %d",&n,&r))
{
//要设置初始值,为什么为r,g_iArr[r] = 就等于最大值了
g_iArr[0] = r;
dfs(n,r);
}
}
int main(int argc,char* argv[])
{
process();
getchar();
return 0;
学习更多的方法(回溯算法)
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn=100+50;
int n,r,a[maxn];
void dfs(int now){
if(now==n+1){
if(a[0]==r){
for(int i=1;i<=a[0];i++)cout<<a[i]<<' ';cout<<endl;
}
return ;
}
a[++a[0]]=now;
dfs(now+1);
a[0]--;
dfs(now+1);
}
int main(){
cin>>n>>r;
dfs(1);
return 0;
}
使用C语言实现
本部分未经过测试
文章目录
组合:数字型
方法一
方法二
完整代码
组合:字符型
组合:数字型
【问题】利用递归方法找出从自然数1,2,…,n中任取r个数的所有组合
【例如】n=5,r=3,所有组合为:
在这里插入图片描述
方法一
【思路】
抽象问题:1,…,n中选r --> f(n,r)
从边界n考虑,n要么取,要么不取 --> f(n,r) = f(n-1, r) + f(n-1, r-1)
退出条件:r==0时,就已经选完了
异常条件:n<r的时候
int a[50]; //存放组合的结果数组
void f(int n,int r,int m) {
// 从1,...n序列中选r个数字进行组合,当前已选m个数
// 【m理解】当前已选择m个数 or 此次选择的数放到a[m]的位置 or 结果数组的最后一位
int i;
if (n<r) return ; //异常条件
if (r==0) { //从1,...,n序列中选0个数字进行组合
// 打印输出此次组合的结果
for (i=0; i<m; i++) printf("%d", a[i]);
printf("\n");
} else {
// 将n选入数组,赋值到结果数组
a[m] = n;
f(n-1, r-1, m+1); //已经选了n这个数字-->从1,...,n-1选r-1个
//不选n
f(n-1, r, m); //n没有选-->从1,...,n-1选r个
}
}
方法二
【代码】
// 从1-n的数字中选r个数字
// 目前选的一个放入a[m]位置中
void C(int n, int r, int a[], int m) {
int i;
if (r==0) { //选完了
//输出
for (i=0; i<m; i++) printf("%d", a[i]);
printf("\n");
} else {
// 在[r,n]的范围内选一个数字放入a[m]
for (i=n; i>=r; i--) {
a[m] = i;
C(i-1, r-1, a, m+1);
}
}
}
【理解】用树状的形式输出递归树(先序)
树状的方式类似于这种https://blog.csdn.net/summer_dew/article/details/82937941
在这里插入图片描述
完整代码
方法一:
#include<stdio.h>
int a[50];
void f(int n,int r,int m) {
int i;
if (n<r) return ;
if (r==0) {
for (i=0; i<m; i++) printf("%d", a[i]);
printf("\n");
} else {
//选n
a[m] = n;
f(n-1, r-1, m+1);
//不选n
f(n-1, r, m);
}
}
int main() {
int n,r;
while (1) {
printf("输入n与r,空格分割\n>>> ");
scanf("%d%d", &n, &r);
f(n, r, 0);
printf("\n");
}
return 0;
}
方法二:
#include<stdio.h>
// 从1-n的数字中选r个数字
// 目前选的一个放入a[m]位置中
void C(int n, int r, int a[], int m) {
int i;
// 以树状输出递归树
/*
for (i=0; i<m; i++) {
printf(" ");
}
printf( "C(%d,%d, '" , n,r);
for (i=0; i<m; i++) {
printf("%d ", a[i]);
}
printf("', %d)",m );
printf("\n");
*/
if (r==0) { // 选完了
// 输出
for (i=0; i<m; i++) printf("%d", a[i]);
printf("\n");
} else {
// 在[r,n]的范围内选一个数字放入a[m]
for (i=n; i>=r; i--) {
a[m] = i;
C(i-1, r-1, a, m+1);
}
}
}
int main() {
int n,r;
int a[50];
while (1) {
printf("输入n与r,空格分割\n>>> ");
scanf("%d%d", &n, &r);
C(n, r, a, 0);
printf("\n");
}
return 0;
}
组合:字符型
【问题】从长度为n个字符串str中选出m个元素的可能
【思路】
在这里插入图片描述
【代码】
char *tmp; //中间结果
int top;
int count; //种数
//递归求组合数
void combination(char *str, int n, int m )
{
if( n < m || m == 0 ) return ; //case 1:不符合条件,返回
combination( str+1, n-1, m ); //case 2:不包含当前元素的所有的组合
tmp[ top++ ] = str[0]; //case 3:包含当前元素
if( m == 1 ){ //case 3.1:截止到当前元素
printA( tmp, top );
printf("\n");
count++;
top--;
return;
}
combination( str+1, n-1, m-1); //case 3.2:包含当前元素但尚未截止
top--; //返回前恢复top值
}
【理解】将递归树进行先序输出
树状的方式类似于这种https://blog.csdn.net/summer_dew/article/details/82937941
在这里插入图片描述
【完整代码】
#include<stdio.h>
#include<stdlib.h>
char *tmp; //中间结果
int top; //
int count; //种数
//打印长度为n的数组元素
void printA(char *str,int n)
{
int i;
for(i=0;i<n;i++){
printf("%c ",str[i]);
}
}
//递归求组合数
void combination(char *str, int n, int m )
{
if( n < m || m == 0 ) return ; //case 1:不符合条件,返回
combination( str+1, n-1, m ); //case 2:不包含当前元素的所有的组合
tmp[ top++ ] = str[0]; //case 3:包含当前元素
if( m == 1 ){ //case 3.1:截止到当前元素
printA( tmp, top );
printf("\n");
count++;
top--;
return;
}
combination( str+1, n-1, m-1); //case 3.2:包含当前元素但尚未截止
top--; //返回前恢复top值
}
int main()
{
int n,m;//存放数据的数组,及n和m
char *str;
printf("输入n与m,用空格隔开\n>>> ");
scanf("%d%d",&n,&m);
str = (char *) malloc( sizeof(char) * n );
tmp = (char *) malloc( sizeof(char) * m );
printf("输入字符串\n>>> ");
scanf("%s", str);
printf("\n%s %d中选取%d个", str, n, m);
combination( str, n, m );//求数组中所有数的组合
printf("总数%d\n", count);
return 0;
}
2、假设一个0/1背包问题是n=3,重量为w=(16,15,15),价值为v=(45,25,25),背包限重为W=30,求放入背包总重量小于等于W并且价值最大的解,设解向量为x=(x1,x2,x3),请通过队列式和优先队列式(带限制条件的)两种分支限界法求解该问题。
层次
这个是分析
from
https://wenku.baidu.com/view/7e75f55c86c24028915f804d2b160b4e777f8107.html
实验课代码
#include <iostream>
#include <queue>
#include <algorithm>
using namespace std;
#define MAXN 50
//问题表示
int n=3,W=30;
vector<int> w;//={0,16,15,15}; //重量,下标0不用
vector<int> v;//={0,45,25,25}; //价值,下标0不用
//求解结果表示
int maxv=-9999; //存放最大价值,初始为最小值
int bestx[MAXN]; //存放最优解,全局变量
int total=1; //解空间中结点数累计,全局变量
// # 贪心法----非0/1背包问题,而是部分背包问题
// 使用n,W,w[],v[]
struct NodeType_Knap
{
double w;
double v;
double p; //p=v/w
bool operator<(const NodeType_Knap &s) const
{
return p>s.p; //按p递减排序
}
};
vector<NodeType_Knap> A; // 含有输入的数据和排序后的数据
double V = 0; // 价值,之前是int型,在这里为double
double x[MAXN]; // 最优解double类型,可以选择部分,即一定的比例
/*
* 求单位重量的价值->按照自定义的格式排序->调用 Knap
*/
void knap_m();
/*
* 排序后则贪心循环选择,如果剩余的容量还能容纳当前的,则放进去,不能的话跳出循环,选择部分放入
*/
void Knap();
// !# 贪心法
// # 动态规划法
// 使用n,W,w[],v[],maxv,bestv[]
// 动态规划数组
int dp[MAXN][MAXN];
/*
* 根据状态转移方程来构造动态
* 1>两个边界条件
* 2>由于动态规划数组为二维数组,则两层for循环里判断是否扩展活动节点
扩展则dp[i][r]=dp[i-1][r];
不扩展则二者求最大
*/
void dp_Knap();
/*
* 动态规划数组已经填充完毕,逆着推出最优解
根据状态转移方程中的条件,判断每个物品是否选择
*/
void buildx();
// !# 动态规划法
int main()
{
// 输入格式
/*
3 n个物品假设为3
16 45 第一个物品的重量和价值
15 25 第二个物品的重量和价值
15 25 第三个物品的重量和价值
30 背包容量W
*/
cin >> n;
int m,l;
// 下表0不用,填充0
w.push_back(0);
v.push_back(0);
for (int j = 1; j <= n;j++)
{
cin >> m >> l;
w.push_back(m);
v.push_back(l);
}
cin >> W;
// # 贪心法
//knap_m();
// !# 贪心法
// # 动态规划法
dp_Knap();
buildx();
// !# 动态规划法
cout << "最优解:";
for (int i = 1;i <= n;i++)
{
if (V > 0)
{// 贪心法 输出的是double类型
cout << x[i] << " ";
}else
{// 动态规划输出的是int型
cout << bestx[i] << " ";
}
}
if (V > 0)
{// 贪心法 输出的是double类型
cout << endl << "最大价值为:" << V << endl;
}else
{// 动态规划输出的是int型
cout << endl << "最大价值为:" << maxv << endl;
}
return 0;
}
// 贪心法
void knap_m()
{
int i;
for ( i=0;i<=n;i++)
{
NodeType_Knap k;
k.w = w[i];
k.v = v[i];
A.push_back(k);
}
for ( i=1;i<=n;i++) //求v/w
A[i].p=A[i].v/A[i].w;
// sort(++A.begin(),A.end()); //A[1..n]排序
sort(A.begin(),A.end()); //A[1..n]排序
Knap();
}
// 求解背包问题并返回总价值
void Knap()
{
V=0; //V初始化为0
double weight=W; //背包中能装入的余下重量
int i=1;
while (A[i].w < weight) //物品i能够全部装入时循环
{
x[i]=1; //装入物品i
weight -= A[i].w; //减少背包中能装入的余下重量
V += A[i].v; //累计总价值
i++; //继续循环
}
if (weight > 0) //当余下重量大于0
{
x[i] = weight / A[i].w; //将物品i的一部分装入
V += x[i] * A[i].v; //累计总价值
}
}
// 动态规划法
void dp_Knap()
{
int i,r;
for(i = 0;i <= n;i++) //置边界条件dp[i][0]=0
dp[i][0] = 0;
for (r = 0;r <= W;r++) //置边界条件dp[0][r]=0
dp[0][r] = 0;
for (i = 1;i <= n;i++)
{
for (r = 1;r <= W;r++)
if (r < w[i])
dp[i][r] = dp[i-1][r];
else
if ((dp[i-1][r])>(dp[i-1][r-w[i]]+v[i]))
dp[i][r] = dp[i-1][r];
else
dp[i][r] = dp[i-1][r-w[i]]+v[i];
}
}
void buildx()
{
int i=n,r=W;
maxv=0;
while (i>=0) //判断每个物品
{
if (dp[i][r] != dp[i-1][r])
{
bestx[i] = 1; //选取物品i
maxv += v[i]; //累计总价值
r = r - w[i];
}
else
bestx[i]=0; //不选取物品i
i--;
}
}
#include<iostream>
#include<algorithm>
using namespace std;
#define NUM 100
int c; //背包的容量
int n; //物品的数量
int cw; //当前背包内物品的重量
int cv; //当前背包内物品的总价值
int bestv; //当前最优价值
//描述每个物品的数据结构
struct Object
{
public:
int w; //物品的重量
int v; //物品的价值
double d; //物品的单位价值
public:
double getd()
{
return d;
}
}; //物品数组
Object Q[NUM];
void backtrack(int i);
int Bound(int i);
bool cmp(Object, Object);
//物品的单位价值重量比是在输入数据时计算的
//int main()
void main()
{
cin >> c >> n;
for (int i = 0; i < n; i++)
{
//物品的单位价值重量比是在输入数据时计算的
// scanf_s("%d%d", &Q[i].w, &Q[i].v);
scanf("%d %d", &Q[i].w, &Q[i].v);
Q[i].d = 1.0 * Q[i].v / Q[i].w;
}
sort函数第三个参数采用默认从小到大 ,C++内置函数
sort(Q, Q + n, cmp);
//for ( i = 0; i < n; i++)
//cout << Q[i];
backtrack(1);
cout << bestv;
}
//以物品单位价值重量比递减排序的因子:
bool cmp(Object a, Object b)
{
if (a.d>= b.d)
return true;
else
return false;
}
void backtrack(int i)
{
//到达叶子节点时更新最优值
if (i + 1 > n)
{
bestv = cv;
return;
}
//进入左子树搜索
if (cw + Q[i].w <= c)
{
cw += Q[i].w;
cv += Q[i].v;
backtrack(i + 1);
cw -= Q[i].w;
cv -= Q[i].v;
}
//进入右子树搜索
if (Bound(i + 1) > bestv)
backtrack(i + 1);
}
int Bound(int i)
{
int cleft = c - cw; //背包剩余的容量
int b = cv; //上界
//尽量装满背包
while (i < n && Q[i].w <= cleft)
{
cleft -= Q[i].w;
b += Q[i].v;
i++;
}
//剩余的部分空间也装满
if (i < n)
b += 1.0 * cleft * Q[i].v / Q[i].w;
return b;
}
实验课用
方法一:
0-1背包的裸题,那就可以直接写一个01背包的动态转移方程:dp[j]=max(dp[j],dp[j-w[i]]+p[i])。dp[j]的意思是:当背包已装j的重量的物品时的最大价值。那么它可以由背包已装j-w[i]时最大的价值进行转移,即由dp[j-w[i]]+p[i]得到。注意每一次要将dp[]设置为0,因为背包此时无价值。当状态方程枚举结束后,我们再从 dp[]数组中找一遍,求得答案maxx=max{dp[i]}(i from 0 to c),输出答案maxx。这种动态规划的方法的时间复杂度为O(n^2).
ps:0-1背包也可以写成二维dp[][],只是这样写成滚动数组可以更加节省空间。
代码:
#include<algorithm>
using namespace std;
const int maxn= 2000+50;
int n,c,w[maxn],dp[maxn],p[maxn];
int main(){
int i,j;
while(1){
scanf("%d %d",&n,&c);
if(n==0&&c==0)break;
for(i=1;i<=n;i++)cin>>w[i];
for(i=1;i<=n;i++)cin>>p[i];
memset(dp,0,sizeof(dp));
for(i=1;i<=n;i++){
for(j=c;j>=1;j--){
if(j-w[i]>=0&&dp[j]<dp[j-w[i]]+p[i]){
dp[j]=dp[j-w[i]]+p[i];
}
}
}
int maxx=0;
for(i=0;i<=c;i++)
if(maxx<dp[i])
maxx=dp[i];
cout<<maxx<<endl;
}
return 0;
}
方法二:
除了直接写0-1背包的动态转移方程,还可以直接写dfs,每一个背包无非就是取和不取两个状态,如果要取则要求背包容量 res>=w[now]。分别用ans1,ans2表示取当前物品,不取当前物品的最大价值,dfs返回max(ans1,ans2),dfs的终止条件是now ==n+1。时间复杂度(2^n)。
ps:方法二相较于方法一思维上更加简单,容易想到,但是代码就相对麻烦,并且时间复杂度不够优秀,当然如果加上记忆化搜索后时间复杂度和动态规划是相当的。我个人更喜欢方法一。
代码:
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int maxn= 2000+50;
int n,c,w[maxn],p[maxn];
int dfs(int now,int res){
if(now==n+1)return 0;
int ans1=0,ans2=0;
if(res>=w[now]){
ans1=dfs(now+1,res-w[now])+p[now];
}
ans2=dfs(now+1,res);
if(ans1>=ans2)return ans1;
return ans2;
}
int main(){
int i,j;
while(1){
scanf("%d %d",&n,&c);
if(n==0&&c==0)break;
for(i=1;i<=n;i++)cin>>w[i];
for(i=1;i<=n;i++)cin>>p[i];
cout<<dfs(1,c)<<endl;
}
return 0;
}
参考网址
http://phoenix-zh.cn/2020/10/29/NOJ-%E7%AE%97%E6%B3%95%E8%AE%BE%E8%AE%A1%E7%90%86%E8%AE%BA%E4%BD%9C%E4%B8%9A/#3-0-1%E8%83%8C%E5%8C%85