1.1 教学计划与递归


1.1 教学计划与递归




由数据结构反推算法复杂度以及算法内容

作者: yxc

一般 ACM 或者笔试题的时间限制是\(1\)秒或\(2\)秒。
在这种情况下,C++ 代码中的操作次数控制在\(10^7 \sim 10^8\)为最佳。

下面给出在不同数据范围下,代码的时间复杂度和算法该如何选择:

1.\(n \leqslant 30\):指数级别、DFS+剪枝、状态压缩DP;
2.\(n \leqslant 100 \Rightarrow O(n^3)\):Floyd、DP、高斯消元;
3.\(n \leqslant 1000 \Rightarrow O(n^2),\; O(n^2logn)\):DP、二分、朴素版Dijkstra、朴素版Prim、Bellman-Ford;
4.\(n \leqslant 10000 \Rightarrow O(n\sqrt n)\):块状链表、分块、莫队;
5.\(n \leqslant 100000 \Rightarrow O(nlogn)\):各种sort、线段树、树状数组、set/map、heap、拓扑排序、Dijkstra+heap、Prim+heap、SPFA、求凸包、求半平面交、二分、CDQ分治、整体二分;
6.\(n \leqslant 1000000 \Rightarrow O(n),\;\)以及常数较小的\(O(nlogn)\)算法:单调队列、hash、双指针扫描、并查集、KMP、AC自动机;常数较小的\(O(nlogn)\)的做法:sort、树状数组、heap、Dijkstra、SPFA;
7.\(n \leqslant 10000000 \Rightarrow O(n)\):双指针扫描,KMP、AC自动机、线性筛素数;
8.\(n \leqslant 10^9 \Rightarrow O(\sqrt n)\):判断质数;
9.\(n \leqslant 10^{18} \Rightarrow O(logn)\):最大公约数,快速幂;
10.\(n \leqslant 10^{1000} \Rightarrow O((logn)^2)\):高精度加减乘除;
11.\(n \leqslant 10^{100000} \Rightarrow O(logk \times loglogk)\)\(k\)表示位数:高精度加减、FFT/NTT;

注:这里的\(log\)指的是以\(2\)为底的对数;






递归

int f(int n){
	f(n-1);
}
  • 递归即自己调用自己

例:斐波那契数列

\(f=\{1,2,3,5,8,cdots\}\)

\(n=1 \quad f_1=1\)\(n=2 \quad f_2=2\)

$f_n=f_{n-1}+f_{n-2} \quad n \geqslant 3 $

int f(int n){
	if(n==1) return 1;
	if(n==2) return 2;
	return f(n-1)+f(n-2);
}

递归\(\Rightarrow\)递归搜索树






92. 递归实现指数型枚举


\(1\sim n\)\(n\) 个整数中随机选取任意多个,输出所有可能的选择方案。


输入格式

输入一个整数 \(n\)


输出格式

每行输出一种方案。

同一行内的数必须升序排列,相邻两个数用恰好 \(1\) 个空格隔开。

对于没有选任何数的方案,输出空行。

本题有自定义校验器(SPJ),各行(不同方案)之间的顺序任意。


数据范围

\(1\leqslant n \leqslant 15\)


输入样例

3

输出样例


3
2
2 3
1
1 3
1 2
1 2 3

难度: 简单
时/空限制: 5s / 256MB
来源: 《算法竞赛进阶指南》
算法标签:递归



思路

  • 数据范围为\(1\leqslant n \leqslant 15\),所以可以用时间复杂度为\(O(2^n)\)的算法来做;

  • 对于\(1\sim n\)\(n\)个数,每个数有 选/不选 两种情况,所以总共的方案数即为\(2^n\),故总的时间复杂度为\(O(n 2^n)\)

  • 递归(即DFS),最重要的是顺序,即找一个顺序,可以把所有方案不重不漏地找出来;

  • \(1 \sim n\),依此考虑每个数 选/不选;

例:\(n=3\)时的递归搜索树

  • 状态(即每个数 选/不选)可以开一个长度为\(n\)的数组来记录;

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=16;
int n;
int st[N];//状态,记录每个位置当前的状态:0表示还没考虑,1表示选它,2表示不选它
void dfs(int u){//u代表当前枚举到第u位
    if(u>n){
        for(int i=1;i<=n;++i)
        	if(st[i]==1)
            	printf("%d ",i);
		printf("\n");
        return;
    }
	st[u]=2;
	dfs(u+1); //第一个分支:不选
	st[u]=0; //恢复现场
	
	st[u]=1;
	dfs(u+1); //第二个分支:选
	st[u]=0; //恢复现场
}
int main(){
	cin>>n;
	dfs(1);
	return 0;
}
/*
运行时间: 32 ms
运行空间: 856 KB 
*/

