南京邮电大学算法分析与设计实验三(回溯法)

1 回溯法求解 8 皇后问题

1.0 简介

回溯法求解8皇后问题是一种经典应用,通过系统地搜索所有可能的放置组合来避免冲突,找到所有符合条件的解。

8皇后问题是一个经典的计算机科学问题,它要求在8x8的棋盘上放置8个皇后,使得它们互不攻击,即任何两个皇后都不在同一行、同一列或同一对角线上。该问题通过回溯法得到有效解决,下面介绍回溯法在8皇后问题中的应用。

1.0.1 回溯法简介

回溯法是一种通过试错来解决问题的算法,它尝试分步解决问题,并在发现当前步骤的解决方案不可行时,将该步骤的选择撤销,然后再次尝试其他可能的选择。这种方法可以视为一种暴力搜索,但它通过剪枝(即及早停止某些不可能达到最终解的路径)来提高效率。

1.0.2 8皇后问题的求解过程

  1. 逐行放置皇后

算法从空棋盘开始,每次选择一行放置一个皇后。在每一步,算法尝试在当前行的所有列中放置一个皇后,并检查是否与之前行的皇后冲突。

  1. 检查冲突

放置皇后时,需要确保新放置的皇后不仅不与当前棋盘上的任何皇后处在同一列,而且不处于同一对角线上。这可以通过简单的循环和条件判断实现。

  1. 递归与回溯

一旦在某一行成功放置了一个皇后,算法递归地在下一行尝试放置下一个皇后。如果在任何点发现无法放置皇后(即所有列都会有冲突),则回溯到上一行,移动皇后到下一列,并继续尝试。

  1. 记录解决方案

每当在最后一行成功放置皇后时,就找到了一个解决方案。在这种情况下,通常会记录或输出棋盘的配置。

  1. 重置与重复

找到解决方案后,算法会回溯到上一步,继续尝试其他可能的列位置。这个过程一直持续到所有可能的列都已经尝试过,这时算法结束。

1.0.3 实现细节

  1. 数据结构:使用一个数组或列表来表示棋盘,其中每个元素的值表示对应列中的皇后所在的行号。
  2. 递归函数:设计一个递归函数,参数包括当前行号、棋盘状态等。
  3. 剪枝策略:在尝试每一列之前,检查是否已经达到了剪枝条件,例如检查当前列是否已经被完全搜索过。

1.0.4 总结

回溯法是解决约束满足问题的有效工具,8皇后问题就是一个典型的例子。该问题展示了如何利用回溯法系统地搜索解空间,并通过剪枝减少不必要的计算。尽管回溯法可能不是最高效的方法来解决特定问题,但它的简单性和一般性使其成为教学和实践中非常有价值的工具。通过逐步构建解决方案并及时回退,回溯法能够有效地找到所有可能的解,从而解决复杂的布局和配置问题。

1.1 题目

要求用回溯法求解 8 皇后问题 ,使放置在 8*8 棋盘上的 8 个皇后彼此不受攻击,即:任何两个皇后都不在同一行、同一列或同一斜线上。请输出 8皇后问题的所有可行解。

1.2 代码

#include <iostream> 
using namespace std;
#include <math.h> 
#include <vector>

static int ii = 0;

bool Place(int k, int i, int* x) //判定两个皇后是否在同一列或在同一斜线上 
{
	for (int j = 0; j < k; j++)
		if ((x[j] == i) || (abs(x[j] - i) == abs(j - k))) return false;
	return true;
}

void NQueens(int k, int n, int* x) //递归函数(求解n皇后问题) 
{
	for (int i = 0; i < n; i++)
	{
		if (Place(k, i, x))
		{
			x[k] = i;
			if (k == n - 1)
			{	
				ii++;
				cout << "(" << ii << ") ";
				for (i = 0; i < n; i++) {
					cout << x[i] << " ";
				}
				cout << endl;
			}
			else
			{
				NQueens(k + 1, n, x);
			}
		}
	}
}
void NQueens(int n, int* x)
{
	NQueens(0, n, x);
}

void main()
{
	int x[8];
	for (int i = 0; i < 8; i++) x[i] = -1;
	NQueens(8, x);

}

1.3 实验结果

八皇后92个解

2 回溯法解决装载问题

2.0 简介

