整数选择问题:给定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++;
//得到子集树的一个解,对应的是一个叶子节点
//x存储的是所选择的个数,如果小于全部数据的一半,不合题意
if (x.size() != length / 2)
{
return;//直接结束,因为这种解不满足题目要求
}
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个元素
//右树枝表示不选择,但是右孩子的左孩子可以被选。走右边是要能选够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;
}
算法优化分析
如果不进行优化,我们用cnt变量测试一下
2^8=256,这个子集树有256个叶子节点,把所有的叶子节点全部都遍历了1遍!但是题目给的条件:2n个整数,从里面挑选出n个整数!
选n+1个可以吗?不可以!
选n-1个可以吗?不可以!
这样就不用去选择新的数字了!
子集的个数已经超过n个了,就不用跑去叶子节点了,浪费算法效率了。
优化方法:
优化1:剪左树枝,提高算法效率。选择数字的前提:还未选择够n个整数
从256优化到163了。
优化2:这里右树枝可以剪枝! 已选择的数字的个数 + 未来能选择的所有的数字的个数(i+1,i+2…n) >= n个元素,才去当前节点的右子树走。如果小于n个元素,就不走右边去了。
因为右树枝表示不选择,但是右孩子的左孩子可以被选。前提是走右边是要能选够n个元素,才去走。
未来能选择的所有的数字的个数表示:右孩子的未处理的个数。
因为每个元素都有选择和被选择两种情况,对应的是左右孩子节点。
int leftcnt = length;//记录未处理的数字的个数,刚开始是一个都没有处理
//所以初始化为数组的元素个数
256-70了
算法提高了3倍效率