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

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

用形象的“状态空间树”来表示上面的过程,如下:

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

我们假设对于一个集合生成所有子集的函数为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;
         }
     }
}

这个问题还有动态规划的方法,以后补充上。






你可能感兴趣的:(set)