所谓子集,是一个数学中的概念。例如一个集合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; } } } |