五大常用算法--回溯算法

 

回溯算法的基本思想

       回溯法采用深度优先方法搜索遍历问题的解空间树,可以看作是蛮力法穷举搜索的改进。先判断该节点对应的部分是否是满足约束条件,也就是判断该节点是否包含问题的最优解。如果肯定不包含,则跳过对该节点为根的子树的搜索,即所谓的剪枝;否则,进入该节点为根的子树,继续按照深度优先策略搜索。回溯法常常可以避免搜索所有可能的解,所以,适用于求解组合数组较大的问题。

       首先我们先了解一下一个基本概念“解空间树”:

       解空间树的动态搜索:在搜索至树中任一节点时,先判断该节点对应的部分是否是满足约束条件,或者是否超出目标函数的界,也就是判断该节点是否包含问题的最优解。如果肯定不包含,则跳过对该节点为根的子树的搜索,即所谓的剪枝;否则,进入该节点为根的子树,继续按照深度优先策略搜索。(这也是为什么回溯可以避免搜索所有的解)

回溯与分支限界很类似,回溯是DFS+Cutting(约束剪枝),而分支限界是BFS+Cutting(限界剪枝)

分支限界法与回溯法

(1)求解目标:回溯法的求解目标是找出解空间树中满足约束条件的所有解,而分支限界法的求解目标则是找出满足约束条件的一个解,或是在满足约束条件的解中找出在某种意义下的最优解。

(2)搜索方式的不同:回溯法以深度优先的方式搜索解空间树,而分支限界法则以广度优先或以最小耗费优先的方式搜索解空间树。

在用回溯法求解问题时,常常遇到两种典型的解空间树:

子集树:从n个元素的集合中找出满足某种性质的子集时,相应的解空间树成为子集树

排列树:确定n个元素满足某种性质的排列时,相应的解空间称为排列树。 

 

经典样例1:(01背包问题的回溯解法)

01背包问题属于子集树问题

假设现有容量m kg的背包,另外有i个物品,重量分别为w[1] w[2] ... w[i] (kg),价值分别为p[1] p[2] ... p[i] (元),将哪些物品放入背包可以使得背包的总价值最大?最大价值是多少?

在选择物品的时候,对于每种物品i只有两种选择,即装入背包或不装入背包。某种物品不能装入多次(可以认为每种物品只有一个),因此该问题被称为0-1背包问题。

01背包问题真的是一个老生常谈的问题,分治,动态规划,贪心,回溯到处都可以见到它。

 

第一步:画解空间树, ABC是三个物品,1是放入,0是不放入

第二步:设计Cutting函数

Cutting函数有两个分类:

第一个:约束函数:即不满足约束条件,比如01背包问题中的背包装不下了

第二个:限界函数:即这条路没有走另外一条路好,少年,回头吧,走另外一条路

 

下面开始我们Cutting函数的设计

最简单的一个:背包装不下的时候肯定不能再装该物品

即:要求已经装了的物品重量加上该物品的重量<=背包容量

即装得下就装,装不下就剪

这个是考虑得装不下的情况,即剪的是右枝

现在考虑剪左枝的情况

左枝:即当前考虑的物品不装入背包的Cutting函数

如果已经选了的物品的总价值加上当前物品后面可以装的物品的价值大于最优总价值

那么当前物品就没有必要装。

我装后面的物品得到的价值比当前的最优价值还大,那我当前物品完全就没有装的必要嘛

最优价值是更新的

代码:

#include<iostream>
using namespace std;

const int MAX = 100;
int num;//物品数量
int price[MAX], weight[MAX];
int bagSize;//背包容量
int curPrice = 0;//当前装的物品的总价值
int curWeight = 0;//当前装的物品的总重量
int bestPrice = 0;//最优总价值
int best[MAX];//最优解
int temp[MAX] = { 0 };//当前解

