一篇博客让你跟深入了解回溯算法

回溯算法的思想

1.回溯算法就是一种有组织的系统最优化搜索技术,可以看作蛮力法穷举搜索的改进。回溯法常常可以避免搜索所有可能的解,所以它适用于求解组织数量较大的问题。

2.首先我们先了解一下一个基本概念“解空间树”:问题的解空间一般使用解空间树的方式来组织,树的根节点位于第1层,表示搜索的初始状态,依次向下排列。

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

4.在搜索过程中,通常采用两种策略避免无效搜索:

  1. 用约束条件剪除得不到的可行解的子树
  2. 用目标函数剪取得不到的最优解的子树 (这两种方式统称为:剪枝函数)

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

  1. 子集树:但所有的问题是从n个元素的集合中找出满足某种性质的子集时,相应的解空间树成为子集树
  2. 排列树:当所给出问题是确定n个元素满足某种性质的排列时,相应的解空间称为排列树。          

6.回溯法的一般步骤:

  1. 设置初始化的方案(给变量赋初始值,读入已知数据等)
  2. 变换方式去试探,若全部试完侧转(7)
  3. 判断此法是否成功(通过约束函数),不成功则转(2)
  4. 试探成功则前进一步再试探
  5. 正确方案还是未找到则转(2)
  6. 以找到一种方案则记录并打印
  7. 退回一步(回溯),若未退到头则转(2)
  8. 已退到头则结束或打印无解

7.回溯法的优点在于其结构明确,可读性强,易于理解,而且通过对问题的分析可以大大提高运行效率。

求子集树

#include<iostream>
using namespace std;

void fun(int arr[], int i, int len,int x[])
{
	if (i == len)
	{
		for (int j = 0; j < len; ++j)
		{
			if (x[j] == 1)
			{
				cout << arr[j] << " ";
			}
		}
		cout << endl;
	}
	else
	{
		x[i] = 1;		//选择i节点
		fun(arr, i + 1, len, x);	//遍历i的右孩子

		x[i] = 0;
		fun(arr, i + 1, len, x);
	}
}

int main()
{
	int arr[] = { 1,2,3 };
	int len = sizeof(arr) / sizeof(arr[0]);
	int x[3] = { 0 };
	fun(arr, 0, len, x);

	return 0;
}

整数选择问题

给定一组整数,从里面挑选出一组整数,让选择的整数和与剩余的整数和的差最小。

int arr[] = { 12,6,7,11,16,3,9};		//给出的数组
const int length = sizeof(arr) / sizeof(arr[0]);
int x[length] = { 0 };	   // 子集树辅助数组  记录节点走向左孩子还是右孩子,代表i节点被选择&未被选择
int bestx[length] = { 0 }; // 记录最优解
unsigned int min = 0xFFFFFFFF; // 记录最小的差值
int sum1 = 0; // 记录所选子集数字的总和
int sum2 = 0; // 记录未选择数字的和

void fun1(int i)
{
	if (i == length)
	{
		int result = abs(sum1 - sum2);
		if (result < min)
		{
			min = result;
			for (int k = 0; k < length; k++)
			{
				bestx[k] = x[k];
			}
		}
	}
	else
	{
		sum2 -= arr[i];	//更新未选择的总和
		sum1 += arr[i];	//更新选择的总和
		x[i] = 1;
		fun1(i + 1);
		sum2 += arr[i];	//更新没有选择的总和
		sum1 -= arr[i];	//更新选择的总和

		x[i] = 0;
		fun1(i + 1);
	}
}

int main()
{
	//未选择的数字和
	for (int v : arr)
	{
		sum2 += v;
	}
	fun1(0);
	for (int i = 0; i < length; ++i)
	{
		if (bestx[i] == 1)
		{
			cout << arr[i] << " ";
		}
	}
	cout << endl;
	cout << "min:" << min;
	return 0;
}

那么我们如何提高子集树的效率呢?

我们要想提高子集树的效率就必须对子集树进行剪枝操作,也就是避免无效的搜索。

接下来我们对上面的题进行改变:给定2N个整数,从里面挑选出N个整数,让选择的整数和与剩余的整数和的差最小。

