递归与分治策略

基本题一:全排列问题

一、实验目的与要求

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

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

结果:

分析:

  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<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)

基本题三:棋盘覆盖问题

一、实验目的与要求

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

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

结果:

分析:

  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<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)

基本题五:快速排序

一、实验目的与要求

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

二、实验题目

实现快速排序算法。

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

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

Simple input:

4

1 5 4 12

Simple output:

1 4 5 12

三、实现思想

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

算法流程:

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

四、遇到的困难和收获

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

五、实现代码

#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)

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

一、实验目的与要求

  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<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)

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

一、实验目的与要求

  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<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)

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

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