问题描述:给定一组2n个整数,从里面选出n个整数,让选择的整数的和与剩下的整数的和的差最小。
解题思路:使用子集树,左子树代表选择该节点,右子树代表未选择该节点,当递归遍历到叶子节点的时候,判断此时选择节点的数量是否为n,如果为n,将选择的节点和与未选择的节点和做差,选择最小的即可。如果不为n,返回遍历另一条路径。
代码如下:(在代码中加了cnt变量,用来测试遍历叶子节点的次数。)
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int arr[] = { 12, 6, 7, 11, 16, 3, 8,4 };
const int length = sizeof(arr) / sizeof(arr[0]);
int sum = 0;//选择的数字的和
int r = 0; //未被选择的数字的和
vector<int> x;
vector<int> bestx;
unsigned int min = 0XFFFFFFFF; //记录最小差值
int cnt = 0;//记录遍历叶子节点的次数 用于测试代码效率
void func(int i)
{
if (i == length)//遍历到叶子节点
{
cnt++;
if (x.size() != length / 2) //判断当前选择的整数个数是否为n
{
return;
}
int result = abs(sum - r);
if (result < min)
{
min = result;
bestx = x;
}
}
else
{
//递归
sum += arr[i];
r -= arr[i];
x.push_back(arr[i]);
func(i + 1);//遍历左子树,选择节点
//回溯
x.pop_back();
sum -= arr[i];
r += arr[i];
func(i + 1);//遍历右子树,未选择节点
//当前节点已经处理完成,回溯到其父节点
}
}
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;
system("pause");
return 0;
}
运行结果:
此时的cnt=256,说明我们将每一个叶子节点都遍历到了,其实我们可以思考一下,有些路径是没有必要遍历完的,在遍历左子树的时候,当前已经选择的节点个数若是大于length/2时就直接不往下递归了,在遍历右子树的时候,当已经选择的节点+未来可能被选择的节点的和如果小于length/2就不往下进行递归了 。这就是我们在回溯算法讲解最开始提到的剪枝操作。加上剪枝操作后,我们来看看代码运行如何:
修改后代码:
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
int arr[] = { 12, 6, 7, 11, 16, 3, 8,4 };
const int length = sizeof(arr) / sizeof(arr[0]);
int sum = 0;//选择的数字的和
int r = 0; //未被选择的数字的和
vector<int> x;
vector<int> bestx;
unsigned int min = 0XFFFFFFFF; //记录最小差值
int cnt = 0;//记录遍历叶子节点的次数 用于测试代码效率
int leftcnt = length;//记录未被选择的节点个数
void func(int i)
{
if (i == length)//遍历到叶子节点
{
cnt++;
if (x.size() != length/2) //判断当前选择的整数个数是否为n
{
return;
}
int result = abs(sum - r);
if (result < min)
{
min = result;
bestx = x;
}
}
else
{
leftcnt--;
if (x.size() < length / 2) //对左子树进行剪枝操作:当前已经选择的节点个数若是大于n时就直接不往下递归了
{
//递归
sum += arr[i];
r -= arr[i];
x.push_back(arr[i]);
func(i + 1);//遍历左子树,选择节点
//回溯
x.pop_back();
sum -= arr[i];
r += arr[i];
}
if (x.size() + leftcnt >= length / 2) //对右子树进行剪枝操作:当已经选择的节点+未来可能被选择的节点的和如果小于length/2就不往下进行递归了
{
func(i + 1);//遍历右子树,未选择节点
}
//当前节点已经处理完成,回溯到其父节点
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;
system("pause");
return 0;
}
运行结果:
此时cnt=70,我们可以明显感觉到修改后的代码将效率提高了1/3。