您的位置:首页 > 其它

子集与子集和问题(Subset sum)的递归回溯解

2011-05-11 22:57 423 查看
所谓子集,是一个数学中的概念。例如一个集合S = {1,2,3,4,5},那么X = {1,3,5}就是它的一个子集,1+3+5等于9就是对应于X的一个子集和。其实子集对于一个数组来说,就是相当于一个子序列(不是子数组,因为子序列意味着可以不连续,而子数组往往是连续的);那么子集和也就是子序列和。

另外,子集问题需要与数学中的“排列”问题区分开来。因为子集往往是无序的,但排列是需要考虑顺序的;所以子集问题常常只是一个“组合”问题,而不是“排列”问题。

从另一个角度讲,这种子集和问题是一种背包问题的特例。

【问题1】

下面举一个退化(之所以说退化,因为这里的子集的大小是2,比较特殊)的子集和问题的例子,出自《编程之美》P.178的问题:快速寻找满足条件的两个数。

问题如下:给定一个数M,在数组arr中寻找两个数,使得这两个数之和等于M。

最初的想法就是:把数组中任意两个数的和求出来(复杂度是N^2),然后在这个和的数组中遍历,查找是否存在等于M的数。

但是这个问题还有更巧妙的方法,采用“变治”:将问题转化为另一个问题再求解。第一种变治方法就是将求两个数的和,转化为是否能在数组中找到M减去数组任意元素的差。但是这种方案没有降低任何复杂度。于是想到了第二种“变治”方法:对数组进行预处理排序,然后从数组的两头用两个指针开始向中间搜索(如果两个指针所指的和小于M,则左指针右移;反之则右指针左移)。

【问题2】

还是《编程之美》中的“数组分割”问题,详见P207。

【问题3】

输出一个集合的所有子集,也就是一个集合的所有组合(你想到了什么?)。

也有递归的解法,这里的递归思想就是:

n个数,每个数取或不取,
f(判断第i个数)
{
若n个数都判断完(i>=n),输出刚才所取的数,返回。
否则:
不取第i个数,
f(判断第i+1个数)
取第i个数,
f(判断第i+1个数)
}
代码如下:

#include <iostream>
#include <vector>

using namespace std;

void all_subset( int arr[], unsigned int size, vector<bool>& contains, int depth )
{
//when reach the needed length, output
if ( depth == size )
{
for( int j = 0 ; j < size ; j++ )
{
if( contains[j] )
cout<<arr[j]<<" ";
}
cout<<endl;
}
else
{
// generate the result that doesn't contain arr[depth]
contains[depth] = false;
all_subset( arr, size, contains, depth+1 );
// generate the result that contains arr[depth]
contains[depth] = true;
all_subset( arr, size, contains, depth+1 );
}
return;
}

int main()
{
int s[] = { 1, 2, 3, 4, 5 };
int size = sizeof(s)/sizeof(int);
vector<bool> contains( 5, false );
all_subset( s, 5, contains, 0 );
}
用形象的“状态空间树”来表示上面的过程,如下:



我们假设对于一个集合生成所有子集的函数为F。那么F(1,2,3,4,5)将由两种可能组成:(1)对除1之外的元素组成的集合施加F;(2)对必然包含1在内的所有元素组成的集合施加F。注意:这两种情况是互斥的,所以不可能有情况重复!然后继续递归下去,就能生成最后结果。

===============================

当然这个问题也有另外一种解法:我们知道一个集合的子集的个数就等于其所有组合之和,即任选1个元素的集合个数+任选2个元素的集合个数+任选3个元素的集合个数+......+任选N个元素的集合个数,最后结果呢是2的N次方个。既然是2的N次方,我们就可以用二进制位表示,如果某位为1,则表示这个集合中含有这一位所代表的元素。例如一个集合是{1,2,3,4,5},则二进制10011就表示这个集合为{1,4,5}。这个代码我就不写了,然后将这个数每次加1,只要判断某位是否为1即可。

【问题4】

子集和问题。题目如下:

给定n 个整数的集合X={x1,x2,......,xn}和一个正整数y,编写一个回溯算法,
在X中寻找子集Yi,使得Yi中元素之和等于y。
下面是回溯法的代码:

#include <iostream>
#include <vector>
#include <algorithm>
#include <string>

using namespace std;

//find if the given subset sum exists.
int findSubsetSum( vector<int>& arr, int given, vector<bool>& included )
{
int cur = 0; // 指向当前值.
int sum = 0; // 当前子集合和.

while( cur >= 0 )
{
//if current one is not included
if( false == included[cur])
{
//include current one
included[cur] = true;
sum += arr[cur];
//find the given subset sum
if( sum == given )
{
return 1;
}
else if( sum > given ) //exceed the given sum
{
included[cur] = false;
sum -= arr[cur];
}
cur++;
}
//backtrace
if( cur >= arr.size() )
{
/*
** 下面两个循环依次排除匹配不成功的结果中
** 包括在结果内以及不包括在结果内的元素
** 直到找到下一个包括在结果内的元素
** 例如:用1和0表示包括和未包括
** 若结果为1110011,则第三个1为所找元素
** 将其变为0,但是从第4个0开始遍历。
**/
while( true == included[cur-1] )
{
cur--;
included[cur] = false;
sum -= arr[cur];
//backtrace to the head
if(cur < 1)
return 0;
}
while( false == included[cur-1] )
{
cur--;
if( cur < 1 )
return 0;
}
//change the status of current - 1,not current!
included[cur-1] = false;
sum -= arr[cur-1];
}
}
return 0;
}

int main()
{
int arr[] = { 2,5,15,8,20 };
vector<int> v( arr, arr + sizeof(arr)/sizeof(int) );
vector<bool> included( sizeof(arr)/sizeof(int), false );
int given = 33;//5+8+20

if( findSubsetSum( v, given, included ) )
{
vector<bool>::iterator iterb = included.begin();
vector<int>::iterator iteri = v.begin();
for( ; iterb != included.end() ; iterb++,iteri++ )
{
if( *iterb )
cout<<*iteri<<endl;
}
}
}
这个问题还有动态规划的方法,以后补充上。

通过 Wiz 发布
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: