子集Subsets(组合)问题
从n个数中任取k个数,问有多少种取法。(其中k ≤ n)
这是典型的组合问题,对这种问题可以直接用现成的数学公式(m=(n-k+1)!/k!)来求得。如果要用递归来构建此问题的算法则可以作如下思考:
设从n个数中取k个数组成数集记为g,则有,|g| = k;如果S表示所有的g的集合则有,S={g}。如果从n个数中任意指定一个数t,那么S = Sit ∪Sot,其中,Sit={g| t∈g},Sot={g| t不属于g}。设|Sit| = mit,|Sot| = mot,那么很明显有,|S| = m 且 m = mit + mot,其中 m=(n-k+1)!/k!。
选取的t将所有的组合结果分为了两类,一类包含t,一类不包含t。对于包含t 的g组成的集合Sit,该如何求得其中的元素个数mit?因为每个元素g都包含了t,所以t 实际上对mit 的大小没有影响,影响mit大小的是g中去除了t 以后的所有元素的组合数。所以对mit 来说问题就变成了,求从n-1(去除t)个数中任选k-1(去除t)个数的组合值。找到递归结构。这一半的递归由于问题规模n会不断缩小,所选值也会不断缩小,因而其最终会落在Base Case k==0 中。 对于不包含t 的g组成的集合Sot,又该如何求得其中元素个数mot?由于每个元素g都不包含t,而要选的数仍为k个,所以对mot来说问题变成了,求从n-1个数中任选k个数的组合值。找到另一半的递归结构。这一半的递归由于问题规模不断缩小且k值不变,所以最终会落在Base Case k==n 上,因此算法为:
int c(int n, int k)
{
if (k==0 || k==n)
return 1;
else
return c(n-1, k) + c(n-1, k-1);
}
另一个典型的子集(组合)问题是,输入一个字符串比如“abcd”,假设字符串中所有字符可以任意组合,要求输出它们可能的所有的组合方式。对这一问题的思考方式与上面的相同,首先从中输入中任选一个字符,那么可以把所有的组合的字符串分为两类,包含此字符的与不包含此字符的。包含此字符的字符串的剩余部分的组合,要从剩余的3(n-1)个字符中产生,不包含此字符的字符串全部由剩余的3(n-1)个字符中产生。也就是要在剩余的字符中求所有满足要求的组合方式。递归结构找到。随着问题规模n的不断缩小,最后两者都会终止于Base Case n==0,因此有算法:
void RecSubsets(string soFar, string rest)
{
if ( rest == "")
cout << soFar << endl;
else
{
RecSubsets(soFar + rest[0], rest.Substr(1)); //rest[0] 表取字符串中第一个字符,rest.Substr(1)表返回字符串rest的一个子串,子串从rest的第二个字 //符开始直到原字符串结束为止。
RecSubsets(soFar, rest.Substr(1));
}
}
需要注意的是,第一个组合问题与第二个组合问题略有不同。
还有比较经典的背包问题(knapsack filling)也属于子集(组合)问题。
一个有限容积的背包,n个有体积不一价值不同的物品,求算法,使背包中装的物品价值最大化。