虽然Hanoi塔为递归的强大提供了很好的例证,但其作为一个例子,它有效性却受到缺乏实际应用的影响。策略之所以被许多人应用到编程中,是因为它能够解决很多实际问题。如果递归的唯一例子就是像Hanoi塔一样(很容易得出结论),那么递归就仅仅仅是用于解决抽象谜题。但是没有什么东西会离事实很远。其实递归策略为实际问题提出了非常有效的解决方案(最有代表性的是我们以后讲到的的排序问题,因为这一类问题很难以用其他方式解决)。
这次我们谈论到的是被称为子集和(The subset-sum problem)的问题,我们可以根据下列来定义这个问题:
Given a set of integers and a target value, determine whether it is possible to find a subset of those integers whose sum is equal to the specified target.
给定一组整数和目标值,判断是否可以找到在这组数据的子集中找到总和等于指定目标整数的子集。
例如,给定你一个集合{ –2, 1, 3, 8 },以及目标值7。这样的组合是存在子集和的,其中的一个答案(假设我们现在还不知道有具体多少个)是{ –2, 1, 8 },因为 -2 + 1 + 8 = 7.符合要求。但是如果要求的目标值为5,那么我们就找不到这样的子集了。
那么,我们可以很容易的将问题转换为C++语言,无非就是要求写一个函数(我们暂且命名为subsetSumExists),然后用来判断是否存在问题。我们可以写出函数的原型(这一步我们之前提到过,对集合操作肯定参数包含集合,要跟目标值比较,肯定还要有目标值):
bool subsetSumExists(set<int> & integerSet, int target);
如果我们可以找到这样的子集,我们就返回true,否则就返回false。
即使我们第一眼看着似乎子集合问题与Hanoi塔一样,但是它在计算机科学的理论和实践中都具有重要意义。正如你以后会在排序算法看到那样,子集和问题是难以有效解决的重要类别的计算问题的一个实例。 然而,事实上,子集合的问题通常用于信息保密的应用中。例如,公钥密码学的第一个实现是使用子集和问题的变体作为其数学基础。将操作放在难以解决的问题上,现代加密策略将会变得更难破解。
使用传统的迭代方法是难以解决子集和问题的。为了取得进展,我们需要递归地思考这个问题。跟以前一样,我们需要先确定simple case,跟recursive decomposition。 在使用集合的应用程序中,simple case几乎总是当该集合为空时。因为如果该集合为空,则除非目标为零,否则无法添加元素以生成目标值。该发现表明,subsetSumExists的代码将像这样开始:
bool subsetSumExists(set<int> & integerSet, int target) {
if (integerSet.empty()) {
return target == 0;
} else {
Find a recursive decomposition that simplifies the problem.
}
在这个问题上,最难但也是最重要的就是寻找到递归分解(recursive decomposition)。
当我们在寻找递归分解时,需要注意输入中的一些值(这些值作为参数在C++中解决问题),我们可以使其变小。在这个题目中,我们需要做的是使集合更小,因为我们尝试做的是移动到当集合为空时发生的简单情况。如果从元素中取出一个元素,剩下的是一个更小的集合,由set类导出的操作可以轻松地从一个集合中选择一个元素,然后确定剩下的元素。 您需要的是以下代码:
int element = set.first();
Set<int> rest = set - element;
这两句代码的含义就是,先记录下集合的第一个元素,然后减去这个元素,所得的集合就是剩下的集合。(当然我们前面提到过我们在集合类中是没有 - 号的运算的,所以我们可能用到< algorithm >的一些算法。
然而,使集合更小,却还不足以解决这个问题。我们将集合分成更小部分的代码,和集合的其余部分将在许多递归应用程序中再次出现。在结构上,我们知道subsetSumExists必须在现在存储在变量rest中的较小集合上递归调用自身。我们现在还没有确定的是,这些递归子问题的解决方案是如何解决原始问题的。
解决这个问题的关键是我们要注意到,在识别特定元素之后,我们可以通过两种方式生成所需的目标总和。第一中可能是我们要查找的子集包含该元素。为了考虑发生这种情况,必须可以存取该集合的其余部分,并产生一个值target - element。另一种可能性是要查找的子集不包括该元素,在这种情况下,只能使用剩余的元素集来生成值目标。但这些就足以完成subsetSumExists的实现,代码如下所示
#include
#include
using namespace std;
bool subsetSumExists(set<int> &integerSet, int target);
int main(){
set<int> integerSet;
set<int>::iterator it;
while(true){
cout << "输入集合放入个数 ";
int n; cin >> n;
cout << "输入目标值 ";
int target; cin >> target;
cout << "输入集合" << endl;
for(int i = 0; i < n; i++){
int k; cin >> k;
integerSet.insert(k);
}
if(subsetSumExists(integerSet, target)){
cout << "存在这样的集合" << endl;
}else{
cout << "No find" << endl;
}
}
return 0;
}
bool subsetSumExists(set<int> &integerSet, int target){
set<int>::iterator it;
it = integerSet.begin();
if(integerSet.empty()){
return target == 0;
}else{
integerSet.erase(it);
return subsetSumExists(integerSet,target)||
subsetSumExists(integerSet,target - (*it));
}
}
因为这个递归策略将一般情况细分为包含了一个特定的元素的分支,另一个分支排除它,这个策略被称为包含/排除模式(The inclusion/exclusion pattern)。后续练习,我们会发现,这种模式有轻微的变化,可以在许多不同的应用程序中出现。 虽然这种模式在使用集合时最容易识别,但它也涉及到涉及Vector和字符串的应用程序,我们也应该在这些情况下使用它。