算法设计与分析 | 回溯法

实验五、回溯法

一 实验目的与要求

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--;
	}
}

贪心法
不过可以运行
原文链接
https://blog.csdn.net/qq_43717119/article/details/109332899?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522160752507419724827626752%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=160752507419724827626752&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_v2~rank_v29-3-109332899.pc_search_result_no_baidu_js&utm_term=%E5%81%87%E8%AE%BE%E4%B8%80%E4%B8%AA0%201%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%98%E6%98%AFn&3%EF%BC%8C%E9%87%8D%E9%87%8F%E4%B8%BAw=%EF%BC%8816,15,15%EF%BC%89%EF%BC%8C%E4%BB%B7%E5%80%BC%E4%B8%BAv&(45,25,25),%E8%83%8C%E5%8C%85%E9%99%90%E9%87%8D%E4%B8%BAW=30%EF%BC%8C%E6%B1%82%E6%94%BE%E5%85%A5%E8%83%8C%E5%8C%85%E6%80%BB%E9%87%8D%E9%87%8F%E5%B0%8F%E4%BA%8E%E7%AD%89%E4%BA%8EW%E5%B9%B6%E4%B8%94%E4%BB%B7%E5%80%BC%E6%9C%80%E5%A4%A7%E7%9A%84%E8%A7%A3%EF%BC%8C%E8%AE%BE%E8%A7%A3%E5%90%91%E9%87%8F%E4%B8%BAx&spm=1018.2118.3001.4449

0-1背包问题(回溯法解决)

#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

  • 4
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值