n个元素的集合{1,2,, n }可以划分为若干个非空子集。例如,当n=4 时,集合{1,2,3,4}可以划分为15 个不同的非空子集如下:
{{1},{2},{3},{4}},
{{1,2},{3},{4}},
{{1,3},{2},{4}},
{{1,4},{2},{3}},
{{2,3},{1},{4}},
{{2,4},{1},{3}},
{{3,4},{1},{2}},
{{1,2},{3,4}},
{{1,3},{2,4}},
{{1,4},{2,3}},
{{1,2,3},{4}},
{{1,2,4},{3}},
{{1,3,4},{2}},
{{2,3,4},{1}},
{{1,2,3,4}}
给定正整数n,计算出n个元素的集合{1,2,, n }可以划分为多少个不同的非空子集。
所求的是Bell 数 满足递推公式 B(n) =
所以这道题实际求第二类Stirling数 S(n,m)
解决思想:
1. 若 m == 1 则S(n,m) = 1; ( n >= 0 )
2. 若 m == 0 则S(n,m) = 0; ( n >= 1 )
3. 若 n == m 则S(n,m) = 1;
4 . 若非以上3中情况 则:
(1) 已知前S(n-1,m)的解决方案 , 第n个数可以向分出来的m个非空子集中的每一个任意添加进去 共有m*S(n-1,m)方法
(2) 前n-1个数已经划分成 m-1 剩余第n个数单独作为一个非空子集添加进去 共有S(n-1,m-1)种方法
#include <iostream>
using namespace std;
int f( int n , int m ){
if( n == m && n >= 0 )
return 1;
if( n >=1 && m == 0 )
return 0;
else
return m*f(n-1,m)+f(n-1,m-1);
}
//Bell数
int main()
{
int n;
cin >> n;
int sum = 0;
for( int i = 1 ; i <= n ; i ++ ){
//n次循环第二类Stirling数
sum += f(n,i);
}
cout << sum << endl;
return 0;
}
对于递归中数据量为n 的时候 一般情况下写出的递归公式里面一定含有n 或者有其他题目中的参数 例如此题中的m
递归最重要的思想: 至顶向下 假设已知前n-1时的解决方案 有了第n-1是的解决方案 才能求第n个 。 要求第n个时 可能有多种情况(每一种情况都能列出一个递归公式) 最后将多种情况的结果相加即最终答案 所谓的终止条件即: 当递归回溯到了人脑可以算出的小数据量的时候, "例如" 当n到了1 那么可以知道n=2时的结果 知道2的结果可求3 继而递推到n。
这里面的回溯和搜索里面的回溯虽然名字相同 但是意义略有不同 搜索里面的回溯一般是一种不断去尝试的过程 例如N皇后问题 一遍一遍的去尝试 尝试失败就回来换个方向尝试 直到找出答案。
而递归中的回溯 更像一种要得到那种终止条件不得已的将问题规模缩小 当得到答案后 再一层一层扩大直到得出最终答案。
递归的两种结题技巧:
1. 回溯到底得到临界条件 利用临界条件递推到 规模为n的为题。 例如:
int find(int x) //查找根结点
{
int temp;
if(x == p[x].pre)
return x;
temp = p[x].pre; //路径压缩
p[x].pre = find(temp);
p[x].relation = (p[x].relation + p[temp].relation) % 3; //关系域更新
return p[x].pre; //根结点
}
这是一段带权并查集的路径压缩算法。 在这段代码中分为两部分, find的函数中间截止 以上回溯,以下递推 画图即可理解。
2. 在回溯的同时解决问题, 可能会不断的进入临界条件继而返回,例如图的遍历, 但是图的遍历更像一种 一边回溯一边递推一边求解三者同时进行的思维模式。 这里的递推没有具体值 是状态量。 例子代码后粘。