如果要将方案记录下来

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
const int N=16;
int n;
int st[N];
vector<vector<int>> ways;
//ways代表方案
void dfs(int u){
    if(u>n){
		vector<int>way;
		for(int i=1;i<=n;++i) //记录方案
			if(st[i]==1)
				way.push_back(i);
		ways.push_back(way);
        return;
    }
	st[u]=2;
	dfs(u+1); 
	st[u]=0; 
	
	st[u]=1;
	dfs(u+1); 
	st[u]=0;
}
int main(){
	cin>>n;
	dfs(1);
	for(int i=0;i<ways.size();++i){
		for(int j=0;j<ways[i].size();++j) printf("%d ",ways[i][j]);
		puts("");
	}
	return 0;
}
/*
运行时间: 89 ms
运行空间: 3084 KB 
*/






94. 递归实现排列型枚举


\(1\sim n\)\(n\) 个整数排成一行后随机打乱顺序,输出所有可能的次序。


输入格式

一个整数 \(n\)


输出格式

按照从小到大的顺序输出所有方案,每行 \(1\)个。

首先,同一行相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。


数据范围

\(1 \leqslant n \leqslant 9\)


输入样例

3

输出样例

1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

难度: 简单
时/空限制: 5s / 256MB
来源: 《算法竞赛进阶指南》
算法标签:递归



思路

  • 数据范围为\(1 \leqslant n \leqslant 9\)\(9!=326880\),因此时间复杂度大约为\(O(n \times n!) \Rightarrow DFS\)

字典序:

\(A:a_1,a_2,\cdots,a_n\)

\(B:b_1,b_2,\cdots,b_m\)

\(a_i<b_i\)\(a_i\)不存在但\(b_i\)存在 \(\quad\Rightarrow A<B\)

\(a_i>b_i\)\(b_i\)不存在但\(a_i\)存在 \(\quad\Rightarrow A>B\)

\(n=m\)并且\(a_n=b_m \quad\Rightarrow A=B\)

  • 全排列问题一般有两种枚举方式:

    1.依此枚举每个数放哪个位置;

    2.依此枚举每个位置放哪个数;

2.的例:\(n=3\)时的递归搜索树

上图保证了(相对意义上的)左子树的方案 字典序一定小于 右子树的方案;

  • 开一个长度为\(n\)的数组来记录状态;

代码

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=10;
int n;
int state[N];// 0 表示还没放数,1~n表示放了哪个数
bool used[N];//true表示用过,false表示还未用过
void dfs(int u){
	if(u>n){//边界
		for(int i=1;i<=n;++i) printf("%d ",state[i]); //打印方案
		puts("");
		return;
	}
	//依次枚举每个分支,即当前位置可以填哪些数
	for(int i=1;i<=n;++i)
		if(!used[i]){
			state[u]=i;
			used[i]=true;
			dfs(u+1);
			//恢复现场
			state[u]=0;
			used[i]=false;
		}
}
int main(){
	scanf("%d",&n);
	dfs(1);
	return 0;
}
/*
运行时间: 373 ms
运行空间: 7000 KB 
*/

分析时间复杂度

  • 需要递归\(n\)层;
  • 第一层时间复杂度为\(O(n)\);(一个for循环)
  • 第二层时间复杂度为\(O(n\times n)\);(由第一层衍生出\(n\)个分支,每个分支一个for循环)
  • 第三层时间复杂度为\(O(n \times n-1 \times n)\);(由第二层衍生出\(n-1\)个分支)
  • \(\cdots\)
  • 倒数第二层时间复杂度为\(O(n! \times n)\)
  • 最后一层时间复杂度为\(O(n! \times n)\);(最后一层有\(n!\)个结点,且需要输出方案)

总的时间复杂度为\(O[n(1+n+n(n-1)+n(n-1)(n-2)+ \cdots + n!)]\)

相当于\(P_n^0+P_n^1+P_n^2+\cdots +P_n^n \geqslant n!\)

原式相当于:

\[n(n!+\frac{n!}{1}+\frac{n!}{1 \times 2} +\frac{n!}{1 \times 2 \times 3}+\cdots \frac{n!}{(n-1)!}) \]

对其进行放缩:

\[原始\leqslant n(n!+\frac{n!}{1}+\frac{n!}{2} +\frac{n!}{4}+\frac{n!}{8}\cdots \frac{n!}{(n-1)!}) \leqslant n \times n!(1+1+\frac{1}{2}+\frac{1}{4}+\frac{1}{8}+\cdots)\leqslant 3n! \]

故整个时间复杂度小于等于\(O(n \times n!)\)






作业

  • AcWing 93.递归实现组合型枚举

  • AcWing 1209.带分数

posted @ 2021-03-15 15:55  PotremZ  阅读(160)  评论(0编辑  收藏  举报