回溯法解决装载问题展示了通过系统地搜索解决方案空间来找到满足特定约束条件的解的过程。装载问题通常描述为:给定一组物品,每个物品有各自的重量,以及一个容器(如背包或船只)的承载能力,目标是选择一些物品装入容器中,使得总重量不超过容器的承载能力,同时尽可能使总价值最大化(或满足其他优化条件)。

2.0.1 装载问题的定义和重要性

装载问题,也称为背包问题的一种变体,是组合优化领域中的一个典型问题。在实际应用中,装载问题可以应用于物流、资源分配、金融投资等多个领域。例如,在物流公司需要将不同重量和价值的货物装载到有限容量的车辆中时,该问题就变得非常实际和关键。

2.0.2 回溯法的应用

  1. 算法基础

回溯法通过从可能的解决方案空间中逐步构建候选解,并在发现当前候选解不符合问题约束时进行剪枝(即停止进一步探索该路径),来尝试寻找所有可能的解。

  1. 深度优先搜索

回溯法通常以深度优先搜索的方式递归地遍历解决方案空间。在装载问题中,这意味着算法会从无物品装载开始,逐步添加物品,直至达到载具的最大承载量。

  1. 剪枝策略

有效的剪枝策略是回溯法提升效率的关键。在装载问题中,如果当前已装载的物品总重量已经超过了最大承载量,或者在搜索树的某一层发现无法达到最优解(如当前总价值已低于已知的有效解),则可以立即停止向下搜索。

  1. 优化目标

在装载问题中,优化目标可能是最大化装载物品的总价值,或是最小化运载成本。根据不同的优化目标,回溯过程中的决策逻辑会有所不同。

2.0.3 具体实现步骤

  1. 初始化状态:设置起始状态为没有任何物品被装载。
  2. 递归函数设计:实现一个递归函数,用于尝试将每个物品加入装载列表或从列表中移除。
  3. 冲突检测:检查加入新物品后是否会超出载具的承载能力。
  4. 解的记录与更新:每次找到一个可行解(总重不超过承载能力),比较并记录最优解。
  5. 回溯操作:当发现当前路径不可能达到更好解时,撤销最后的选择,返回上一步继续尝试其他选项。

2.0.4 总结

回溯法提供了一种清晰且直接的方式来解决装载问题,尽管它可能不是最高效的方法(特别是对于非常大的物品集合),但它的简单性和易于理解使其成为解决实际问题的一个有用工具。通过逐步构建解决方案并在必要时进行剪枝,回溯法能有效减少需要考虑的选项,从而在合理的时间内找到问题的解。这种方法的系统性和灵活性使其可以被应用于多种不同的优化问题中,不仅仅是装载问题。

2.1 题目

用回溯法 编写一个 递归程序 解决 如下 装载问题 有 n 个集装箱要装上 2 艘载重分别为 c 1和 c 2 的轮船,其中集装箱 i 的重量为 w i 1 ≤ i ≤ n )),且 Σ𝑤𝑖≤𝑐1+𝑐2𝑛𝑖=1。 问是否有一个合理的装载方案可以将这 n 个集装箱装上这 2 艘轮船?如果有,请给出装载方案。

2.2 代码

#include <iostream> 
using namespace std;
#include <math.h> 
#include <iomanip>

//4、算法实现主体部分如下,请补充完整,并使用下面三个测试案例调试通过。
//第一艘船载重60,第二艘船载重40,5个集装箱重量分别为:
//(1)22 35 24 19 4
//(2)22 35 24 15 4
//(3)22 3524 15 3

template <class T>
class Loading
{
private:
	int n, //集装箱数 
		* x, //当前解 
		* bestx; //当前第一艘船的最优解 
	T c1, //第一艘轮船的核定载重量 
		c2, //第二艘轮船的核定载重量 
		* w, //集装箱重量数组 
		total, //所有集装箱重量之和 
		cw, //当前第一艘船的载重量 
		bestw, //当前第一艘船的最优载重量 
		r; //剩余集装箱总重量 
public:
	Loading(int* wx, int n = 5, int c1 = 60, int c2 = 40) //构造函数 
	{
		this->n = n;
		this->c1 = c1;
		this->c2 = c2;
		this->bestw = 0;
		this->cw = 0;
		this->w = new int[n+1];
		this->x = new int[n+1];
		this->bestx = new int[n+1];
		for (int i = 0; i <= n; i++) {
			this->w[i] = wx[i];
		}
		this->total = 0;
		for (int i = 0; i <= n; i++) {
			this->total += this->w[i];
			this->r = this->total;
		}
	} 