int arr[] = { 12,6,7,11,16,3,8,4 };
const int length = sizeof(arr) / sizeof(arr[0]);
vector<int> x;  // 记录子集中选择的元素
vector<int> bestx; // 记录最优解
int sum = 0; // 记录子集中所选数字的和
int r = 0; // 记录未选择数字的和
unsigned int min = 0xFFFFFFFF; // 记录最小差值
int leftcnt = length; // 记录未处理的数字的个数
// int cnt = 0; // 记录遍历的子集的个数,用于测试

void func(int i)
{
	if (i == length)
	{
		// cnt++;
		// 得到子集树的一个解,对应一个叶子节点
		int result = abs(sum - r);
		if (result < min)
		{
			min = result;
			bestx = x;
		}
	}
	else
	{
		leftcnt--;   // 表示处理i节点,表示剩余的未处理的元素的个数
		if (x.size() < length / 2) // 剪左树枝,提高算法效率。选择数字的前提:还未选择够n个整数
		{
			sum += arr[i];
			r -= arr[i];
			x.push_back(arr[i]);
			func(i + 1); // 遍历i的左孩子,表示选择i号位元素
			sum -= arr[i];
			r += arr[i];
			x.pop_back();
		}
		
		// 这里右树枝可不可以剪枝呢? 已选择的数字的个数 + 未来能选择的所有的数字的个数(i+1,i+2....n) >= n个元素
		if (x.size() + leftcnt >= length / 2)
		{
			func(i + 1); // 遍历i的右孩子,表示不选择i号位元素
		}

		// 当前i节点已处理完成,回溯到其父节点了
		leftcnt++;
	}
}
int main()
{
	for (int v : arr)
	{
		r += v;
	}
	func(0);
	for (int v : bestx)
	{
		cout << v << " ";
	}
	cout << endl;
	cout << "min:" << min << endl;
	//cout << "cnt:" << cnt << endl;
	return 0;
}

挑选数字

有一组整数,请挑选出一组数字,让他们的和等于指定的值,存在解打印,不存在打印

int arr[] = { 4,8,12,16,7,9,3 };		//给出的数组
const int length = sizeof(arr) / sizeof(arr[0]);
int add = 18;	//要等于的和
vector<int> x;	   // 子集树辅助数组  记录节点走向左孩子还是右孩子,代表i节点被选择&未被选择
int sum1 = 0; // 记录所选子集数字的总和
int r = 0; // 记录未处理数字的和
int cnt = 0;

void funn(int n)
{
	if (n == length)
	{
		cnt++;
		if (sum1 != add)
		{
			return;
		}
		for (int v : x)
		{
			cout << v << " ";
		}
		cout << endl;
	}
	else
	{
		r -= arr[n]; // 处理当前i节点
		if (sum1+arr[n] <= add)	 // 剪左树枝   已选择的数字的和+即将要选择的数字
		{
			sum1 += arr[n];
			x.push_back(arr[n]);
			funn(n + 1);
			x.pop_back();
			sum1 -= arr[n];
		}

		if (sum1 + r >= add) // 剪右树枝   已选择的数字的和+剩余的可以被选择的数字的和
		{
			funn(n + 1);
		}

		r += arr[n];
	}
}

int main()
{
	for (int v : arr)
	{
		r += v;
	}
	funn(0);
	cout << cnt;
	return 0;
}

用穷举法进行多叉树的选择

int arr[] = { 4,8,12,16,7,9,3 };
const int length = sizeof(arr) / sizeof(arr[0]);
int number = 18;
vector<int> vec; // 存放选择的数字
void func(int i, int number)
{
	if (number == 0)
	{
		for (int v : vec)
		{
			cout << v << " ";
		}
		cout << endl;
	}
	else
	{
		// 以当前节点开始,把剩余元素的孩子节点生成
		for (int k = i; k < length; ++k)
		{
			if (number >= arr[k]) // 剩余的元素小于number(待组成的元素值)
			{
				vec.push_back(arr[k]);
				// 不允许重复选择元素
				func(k + 1, number - arr[k]); // 遍历孩子节点,arr[k]的孩子节点
				vec.pop_back();
			}
		}
	}
}
int main()
{
	func(0, number);
	return 0;
}

