递归与分治策略

基本题一:全排列问题

一、实验目的与要求

  • 熟悉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

include

include

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;
}

六、实验结果与分析(时间复杂度和空间复杂度)

结果:

分析:

  1. 时间复杂度为 O(n!)
  2. 空间复杂度为 O(n)

基本题二:整数划分问题

一、实验目的与要求

  1. 掌握整数划分问题的算法;
  2. 初步掌握分治算法

二、实验题目

设 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 时停止。

四、遇到的困难和收获

  1. 思路受上一题的限制一直没有想到好的解决方法
  2. 代码实现时,递归参数的初始值以及下标的处理没有思考清楚

五、实现代码

include

include

include

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)

基本题三:棋盘覆盖问题

一、实验目的与要求

  1. 掌握棋盘覆盖问题的算法;
  2. 初步掌握分治算法

二、实验题目:

棋盘覆盖问题:在一个 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

include

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); // 覆盖其余方格
}
}

六、实验结果与分析(时间复杂度和空间复杂度)

结果:

分析:

  1. 时间复杂度为T(k)=\begin{cases}O(1), & k=1,\4T(k-1)+O(1), & k>0 \end{cases},渐进意义下的最优算法T(n)=O(4^k)
  2. 空间复杂度为O(2^{2k})

基本题四:合并排序

一、实验目的与要求

  1. 熟悉合并排序算法;
  2. 初步掌握分治算法;

二、实验题目

采用递归与非递归两种方式实现合并排序算法。

输入:输入第一行包括一个整数 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

include

include

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

include

include

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)

基本题五:快速排序

一、实验目的与要求

  1. 熟悉快速排序算法;
  2. 初步掌握分治算法;

二、实验题目

实现快速排序算法。

输入:输入第一行包括一个整数 n,第二行包含 n 个整数,以空格间隔。

输出:输出一行 n 个整数,代表排序结果。

Simple input:

4

1 5 4 12

Simple output:

1 4 5 12

三、实现思想

基本思想是:选择一个基准数,通过一趟排序将要排序的数据分割成独立的两部分;其中一部分的所有数据都比另外一部分的所有数据都要小。然后,再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。

算法流程:

  1. 从数列中挑出一个基准值。
  2. 将所有比基准值小的摆放在基准前面,所有比基准值大的摆在基准的后面(相同的数可以到任一边);在这个分区退出之后,该基准就处于数列的中间位置。
  3. 递归地把"基准值前面的子数列"和"基准值后面的子数列"进行排序。

四、遇到的困难和收获

快速排序的边界问题需要仔细处理,基准值的不同选择方式算法的效率是不同的。

五、实现代码

include

include

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)

基本题六:线性时间选择问题

一、实验目的与要求

  1. 熟悉线性时间选择问题算法;
  2. 初步掌握分治算法;

二、实验题目

给定一个包含 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

include

include

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)

基本题七:平面最近点对问题

一、实验目的与要求

  1. 掌握最接近点对问题的算法;
  2. 熟悉分治算法的算法思想

二、实验题目

给定二维平面上乱序的 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

include

include

include

include

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

include

include

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

include

include

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

include

include

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)

还可以进一步优化,递归过程存在大量重复,可以使用备忘录或者动态规划,从而达到直接使用之前求解结果的目的。

posted @ 2020-04-27 09:45  早睡身体好呀  阅读(202)  评论(0)    收藏  举报