天天看點

算法設計與分析 | 回溯法實驗五、回溯法實驗課用

實驗五、回溯法

一 實驗目的與要求

1、 了解回溯法的概念。

2、 掌握回溯法糾結問題基本步驟。

3、 了解回溯算法效率的分析方法

二 實驗内容

1、求解組合問題回溯求法

2、0/1背包問題分支求法

三、實驗題

1、編寫一個實驗程式,采用回溯法輸出自然數1~n中任取r個數的所有組合

實驗報告使用

#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

繼續閱讀