0-1背包问题

有一组物品,其重量分别是:w1,w2,…wn,其价值分别是v1,v2,…,vn,现在有一个背包,其容量是C,
问怎么把物品装入背包,能够使背包的价值最大化?

int w[] = { 12,5,8,9,6 };  // 物品的重量
int v[] = { 9,2,4,7,8 };  // 物品的价值
int ccc = 20;		//背包的容量
const int length = sizeof(w) / sizeof(w[0]);
vector<int> x; // 选择的物品
vector<int> bestx; // 记录最优选择的物品
int sum = 0;		//选择的重量
int aaa = 0;		//选择的重量价值
int best = 0;		//最重的重量
int r = 0;			// 未处理物品的总价值

void fun(int i)
{
	if (i == length) 
	{
		if (best < aaa)
		{
			best = aaa;
			bestx = x;
		}
	}
	else
	{
		r -= v[i];
		if (sum+w[i] <= ccc)    // 已选择物品的重量 + 即将选择的第i号物品的重量
		{
			sum += w[i];
			aaa += v[i];
			x.push_back(w[i]);
			fun(i + 1); 
			aaa -= v[i];
			sum -= w[i];
			x.pop_back();
		}

		if (r + aaa > best)      //aaa + [i+1,i+2.....n]总价值 > bestv
		{
			fun(i + 1);
		}

		r += v[i];
	}

}

int main()
{
	for (int val : v)
	{
		r += val;
	}

	fun(0);

	for (int w : bestx)
	{
		cout << w << " ";
	}
	cout << endl;
	cout << "best:" << best << endl;
	return 0;
}

解空间-排列树

如何求一个序列的全排列???

void swap(int arr[], int i, int j)
{
	int tmp = arr[i];
	arr[i] = arr[j];
	arr[j] = tmp;
}
void func(int arr[], int i, int length)
{
	if (i == length)
	{
		for (int j = 0; j < length; ++j)
		{
			cout << arr[j]<<" ";
		}
		cout << endl;
	}
	else
	{
		// 生成i节点的所有孩子节点
		for (int k = i; k < length; ++k)
		{
			swap(arr, i, k); 
			func(arr, i + 1, length);
			swap(arr, i, k); // 一定要再交换回来
		}
	}
}
int main()
{
	int arr[] = { 1,2,3,4 };
	int length = sizeof(arr) / sizeof(arr[0]);
	func(arr, 0, length);

	return 0;
}

八皇后问题求解

以国际象棋为背景的问题:有八个皇后(可以当成八个棋子),如何在 8*8 的棋盘中放置八个皇后,使得任意两个皇后都不在同一条横线、纵线或者斜线上。

int cnt = 0; // 统计8皇后的排列次数
void swap(int arr[], int i, int j)
{
	int tmp = arr[i];
	arr[i] = arr[j];
	arr[j] = tmp;
}
bool judge(int ar[], int i)  // i表示当前放置皇后棋子的位置
{
	for (int j = 0; j < i; ++j)
	{
		if (i == j || ar[i] == ar[j] || abs(i - j) == abs(ar[i] - ar[j]))
		{
			return false;
		}
	}
	return true;
}
void func(int ar[], int i, int length)
{
	if (i == length)
	{
		cnt++;
		for (int j = 0; j < length; ++j)
		{
			cout << ar[j] << " ";
		}
		cout << endl;
	}
	else
	{
		for (int k = i; k < length; ++k)
		{
			swap(ar, i, k);
			if(judge(ar, i)) // 判断第i个位置的元素,是否满足8皇后的条件   0 - i-1
				func(ar, i + 1, length);  // 生成孩子节点,也就是说会生成一系列的排列方式
			swap(ar, i, k);
		}
	}
}
int main()
{
	// 把ar数组的下标当作行,下标对应的元素的值当作列
	int ar[] = { 1,2,3,4,5,6,7,8 };
	int n = 8;
	func(ar, 0, n);
	cout << "cnt:" << cnt << endl;
	return 0;
}

 

    原文作者:爬比我。
    原文地址: https://blog.csdn.net/qq_41078889/article/details/108910935
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