递归与分治策略
基本题一:全排列问题
一、实验目的与要求
-
熟悉C/C++ C#语言的集成开发环境;
-
通过本实验加深对递归过程的理解。
二、实验题目
设计一个递归算法生成 \(n\) 个元素 \(\{r_1,r_2,…,r_n\}\) 的全排列。
三、实现思想
基本思想:利用递归,一层确定一个数字,到达递归边界时输出当前记录的结果。
过程举例:模拟往 n 个位置上填 r_1 \sim r_n 数字的过程。
考虑 N=3 ,代表了有 A, \ B,\ C 三个空位置和写有 1,\ 2, \ 3 的卡片,我们需要将卡片放到空位置上,并且每个位置只能放一张卡片,现在我们需要找出这 3 张卡片的所有不同摆放方法。
在某一个位置,考虑应该先放哪张卡片的时候,我们可以约定一个先后顺序 1 \rightarrow 2 \rightarrow 3。
根据约定顺序放置卡片,首先可以得到:A(1),B(2),C(3),这时我们已经得到了一种全排列 (1, 2, 3)。而现在我们实际上已经走到了一个并不存在的位置D(即结束位置,但是我们排列并没有结束)。
现在我们需要立即回到位置 C,取回卡片 3,然后尝试是否还能放入其它卡片(当然是按照我们的约定顺序),结果显然是我们手里并没有其它可以放入的卡片。
所以,我们需要继续回退一步来到位置 B,取出卡片 2,然后继续尝试是否能放入其它卡片(当然是按照我们的约定顺序),这时我们发现可以放入卡片 3;放入卡片 3 后,我们向前一步来到位置 C,尝试放入卡片(当然是按照我们的约定顺序),这时我们发现可以放入卡片 2;放完卡片 2,我们继续向前一步来到了并不存在的位置 D;这时我们得到了另一种全排 (1, 3, 2);
... ...
按照上述步骤重复下去,就可以得到所有的全排列。
四、遇到的困难和收获
困难:总体思路都已明确,但一开始代码实现起来还有点难度,递归函数的参数和递归边界未能确定,回溯过程中恢复现场也未能理解。
收获:经过手动模拟递归的过程,加深了对递归算法的理解,学会了在回溯中恢复现场的思想。
五、实现代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 20;
int n, a[N]; // 原始数组
int path[N]; // 记录路径
bool vis[N];
void dfs(int idx)
{
if(idx == n)
{
printf("%d", path[0]);
for(int i = 1; i < n; ++i) printf(" %d", path[i]);
puts("");
return;
}
for(int i = 0; i < n; ++i)
{
if(!vis[i])
{
path[idx] = a[i];
vis[i] = true;
dfs(idx + 1);
vis[i] = false; // 恢复现场
}
}
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; ++i) scanf("%d", &a[i]);
sort(a, a + n); // 若按字典序输出,则需提前排序
dfs(0);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
结果:
分析:
- 时间复杂度为 O(n!)
- 空间复杂度为 O(n)
基本题二:整数划分问题
一、实验目的与要求
- 掌握整数划分问题的算法;
- 初步掌握分治算法
二、实验题目
设 n 为正整数,将 n 表示为一系列正整数之和,即 n = n_1+n_2+n_3+….+n_k (n_1 \geq n_2 \geq …. \geq n_k \geq 1) ,正整数 n 的这种表示称为 n 的整数划分。给定整数 n,求正整数 n 的所有的整数划分数。
输入:输入第一行包括一个整数n。
输出:输出一个整数代表结果。
Simple input:
6
Simple output:
11
三、实现思想
与上题的思路一致,递归填数字,所不同的是每次都是从上一个数字开始选取,直到加数 sum 的值等于 n 时才停止,或者当 sum>n 时停止。
四、遇到的困难和收获
- 思路受上一题的限制一直没有想到好的解决方法
- 代码实现时,递归参数的初始值以及下标的处理没有思考清楚
五、实现代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 50;
int n;
int p[N];
// sum记录当前序列和,cur为当前序列元素个数
void dfs(int sum, int cur)
{
if(sum > n) return;
if(sum == n) // 若sum==n,输出序列
{
printf("%d=", n);
for(int i = 1; i < cur; i++) printf("%d+", p[i]);
printf("%d\n", p[cur]);
return;
}
for(int i = p[cur]; i < n; i++)
{
p[cur + 1] = i; // 这个值就是下一层递归时所用到的i的起始值
dfs(sum + i, cur + 1); // 进入到下一层递归
}
}
int main()
{
scanf("%d", &n);
p[0] = 1;
dfs(0, 0);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
结果:
分析:
- 时间复杂度为 O(n^n)
- 空间复杂度为O(n)
基本题三:棋盘覆盖问题
一、实验目的与要求
- 掌握棋盘覆盖问题的算法;
- 初步掌握分治算法
二、实验题目:
棋盘覆盖问题:在一个 2k×2k 个方格组成的棋盘中,恰有一个方格与其它方格不同,称该方格为一特殊方格,且称该棋盘为一特殊棋盘。在棋盘覆盖问题中,要用图示的 4 种不同形态的 L 型骨牌覆盖给定的特殊棋盘上除特殊方格以外的所有方格,且任何 2 个 L 型骨牌不得重叠覆盖。
输入:第一行包括一个整数 k,第二行两个整数 x, y 代表特殊点坐标(从 1 开始)。
输出:输出一个 2k×2k 的矩阵代表结果,0 表示特殊点。
Simple input:
2
1 2
Simple output:
2 0 3 3
2 2 1 3
4 1 1 5
4 4 5 5
三、实现思想
算法设计:
从用 L 骨牌覆盖条件可以得知,在任何一个 2k*2k 的棋盘中,用到的 L 骨牌需要 (4^k-1)/3
(1)当 k>0 时,将 2k*2k 棋盘分隔成为 4 个 2{k-1}*2 子棋盘。
(2)特殊的方格肯定在这 4 个较小的棋盘中,其余 3 个子棋盘肯定没有特殊方格。用一个 L 方格覆盖这 3 个子棋盘的汇合处。
(3)这 3 个子棋盘被覆盖 L 形骨牌就成 3 个有特殊方格棋盘,然后递归这样分隔。直至转化 2*2 棋盘。
四、遇到的困难和收获
理解了分治策略的求解过程,但在代码实现时遇到了一个难点:如何判断特殊方格是否在当前棋盘中。
在实验过程中,刚开始对于棋盘划分后的边界计算有错误,由于下标从一开始所以导致了一些错误,经过修改没有问题了,此题就是分块递归解决问题。
收获:最开始不太懂老师课上讲的数字的顺序,通过程序的实现,深入理解明白了为什么会是这样的覆盖,懂了递归的方式,更加理解了此问题。
五、实现代码
#include <iostream>
#include <cstdio>
using namespace std;
int tile = 1; // 骨牌编号
int Board[4][4]; // 棋盘
void ChessBoard(int tr, int tc, int dr, int dc, int size);
int main()
{
ChessBoard(0, 0, 0, 1, 4); // 初始棋盘的左上角为(0,0),特殊方格位置为(2,3)
for(int i = 0; i < 4; i++)
{
for(int j = 0; j < 4; j++) printf("%4d", Board[i][j]);
printf("\n");
}
return 0;
}
// 当前棋盘左上角为(tr,tc),特殊方格的位置为(dr,dc),size为当前棋盘边长
void ChessBoard(int tr, int tc, int dr, int dc, int size)
{
if(size == 1)
{
/*
for(int i = 0; i < 4; i++)
{
for(int j = 0; j < 4; j++) printf("%4d", Board[i][j]);
printf("\n");
}
printf("\n\n");
*/
return;
}
int t = tile++; // L型骨牌编号
int s = size / 2; // 分割棋盘
/* 覆盖左上角子棋盘 */
if(dr < tr + s && dc < tc + s) // 特殊方格在此棋盘中
{
ChessBoard(tr, tc, dr, dc, s);
}
else // 特殊方格不在此棋盘中
{
Board[tr+s-1][tc+s-1] = t; // 用编号为t的骨牌覆盖右下角
ChessBoard(tr, tc, tr+s-1, tc+s-1, s); // 覆盖其余方格
}
/* 覆盖右上角子棋盘 */
if(dr < tr + s && dc >= tc + s) // 特殊方格在此棋盘中
{
ChessBoard(tr, tc+s, dr, dc, s);
}
else // 特殊方格不在此棋盘中
{
Board[tr+s-1][tc+s] = t; // 用编号为t的骨牌覆盖左下角
ChessBoard(tr, tc+s, tr+s-1, tc+s, s); // 覆盖其余方格
}
/* 覆盖左下角子棋盘 */
if(dr >= tr + s && dc < tc + s) // 特殊方格在此棋盘中
{
ChessBoard(tr+s, tc, dr, dc,s);
}
else // 特殊方格不在此棋盘中
{
Board[tr+s][tc+s-1] = t; // 用编号为t的骨牌覆盖右上角
ChessBoard(tr+s, tc, tr+s, tc+s-1, s); // 覆盖其余方格
}
/* 覆盖右下角子棋盘 */
if(dr >= tr + s && dc >= tc + s) // 特殊方格在此棋盘中
{
ChessBoard(tr+s, tc+s, dr, dc, s);
}
else // 特殊方格不在此棋盘中
{
Board[tr+s][tc+s] = t; // 用编号为t的骨牌覆盖左上角
ChessBoard(tr+s, tc+s, tr+s, tc+s, s); // 覆盖其余方格
}
}
六、实验结果与分析(时间复杂度和空间复杂度)
结果:
分析:
- 时间复杂度为T(k)=\begin{cases}O(1), & k=1,\4T(k-1)+O(1), & k>0 \end{cases},渐进意义下的最优算法T(n)=O(4^k)
- 空间复杂度为O(2^{2k})
基本题四:合并排序
一、实验目的与要求
- 熟悉合并排序算法;
- 初步掌握分治算法;
二、实验题目
采用递归与非递归两种方式实现合并排序算法。
输入:输入第一行包括一个整数 n,第二行包含 n 个整数,以空格间隔。
输出:输出一行 n 个整数,代表排序结果。
Simple input:
4
1 5 4 12
Simple output:
1 4 5 12
三、实现思想
归并排序是一种基于“归并”思想的排序方法,最基本的2-路归并排序的原理是,将序列两两分组,将序列归并为 \lceil \frac{n}{2}\rceil 个组,组内单独排序;然后将这些组再两两归并,生成 \lceil \frac{n}{4} \rceil 个组,组内再单独排序;以此类推,直到只剩下一个组为止。
递归:2-路归并排序的递归写法非常简单,只需要反复将当前区间 [left, right] 分为两半,对两个子区间 [left, mid] 与 [mid + 1, right] 分别递归进行归并排序,然后将两个已经有序的子区间合并为有序序列即可。
非递归:2-路归并排序的非递归实现主要考虑到这样一点:每次分组时组内元素个数上限都是 2 的幂次。
于是就可以想到这样的思路:令步长 step 的初值为 2,然后将数组中每 step 个元素作为一组,将其内部进行排序(即把左 step / 2 个元素与右 step / 2 个元素合并,而若元素个数不超过 step / 2,则不操作);再令 step 乘以 2,重复上面的操作,直到 step / 2 超过元素个数 n 。
四、遇到的困难和收获
归并排序主要就两点:分解和合并。子问题的分解可以递归实现,也可以非递归实现,但是非递归实现时要注意数组下标是从0 \sim n-1 还是 1 \sim n
还有实现归并操作时的代码书写顺序和终止条件。
五、实现代码
递归:
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn = 100;
int a[maxn];
// 将数组 A 的 [L1,R1] 与 [L2,R2] 区间合并为有序区间 (此处 L2 即为 R1 + 1)
void merge(int A[], int L1, int R1, int L2, int R2)
{
int i = L1, j = L2; // i指向A[L1], j指向A[L2]
int temp[maxn], index = 0; // temp临时存放合并后的数组,index为其下标
while(i <= R1 && j <= R2)
{
if(A[i] <= A[j]) // 如果A[i]<=A[j]
temp[index++] = A[i++]; // 将A[i]加入序列temp
else // 如果A[i]>A[j]
temp[index++] = A[j++]; // 将A[j]加入序列temp
}
while(i <= R1) temp[index++] = A[i++]; // 将[L1,R1]的剩余元素加入序列temp
while(j <= R2) temp[index++] = A[j++]; // 将[L2,R2]的剩余元素加入序列temp
for(i = 0; i < index; i++)
A[L1 + i] = temp[i]; // 将合并后的序列赋值回数组A
}
// 将array数组当前区间[left,right]进行归并排序
void mergeSort(int A[], int left, int right)
{
if(left < right) // 只要left小于right
{
int mid = (left + right) / 2; // 取[left,right]的中点
mergeSort(A, left, mid); // 递归,将左子区间[left,mid]归并排序
mergeSort(A, mid + 1, right); // 递归,将右子区间[mid+1,right]归并排序
merge(A, left, mid, mid + 1, right); // 将左子区间和右子区间合并
}
}
int main()
{
int n;
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%d", &a[i]);
mergeSort(a, 1, n);
for(int i = 1; i <= n; ++i) printf("%d ", a[i]);
return 0;
}
非递归:
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
const int maxn = 100;
int n, a[maxn];
// 将数组 A 的 [L1,R1] 与 [L2,R2] 区间合并为有序区间 (此处 L2 即为 R1 + 1)
void merge(int A[], int L1, int R1, int L2, int R2)
{
int i = L1, j = L2; // i指向A[L1], j指向A[L2]
int temp[maxn], index = 0; // temp临时存放合并后的数组,index为其下标
while(i <= R1 && j <= R2)
{
if(A[i] <= A[j]) // 如果A[i]<=A[j]
temp[index++] = A[i++]; // 将A[i]加入序列temp
else // 如果A[i]>A[j]
temp[index++] = A[j++]; // 将A[j]加入序列temp
}
while(i <= R1) temp[index++] = A[i++]; // 将[L1,R1]的剩余元素加入序列temp
while(j <= R2) temp[index++] = A[j++]; // 将[L2,R2]的剩余元素加入序列temp
for(i = 0; i < index; i++)
A[L1 + i] = temp[i]; // 将合并后的序列赋值回数组A
}
void mergeSort(int A[])
{
// step为组内元素个数,step/2 为左子区间元素个数,注意等号可以不取
for(int step = 2; step / 2 <= n; step *= 2)
{
// 每step个元素一组,组内前step/2和后step/2个元素进行合并
for(int i = 1; i <= n; i += step) // 对每一组
{
int mid = i + step / 2 - 1; // 左子区间元素个数为step/2
if(mid + 1 <= n) // 右子区间存在元素则合并
merge(A, i, mid, mid + 1, min(i + step - 1, n));
//左子区间为[i,mid],右子区间为[mid+1, min(i+step-1,n)]
}
}
}
int main()
{
scanf("%d", &n);
for(int i = 1; i <= n; ++i) scanf("%d", &a[i]);
mergeSort(a);
for(int i = 1; i <= n; ++i) printf("%d ", a[i]);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
- 时间复杂度为O(n \log n)
- 空间复杂度为O(n)
基本题五:快速排序
一、实验目的与要求
- 熟悉快速排序算法;
- 初步掌握分治算法;
二、实验题目
实现快速排序算法。
输入:输入第一行包括一个整数 n,第二行包含 n 个整数,以空格间隔。
输出:输出一行 n 个整数,代表排序结果。
Simple input:
4
1 5 4 12
Simple output:
1 4 5 12
三、实现思想
基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
算法流程:
- 从数列中挑出一个基准值。
- 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
- 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。
四、遇到的困难和收获
快速排序的边界问题需要仔细处理,基准值的不同选择方式算法的效率是不同的。
五、实现代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 1e5 + 10;
int q[N];
int n;
void quick_sort(int q[], int l, int r)
{
if(l >= r) return;
int x = q[l + r >> 1], i = l - 1, j = r + 1;
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
quick_sort(q, l, j);
quick_sort(q, j + 1, r);
}
int main()
{
scanf("%d", &n);
for(int i = 0; i < n; ++i)
scanf("%d", &q[i]);
quick_sort(q, 0, n-1);
for(int i = 0; i < n; ++i)
printf("%d ", q[i]);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
- 时间复杂度为 O(nlogn)
- 空间复杂度为 O(nlogn)
基本题六:线性时间选择问题
一、实验目的与要求
- 熟悉线性时间选择问题算法;
- 初步掌握分治算法;
二、实验题目
给定一个包含 n 个元素的乱序数组 a,求这 n 个元素中第 k 小的元素。1 \leq K \leq n \leq 100000
输入:输入第一行包括两个整数 n, k,第二行包含 n 个整数,以空格间隔。
输出:输出一行 1 个整数,代表第 k 小的元素值。
Simple input:
4 2
1 5 4 12
Simple output:
4
三、实现思想
四、遇到的困难和收获
基于快速排序的快速选择算法,同样地,在算法实现时,边界需要仔细考虑。
五、实现代码
#include<iostream>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 100010;
int n, k;
int q[N];
int quick_sort(int l, int r, int k)
{
if(l==r) return q[l];
int x = q[l], i = l - 1, j = r + 1;
while(i < j)
{
while(q[++i] < x);
while(q[--j] > x);
if(i < j) swap(q[i], q[j]);
}
int sl = j - l + 1;
if(k <= sl) return quick_sort(l, j, k);
else return quick_sort(j + 1, r, k - sl);
}
int main()
{
scanf("%d%d", &n, &k);
for(int i = 0; i < n; ++i)
scanf("%d", &q[i]);
printf("%d\n", quick_sort(0, n-1, k));
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
时间复杂度:改进后的随机选择算法最坏时间复杂度为 O(n^2),但是其对任意输入的期望时间复杂度确是 O(n),这意味着不存在一组特定的数据能使这个算法出现最坏情况。
空间复杂度为O(n)
基本题七:平面最近点对问题
一、实验目的与要求
- 掌握最接近点对问题的算法;
- 熟悉分治算法的算法思想
二、实验题目
给定二维平面上乱序的 n (n > 1) 个坐标点,求相距最近的两个坐标点是哪两个点,并且输出最近距离,若答案不唯一,输出任意一组满足情况的结果。
输入:输入第一行包括一个整数 n,第 2 \sim n+1 行,每行两个整数 x, y ,代表一个坐标点,以空格间隔。
输出:输出一行 5 个整数,第一个整数代表最近距离,随后四个点表示两个最近点对的坐标。
Simple input:
3
0 0
1 0
100 10
Simple output:
1 0 0 1 0
三、实现思想
我们已知n个点的坐标,要求距离最近的两个点以及其之间的距离,可以通过以下划分过程
划分过程:
我们可选取一个点,求其左边所有点的最小距离,求其右边所有点的最小距离,还有一种情况就是跨越中间点的两个点的最小距离,由此可以划分处理为三部分。
处理过程:
- 当一段区域内只有两个点时,两个点的距离就是最小距离;
- 当一段区域内有三个点时,我们可分别求得三点间的两两距离,通过比较求得最小值;
- 当区域内大于三个点时,我们又可以找到中间点进行划分递归处理;
- 对于两个点的位置,我们可通过两个全局变量不断更新得到。
四、遇到的困难和收获
在实验中,如何处理跨中间线的两个点的距离是一个问题,我们可通过遍历查找,此部分的代码写的时候有困难,不知道如何处理,此为重难点。
收获:这个题和寻找最大序列和很相似,都需要处理跨过中间分界的进行处理,通过实验,进一步理解了此类题的处理的方式
五、实现代码
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<cstring>
using namespace std;
const double eps = 1e-8;
#define Less(a,b) (((a)-(b))<(-eps))
const int N = 100010;
const double INF = 1e20;
struct Point {
double x, y;
};
Point p[N], tmp[N];
bool cmpx(Point A, Point B) // 对横坐标x排序
{
return Less(A.x, B.x);
}
bool cmpy(Point A, Point B) // 只对y坐标排序
{
return Less(A.y, B.y);
}
double dis(Point A, Point B)
{
return hypot(A.x - B.x, A.y - B.y); // 计算直角三角形的斜边长
}
double closest(int L, int R)
{
double d = INF;
if(L == R) return d; // 只剩1个点
if(L + 1 == R) return dis(p[L], p[R]); // 只剩2个点
int mid = (L + R) / 2; // 分治
double d1 = closest(L, mid); // 求s1内的最近点对
double d2 = closest(mid + 1, R); // 求s2内的最近点对
d = min(d1, d2);
int k = 0;
for(int i = L; i <= R; i++) // 在s1和s2中间附近找可能的最小点对
if(fabs(p[mid].x - p[i].x) <= d) // 按x坐标来找
tmp[k++] = p[i];
sort(tmp, tmp + k, cmpy); // 按y坐标排序,用于剪枝。这里不能按x坐标排序
for(int i = 0; i < k; i++)
{
for(int j = i + 1; j < k; j++)
{
if(tmp[j].y - tmp[i].y >= d) break;
d = min(d, dis(tmp[i], tmp[j]));
}
}
return d; // 返回最小距离
}
int main()
{
int n;
while(~scanf("%d", &n) && n)
{
for(int i = 0; i < n; i++) scanf("%lf%lf", &p[i].x, &p[i].y);
sort(p, p + n, cmpx); // 先排序
printf("%.2f\n", closest(0, n - 1) / 2); // 输出最短距离的一半
}
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
时间复杂度:O(nlogn)
空间复杂度:O(n)
基本题八:集合划分问题
一、实验目的与要求
掌握分治算法
二、实验题目
N 个元素的集合 {1, 2,……,n} 可以划分为若干非空子集。例如,当 n=4 时集合 {1,2,3,4} 可以划分为 15 个不同的非空子集。给定正整数 n,计算出 n 个元素的集合可以划分为多少个非空子集的集合。
三、实现思想
设 n 个元素的集合可以划分为 F(n,m) 个不同的由 m 个非空子集组成的集合。
考虑 3 个元素的集合,可划分为
- 1 个子集的集合:{{1,2,3}}
- 2 个子集的集合:{{1,2},{3}},{{1,3},{2}},{{2,3},{1}}
- 3 个子集的集合:{{1},{2},{3}}
所以,F(3,1)=1;F(3,2)=3;F(3,3)=1
如果要求 F(4,2) 该怎么办呢?
- 往上面第一类里添一个元素 {4},得到 {{1,2,3},{4}}
- 往上面第二类里的任意一个子集添一个 4,得到
- {{1,2,4},{3}},{{1,2},{3,4}}
- {{1,3,4},{2}},{{1,3},{2,4}}
- {{2,3,4},{1}},{{2,3},{1,4}}
所以,F(4,2)=F(3,1)+2F(3,2)=1+23=7
推广得到 F(n,m)=F(n-1,m-1)+m*F(n-1,m)
假设 f(n,m) 表示将 n 个元素的集合划分成由 m 个子集构成的集合的个数,归纳总结:
- 若 m==1,则 f(n,m)=1
- 若 n==m,则 f(n,m)=1
- 若非以上两种情况,f(n,m) 可以由下面两种情况构成
- 向 n-1 个元素划分成的 m 个集合里面添加一个新的元素,则有 m*f(n-1,m) 种方法;
- 向 n-1 个元素划分成的 m-1 个集合里添加一个由一个元素形成的独立的集合,则有 f(n-1,m-1) 种方法。
四、遇到的困难和收获
遇到的困难就是用递归的想法去思考这个问题,用这个方法也要用一次循环相加来求总集合数
熟练了用数学归纳法找规律的方式来求解问题
五、实现代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int f(int n, int m)
{
if(m == 1 || n == m) return 1;
return f(n - 1, m - 1) + f(n - 1, m) * m;
}
int main()
{
int n;
scanf("%d", &n);
int sum = 0;
for(int i = 1; i <= n; i++)
sum += f(n, i);
printf("%d\n", sum);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
- 时间复杂度为O(n^3)
- 空间复杂度为O(n)
基本题九:集合划分问题二
一、实验目的与要求
掌握分治算法
二、实验题目
N 个元素的集合 {1, 2,……,n} 可以划分为若干非空子集。例如,当 n=4 时集合 {1,2,3,4} 可以划分为 15 个不同的非空子集。给定正整数 n 和 m,计算出 n 个元素的集合可以划分为多少个由不同的 m 个非空子集的集合。
三、实现思想
思路分析和上一题完全一致,该题是上一题的一部分。上一题求解的是Bell数,本题求解的是第二类Stirling数,显然,每个贝尔数都是"第二类Stirling数"的和。
四、遇到的困难和收获
解决了上一题之后,该题基本没有什么困难。
五、实现代码
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
int f(int n, int m)
{
if(m == 1 || n == m) return 1;
return f(n - 1, m - 1) + f(n - 1, m) * m;
}
int main()
{
int n, m;
scanf("%d%d", &n, &m);
printf("%d\n", f(n, m));
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
- 时间复杂度为O(n^2)
- 空间复杂度为O(n)
基本题十:整数因子分解问题
一、实验目的与要求
掌握分治
二、实验题目
大于一的正整数 n 可以分解为 n=x_1x_2…*x_m。例如,当 n=12 时,有 8 种不同分解式
对于给定的正整数 n,计算 n 共有多少种不同的分解式。
三、实现思想
基本思路:采用分治策略,将大问题分解为小问题,递归下去进行求解。
比如 12=26,分别对两个子问题进行求解,2=2,6=23;再分别对 2 和 3 进行求解。
四、遇到的困难和收获
递归算法的设计掌握的还是不够好,对于基本的数学知识也不是很熟悉
五、实现代码
#include<iostream>
#include<algorithm>
#include<cstdio>
using namespace std;
int tot;
void solve(int n)
{
if(n == 1) tot++;
else
{
for(int i = 2; i <= n; ++i)
if(n % i == 0) solve(n / i);
}
}
int main()
{
int n;
scanf("%d", &n);
solve(n);
printf("%d\n", tot);
return 0;
}
六、实验结果与分析(时间复杂度和空间复杂度)
- 时间复杂度为O(n)
- 空间复杂度为O(n)
还可以进一步优化,递归过程存在大量重复,可以使用备忘录或者动态规划,从而达到直接使用之前求解结果的目的。

浙公网安备 33010602011771号