	~Loading() //析构函数 
	{
		delete[] w;
		delete[] x;
		delete[] bestx;
	}

	void Backtrack(int i);			//找到最接近第一艘轮船载重c1的最佳装载方案, 
									//最优载重值bestw,最优解数组bestx。 
	void Show();//输出整个装载方案 
};

template <class T>
void Loading<T>::Backtrack(int i)
{ //搜索第i层结点 
	if (i > n)
	{//到达叶节点 

		if (cw > bestw)
		{
			for (int j = 1; j <= n; j++)
				bestx[j] = x[j];
			bestw = cw;
		}
		return;
	}
	//搜索子树 
	r -= w[i];
	if (cw + w[i] <= c1) //x[i]=1时的可行解约束条件 
	{//搜索左子树 
		x[i] = 1;
		cw += w[i];
		Backtrack(i + 1);
		cw -= w[i];
	}
	if (cw + r > bestw) //x[i]=0时增加的约束函数,剪去不含最优解的分枝 
	{//搜索右子树 
		x[i] = 0;
		Backtrack(i + 1);
	}
	r += w[i];
}

template <class T>
void Loading<T>::Show()
{
	//(请自行补充)
	int totalw = 0;
	int c1w = 0;	//11第一艘船总载重
	int i;

	for (i = 1; i <= n; i++) {
		if (bestx[i] == 1) {
			c1w += w[i];
		}
		totalw += w[i];
	}
		
	if (totalw - c1w > c2) {
		cout << "没有合理的装载方案" << endl; 
		return;
	}
	
	cout << "最优解(第一艘船) : (";
	for (i = 1; i < n; i++) 
		cout << bestx[i] << ","; 
	cout << bestx[i] << ")"<<endl;
	cout << "最优解(第二艘船) : (";
	for (i = 1; i < n; i++)
		cout << !bestx[i] << ","; 
	cout << !bestx[i] << ")" << endl;

	cout << "装在方案如下: " << endl;
	cout << "第一艘船装载: " << endl;
	for (i = 0; i <= n; i++) 
		if (bestx[i] == 1) 
			cout << "      集装箱" << i << ": " << setw(2) << w[i] << endl;
			
	cout << "总载重: " << c1w;
	if (c1 - c1w == 0)
		cout << ", 装满" << endl;
	else
		cout << ", 剩余载重量" << c1 - c1w << "! " << endl;

	cout << " 第二艘船装载: " << endl;
	for(i = 1; i <= n; i++)
		if(!bestx[i] == 1)
			cout<<"集装箱"<<i<<": "<< setw(2) << w[i] << endl;

	cout << "总载重: " << totalw - c1w;
	if (c2 - (totalw - c1w) == 0)
		cout << ", 装满!" << endl;
	else
		cout << ", 剩余载重量" << c2 - (totalw - c1w) << endl;


}
int main()
{
	int arr[3][6] = {{0, 22, 35, 24, 19, 4},
					 {0, 22, 35, 24, 15, 4},
					 {0, 22, 35, 24, 15, 3}};
	for (int i = 0; i < 3; i++) {
		cout << "第" << i+1 << "个问题" << endl;
		Loading<int> ld(arr[i]);
		ld.Backtrack(1);
		ld.Show();
		cout << endl << endl;
	}
	return 0;
}

2.3 实验结果

三个装载问题

3 N皇后输出独立解

3.1 题目

请编程实现从 n 皇后问题的所有 92 种可行解中筛选出 12 种 独立解,而其余的解都可以由这 些 独立解利用对称性或旋转而得到。

3.2 代码

#include <iostream>
#include <math.h>
#include <iomanip>
using namespace std;

class nqueen
{
public:
	nqueen(int N);              //构造函数
	~nqueen();                  //析构函数
	void nqueens();
	void Out_Solutions();       //输出Solutions数组中的12个独立解
private:
	bool Place(int k, int i);   //约束函数, 判断两个皇后是否在同一列或同一斜线,不冲突返回true
	void nqueens(int k);
	bool Constructing(int num); //判断是否独立解,是则返回true。
	bool Test(int num);
	void Out_a_Solution(int* xx, char* ch); //输出一个解

	int* y;                     //临时保存变换后的可行解
	int** solutions;            //存12个独立解。由于递归,solutions数组不能只开12行,否则输出结果出错!
	int number;                 //独立解的个数
	int n;                      //n-皇后问题
	int* x;                     //存放一个解
};