int bound(int i)
{
	int l = bagSize - curWeight;
	int b = curPrice;
	while (weight[i] <= l && i <= num)
	{
		b += price[i];
		l -= weight[i];
		i++;
	}
	if (i <= num)
		b += l * price[i] / weight[i];
	return b;

}
void dfs(int i)
{
	if (i > num)//搜完了一条路
	{
		for (int j = 1; j <= num; j++)
		{
			best[j] = temp[j];
		}
		bestPrice = curPrice;
	}
	else
	{
		if (curWeight + weight[i] <= bagSize)//装得下
		{
			temp[i] = 1;//该物品装
			curWeight += weight[i];
			curPrice += price[i];

			dfs(i + 1);//搜索下一个物品

			temp[i] = 0;//回退
			curWeight -= weight[i];
			curPrice -= price[i];
		}
		//装后面的物品得到的价值比当前的最优价值还大,那当前物品完全就没有装的必要
		if (bound(i + 1) > bestPrice)
		{
			temp[i] = 0;
			dfs(i + 1);
		}
	}
}
void putout()
{
	int sum = 0;
	cout << "最优解:" << endl;
	for (int i = 1; i <= num; i++)
	{
		cout << best[i] << " ";
		if (best[i] == 1)
			sum += weight[i];
	}
	cout << endl;
	cout << "放入背包的物品重量为: " << sum << " 价值为: " << bestPrice << endl;
}
int main()
{
	cin >> num >> bagSize;
	for (int i = 1; i <= num; i++)
	{
		cin >> price[i] >> weight[i];
	}
	dfs(1);
	putout();

	system("pause");
	return 0;
}
/*
输入:
5 10
6 2
3 2
6 4
5 6
4 5
输出:
1 1 1 0 0 放入背包的物品重量为:8 价值为:15
*/

 

经典样例二:N皇后问题

N皇后问题属于排列数问题

在N*N的棋盘上,放置N个皇后,要求每一横行,每一列,每一对角线上均只能放置一个皇后,求可能的方案及方案数。

经典的是8皇后问题,这里我们为了简单,以4皇后为例。

    首先利用回溯算法,先给第一个皇后安排位置,如下图所示,安排在(1,1)然后给第二个皇后安排位置,可知(2,1),(2,2)都会产生冲突,因此可以安排在(2,3),然后安排第三个皇后,在第三行没有合适的位置,因此回溯到第二个皇后,重新安排第二个皇后的位置,安排到(2,4),然后安排第三个皇后到(3,2),安排第四个皇后有冲突,因此要回溯到第三个皇后,可知第三个皇后也就仅此一个位置,无处可改,故继续向上回溯到第二个皇后,也没有位置可更改,因此回溯到第一个皇后,更改第一个皇后的位置,继续上面的做法,直至找到所有皇后的位置,如下图所示。

    这里为什么我们用4皇后做例子呢?因为3皇后是无解的。

代码:

实现思路:

遍历所有行,

  1. 判断竖列,左斜列,右斜列是否可以放入皇后;
  2. 可以,则放入皇后,更新放入皇后位置在竖列,左斜列,右斜列中的值;
  3. 行数加一,递归调用本函数;
  4. 如果在递归中没有结果,回溯到递归前,清除上一次放入皇后的位置,同时清除皇后位置在竖列,左斜列,右斜列中的值;
  5. 如果在递归中有结果,继续遍历递归函数中的行。
  6. 直到遍历到最后一行,输出放置皇后方案。
#include<iostream>
using namespace std;

const int MAX = 100;
// 棋盘,为0表示是空的,大于0表示放了皇后
int chess[MAX][MAX];
// 分别表示第i竖列,第i左斜列,第i右斜列有没有放皇后,放了为1,没有为0
// 行有没有皇后是通过直接循环控制的
int column[MAX], L[MAX], R[MAX];
// 结果
int result = 0;

//输出棋盘
void Print(int n)
{
	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < n; j++)
		{
			printf("%d ", chess[i][j]);
		}
		printf("\n");
	}
	printf("\n");
}
void Dfs(int i, int n)
{
	for (int j = 0; j < n; j++)
	{
		if (!column[j] && !L[i - j + n] && !R[i + j])//安全,可以放
		{
			chess[i][j] = i + 1;//放皇后,i+1表示该皇后属于第几个放的皇后
			column[j] = L[i - j + n] = R[i + j] = 1;
			if (i == n - 1)//已经到最后一行,每一行都放了皇后
			{
				Print(n);//输出棋盘
				result++;//方案数加1
			}
			else
			{
				Dfs(i + 1, n);//继续试探
			}
			//试探完成后的回退
			chess[i][j] = 0;
			column[j] = L[i - j + n] = R[i + j] = 0;
		}
	}
}
int main()
{
	int n;
	cin >> n;
	//初始化,把数组的值都设置为0
	memset(chess, 0, sizeof(chess));
	memset(column, 0, sizeof(column));
	memset(L, 0, sizeof(L));
	memset(R, 0, sizeof(R));
	Dfs(0, n);
	printf("Answer:%d\n", result);

	system("pause");
	return 0;
}
/*
输入:
4
输出:
0 1 0 0
0 0 0 2
3 0 0 0
0 0 4 0

0 0 1 0
2 0 0 0
0 0 0 3
0 4 0 0

Answer:2
*/

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值