1.递归与分治的关系
分治是一种算法思想,递归是实现这种思想的一种手段。递归策略只需要少量的代码就可以描述出解决过程所需的多次重复计算,大大减少了代码量。
2.递归的概念
直接或者间接地调用自身的算法/函数称为递归算法/函数。
递归程序设计中地两个问题:
递归体:大问题是如何划分为小问题的。
递归出口:确定递归何时终止。边界条件不满足时,递归前进;边界条件满足时,递归返回。一般递归出口直接return或者cout,不再使用递归。
递归的优缺点
- 优点:描述简洁且易于理解。
- 缺点:运行效率低;递归次数过多容易造成堆栈溢出。
例题
例1:使用递归求n!
#include<iostream>
using namespace std;
int fac(int n){
int result;
if(n<0){
cout<<"error"<<endl;
}
else if(n==0||n==1){
result=1;
}
else{
result=n*fac(n-1);
}
return result;//要写return而不是cout,不然的话中间过程的result都会输出
}
int main()
{
cout<<fac(31);
}
例2:使用递归求斐波那契数列
#include<iostream>
using namespace std;
int fib(int n){
if(n<1){
cout<<"error";
}
else if(n==1||n==2){
return 1;
}
else{
return fib(n-1)+fib(n-2);
}
}
int main()
{
cout<<fib(6);
}
例3:递归解决汉诺塔问题
#include<iostream>
using namespace std;
int han(int n)
{
if(n==0){
return 0;
}
else if(n==1){
return 1;
}
else{
return han(n-1)*2+1;
}
}
int main()
{
cout<<han(3);
}
例4:递归解决猴子吃桃问题。
猴子第一天采摘了一些桃子,第二天吃了第一天的一半多一个,第三天吃了第二天的一半多一个...直到第十天就剩下一个。问:猴子第一天摘了多少桃子?
#include<iostream>
using namespace std;
int monkey(int n){
if(n==10){
return 1;
}
else{
return (monkey(n+1)+1)*2;
}
}
int main()
{
cout<<monkey(1);
}
例5:编写一个递归函数,将10进制转化成radix进制
//除基数,取余数,结果倒序排序
#include<iostream>
using namespace std;
void change(int n,int r)
{
if(n!=0){
change(n/r,r);
cout<<n%r;//因为是倒序输出,所以需要先递归调用,再输出
}
}
int main()
{
change(3,2);
}
例6:逆序输出一个正整数中的每一位数,例如:输入12345,一次输出5 4 3 2 1
#include<iostream>
using namespace std;
void reverse(int n){
if(n!=0){
cout<<n%10<<" ";//先输出,再递归调用
reverse(n/10);//正序的话,先递归调用,再输出
}
}
int main(){
reverse(12345);
}
3.集合的全排列问题
设计一个递归算法生成n个元素{r1,r2…,}的全排列(n!种)
//设R={r1,r2,…,rn}是要进行排列的n个元素,Ri=R-{ri}。
//集合X中元素的全排列记为perm(X)。
//(ri)perm(X)表示在全排列perm(X)的每一个排列前加上前缀ri得到的排列。
#include<iostream>
using namespace std;
//perm函数产生下标为k~m的元素的全排列,作为前k-1个元素的后缀
void perm(int a[],int k,int m){
if(k==m){
for(int i=0;i<=m;i++){
cout<<a[i]<<" ";
}
cout<<endl;
}
else{//数组a种产生下标k~m的元素的全排列
for(int i=k;i<=m;i++){
swap(a[k],a[i]);
perm(a,k+1,m);
swap(a[k],a[i]);//需要再交换回去,防止重复交换
}
}
}
int main(){
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++){
cin>>a[i];
}
perm(a,0,n-1);
}
4.整数划分
将正整数n表示成一系列正整数之和:n=n1+n2+…+nk,
其中n1≥n2≥…≥nk≥1,k≥1。
正整数n的这种表示称为正整数n的划分。求正整数n的不同划分个数。
//例如:整数6的划分方法数如下
//6;(最大加数等于6)
//5+1;(最大加数等于5)
//4+2,4+1+1;(最大加数等于4)
//3+3,3+2+1,3+1+1+1;(最大加数等于3)
//2+2+2,2+2+1+1,2+1+1+1+1;(最大加数等于2)
//1+1+1+1+1+1。(最大加数等于1)
//整数6的划分方法数=最大加数不大于6的方法数=最大加数等于6的方法数+最大加数不大于5的方法数
//将n的最大加数不超过m的划分个数记作q(n,m)
//q(6,2):6的最大加数不超过2的方法数=最大加数等于2的方法数q(4,2)+最大加数不超过1的方法数q(6,1)
//n>m>1时,q(n,m)=q(n-m,m)+q(n,m-1)
//具体分析:
//m=1时,q(n,1)=1;(所有的加数都为1)
//n=1时,q(1,m)=1;(只有一种情况)
//m>=n时,q(n,m)=q(n,n)=1+q(n,n-1)(最大加数等于n的方法数+最大加数不大于n-1的方法数)
#include<iostream>
using namespace std;
int zshf(int n,int m){
if(n<1){
return 0;
}
else if(n==1||m==1){
return 1;
}
else if(n<=m){
return zshf(n,n-1)+1;
}
else{
return zshf(n-m,m)+zshf(n,m-1);
}
}
int main(){
cout<<zshf(6,6);
}
5.分治法的基本思想
分治法是将一个难以解决的大问题,分割成一些较小规模的相同问题,以便各个击破,分而治之。
分解-->求解-->合并
分治法的特征:
6.二分搜索技术
(1)给定已按升序排好序的n个元素a[0:n-1],现要在这n个元素中找出一特定元素x。
#include<iostream>
#include<algorithm>
using namespace std;
//在数组a的下标0~n-1 的元素中寻找x
int bs(int a[],int x,int n){
int left=0;
int right=n-1;
while(left<=right){//二分搜索---使用while循环(不用递归)
int mid=(left+right)/2;
if(a[mid]==x){
return mid;
}
else if(a[mid]>x){
right=mid-1;
}
else{
left=mid+1;
}
}
return -1;//没有找到x
}
int main()
{
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++){
cin>>a[i];
}
sort(a,a+n);
for(int i=0;i<n;i++){
cout<<a[i]<<" ";//排序后的数组a
}
cout<<endl;
cout<<bs(a,4,n);
}
(2)找数对。给定若干个整数,询问其中是否有一对数的和等于给定的数。如果有多对数符合要求,输出最小数最小的一对。
#include<iostream>
#include<algorithm>
using namespace std;
int bs(int a[],int left,int right,int k){
if(left>right){
return -1;
}
else{
int mid=(left+right)/2;
if(a[mid]==k){
return mid;
}
else if(a[mid]>k){
bs(a,left,mid-1,k);//二分搜索---使用递归
}
else{
bs(a,mid+1,right,k);
}
}
}
int main()
{
int n;
cin>>n;
int a[n];
for(int i=0;i<n;i++){
cin>>a[i];
}
sort(a,a+n);//将数组升序排序,便于找到最小数最小的数对
int sum;
cin>>sum;
for(int i=0;i<n;i++){
if(bs(a,i+1,n-1,sum-a[i])){//!!!注意写法
cout<<a[i]<<" "<<sum-a[i];
break;//因为数组已经排序过了,所以找到的第一对数对就是最小值最小的数对
}
}
}
(3)排序且不重复输出
(因为可以用set解决,所以二分搜索先不写了)
7.循环赛日程表
8.棋盘覆盖问题
/******************************************
tr:当前棋盘左上角的方格的行号
tc:当前棋盘左上角的方格的列号
dr:特殊方格的行号
dc:特殊方格所在的列号
size:size=2^k,棋盘的规格为 2^k*2^k
*******************************************/
#include<iostream>
using namespace std;
int board[100][100];
int tile=1;//tile表示L型骨牌的编号
//每次调用chess函数,函数里的if-else语句都会判断一遍,这样才能将左上、左下、右上、右下四个角全部填充
void chess(int tr,int tc,int dr,int dc,int size){
if(size==1){
return;//先判断棋盘的边长,如果是1,则返回
}
int t=tile++;//每进行一次调用,l型骨牌的编号+1
int s=size/2;//每进行一次调用,棋盘进行一次划分
//检查特殊方格是否在左上角子棋盘中
if(dr<tr+s&&dc<tc+s){//在
chess(tr,tc,dr,dc,s);
}
else{//不在,将棋盘右下角的方格视为特殊方格,覆盖L型骨牌
board[tr+s-1][tc+s-1]=t;
chess(tr,tc,tr+s-1,tc+s-1,s);
}
//检查特殊方格是否在右上角子棋盘中
if(dr<tr+s&&dc>=tc+s){//在
chess(tr,tc+s,dr,dc,s);
}
else{//不在,将当前棋盘的左下角方格视为特殊方格,覆盖L型骨牌
board[tr+s-1][tc+s]=t;
chess(tr,tc+s,tr+s-1,tc+s,s);
}
//检查特殊方格是否在左下角子棋盘中
if(dr>=tr+s&&dc<tc+s) {//在
chess(tr+s,tc,dr,dc,s);
}
else{//不在,将该子棋盘的右上角方格视为特殊方格,覆盖L型骨牌
board[tr+s][tc+s-1]=t;
chess(tr+s,tc,tr+s,tc+s-1,s);
}
//检查特殊方格是否在右下角子棋盘中
if(dr>=tr+s&&dc>=tc+s){//在
chess(tr+s,tc+s,dr,dc,s);
}
else{//不在,将该子棋盘的左上角方格视为特殊方格,覆盖L型骨牌
board[tr+s][tc+s]=t;
chess(tr+s,tc+s,tr+s,tc+s,s);
}
}
int main(){
int size;
cin>>size;
int dr,dc;
cin>>dr>>dc;
chess(0,0,dr,dc,size);
for(int i=0;i<size;i++){
for(int j=0;j<size;j++){
cout<<board[i][j]<<" ";
}
cout<<endl;
}
}
9.从n个元素中找出第k小的元素
解决方法:
方法一:排序,时间复杂度O(nlogn)
方法二:优先队列,时间复杂度O(nlogn)
方法三:线性时间选择算法,时间复杂度O(n)(模仿快速排序算法,首先对输入的数组进行划分,然后对划分出的子数组之一进行递归处理。)
快速排序算法思想:
- 首先选第一个数作为分界数据,
- 将比它小的数据存储在它的左边,比它大的数据存储在它的右边,它存储在左、右两个子集之间。
- 这样左、右子集就是原问题分解后的独立子问题。
- 再用同样的方法,继续解决这些子问题,直到每个子集只有一个数据,就完成了全部数据的排序工作。
利用快速排序算法的思想,解决选择问题:
记一趟快速快速排序后,分解出左子集中元素个数为nleft,则选择问题可能是以下几种情况:
- nleft=k-1,则分界数据就是选择问题的答案。
- k-1<nleft,则选择问题的答案在左子集中找,问题规模变小。
- k-1>nleft,则选择问题的答案在右子集中找,问题规模变小。
/**********************************
a[]:要查找的序列
left:要查找的区间(!!!)的左边界下标
right:要查找的区间的右边界的下标
k:第k小的元素
********************************/
#include<iostream>
using namespace std;
int select(int a[],int left,int right,int k){
int i=left;
int j=right;
int tmp;
if(left<right){//区间中至少有两个元素
while(i!=j){//i和j交替移动,直到i=j之后,将a[i]赋值给tmp
tmp=a[left];
while(i<j&&a[j]>tmp){//先判断a[j],j先移动
j--;
}
swap(a[i],a[j]);//j从右向左扫描,知道遇到小于tmp的a[j],交换a[i]和a[j]
while(i<j&&a[i]<tmp){//再判断a[i]
i++;
}
swap(a[i],a[j]);
}
tmp=a[i];
if(k-1==i){
return a[i];//分界点就是问题的答案
}
else if(k-1>i){
return select(a,i+1,right,k);
}
else{
return select(a,left,i-1,k);
}
}
else if(left==right&&left==k-1){//区间中只有一个元素
return a[left];
}
}
int main()
{
int n;
while(cin>>n){
int a[n];
int k;
cin>>k;
for(int i=0;i<n;i++){
cin>>a[i];
}
cout<<select(a,0,n-1,k);
}
}
10.半数集问题
在该问题中,存在很多重复计算的子问题,比如求12的半数集时会包含3的半数集和6的半数集,而求6的半数集时也会包含3的半数集。为了不重复计算,我们使用记忆式搜索方法(备忘录方法)
#include<iostream>
using namespace std;
int a[100];
int comp(int n){
int ans=1;
//半数集问题中的ans定义为局部变量,是因为每一个comp函数都是计算一个整数的半数集,
//每计算一个整数的半数集都需要初始化ans=1,所以定义为局部变量
if(a[n]>0){
return a[n];//如果n的半数集中元素的个数已经算出来了,就不需要重复计算
}
else{
for(int i=1;i<=n/2;i++){
ans=ans+comp(i);
}
a[n]=ans;
return ans;
}
}
int main()
{
int n;
cin>>n;
cout<<comp(n);
}
memset(参数1,参数2,参数3)
将某一内存中的全部内容置为指定的值
- 参数1:这块内存的起始地址
- 参数2:指定的值
- 字节的个数
11.整数因子分解问题
在上图中,叶节点为1的结点的个数,就是要求的方案数
#include<iostream>
using namespace std;
int sum=0; //因为是整体的一个累加(也就是所有函数return结果的累加),所以需要将sum定义为全局变量
//半数集问题中的ans定义为局部变量,是因为每一个comp函数都是计算一个整数的半数集,
//每计算一个整数的半数集都需要初始化ans=1,所以定义为局部变量 。要区分开来
int solve(int n){
if(n==1){//!!!递归出口
sum++;
}
else{
for(int i=2;i<=n;i++){
if(n%i==0){
solve(n/i);
}
}
}
return sum;
}
int main()
{
int n;
cin>>n;
cout<<solve(n);
}