nqueen::nqueen(int N)  //构造函数
{
	n = N;  number = 0;
	x = new int[n];
	solutions = new int* [20];
	for (int i = 0; i < 20; i++)
		solutions[i] = new int[n];
	y = new int[n];
}

nqueen::~nqueen()      //析构函数
{
	delete[]x;
	for (int i = 0; i < 20; i++) delete[]solutions[i];
	delete[]solutions;
	delete[]y;
}

//输出solutions数组中的12个独立解
void nqueen::Out_Solutions()
{
	int i, j;

	for (i = 0; i < number; i++)
	{
		cout << setw(4) << i + 1 << " (";
		for (j = 0; j < n - 1; j++) cout << solutions[i][j] << ",";
		cout << solutions[i][j] << ")" << endl;
	}
}

//输出1个解,调试理解使用
void nqueen::Out_a_Solution(int* xx, char* ch)
{
	int i;

	if (number < 1)
	{
		cout << ch << "(";
		for (i = 0; i < n - 1; i++) cout << xx[i] << ",";
		cout << xx[i] << ")" << endl;
	}
}

//约束函数, 判断两个皇后是否在同一列或同一斜线,不冲突返回true
bool nqueen::Place(int k, int i)
{
	for (int j = 0; j < k; j++)
		if ((x[j] == i) || (abs(x[j] - i) == abs(j - k)))
			return false;	//在同一列或同一斜线,返回false 
	return true;
}

void nqueen::nqueens()
{
	nqueens(0);
}

void nqueen::nqueens(int k)
{
	for (int i = 0; i < n; i++)	//显式约束 
	{
		if (Place(k, i))					  //判断第k个皇后能否放在下标i列  
		{
			x[k] = i;
			if (k == n - 1)
			{
				for (i = 0; i < n; i++)
					solutions[number][i] = x[i];  //第x[i]列元素放在第number个解的第i个位置,即将一个解x[]放到solutions数组中的number行。
				if (Constructing(number))	   //判断是否独立解 
					number++;
			}
			else nqueens(k + 1);
		}
	}
}

bool nqueen::Constructing(int num)	//把新得到的解x[]做7种变换后,得到的结果保存到y[]中,
{
	int j, k;						//和之前的独立解比较,相同返回false,不同返回true 

	 //把新得到的解x[]旋转90°保存到y[]中
	for (j = 0; j < n; j++)                     
		y[solutions[num][j]] = n - 1 - j;
	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	//把新得到的解x[]旋转180°保存到y[]中
	for (j = 0; j < n; j++)                    
		y[n-1-j] = n - 1 - solutions[num][j];
		for (k = 0; k < num; k++)
			if (Test(k)) return false;

	//把新得到的解x[]旋转270°保存到y[]中
	for (j = 0; j < n; j++)                   
		y[n - 1 - solutions[num][j]] = j;

	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	//把新得到的解x[]上下对称变换后保存到y[]中
	for (j = 0; j < n; j++)                    
		y[n - 1 - j] = solutions[num][j];
	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	//把新得到的解x[]左右对称变换后保存在y[]中
	for (j = 0; j < n; j++)                      
		y[j] = n - 1 - solutions[num][j];
	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	//把新得到的解x[]关于y=x对角线对称变换后保存到y[]中
	for (j = 0; j < n; j++)                      
		y[solutions[num][j]] = j;
	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	//把新得到的解x[]关于y=-x对角线对称变换后保存到y[]中
	for (j = 0; j < n; j++)                      
		y[n - 1 - solutions[num][j]] = n - 1 - j;
	for (k = 0; k < num; k++)
		if (Test(k)) return false;

	return true;                            //与之前得到的独立解不同,返回true
}

//判断变换后得到的解y[]与之前得到的独立解solutions[num][]是否相同
bool nqueen::Test(int num)
{
	for (int i = 0; i < n; i++)
	{
		if (y[i] != solutions[num][i])
			return false;                //不相同,返回false,该解为独立解
	}
	return true;                   //相等,返回true,该解为重复解
}

void main()
{
	int n = 8;

	nqueen solution(n);
	cout << "8-皇后问题的12个独立解(solutions数组):" << endl;
	solution.nqueens();
	solution.Out_Solutions();

}


3.3 实验结果

在这里插入图片描述

  • 12
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亦是远方

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值