算法笔记7_递归与分治

递归

半数集

给定一个自然数n,由n开始可以依次产生半数集set(n)中的数如下。
(1) n∈set(n);
(2) 在n的左边加上一个自然数,但该自然数不能超过最近添加的数的一半;
(3) 按此规则进行处理,直到不能再添加自然数为止。
例如,set(6)={6,16,26,126,36,136}。
半数集set(6)中有6个元素。

  • 注意半数集是多重集。

对于给定的自然数n,编程计算半数集set(n)中的元素个数。
设set(n)中的元素个数为f(n) ,则显然有:
在这里插入图片描述
数字12为例:在这里插入图片描述
递归算法:

int comp(int n)
{
	int ans=1;
	if (n>1) for(int i=1;i<=n/2;i++)
		ans+=comp(i);
	return ans;
}

算法中显然有很多重复的子问题计算。
使用数组存储忆计算过的结果,避免重复计算,可明显改进算法的效率。

递归算法—记忆式搜索:

int a[1001];
int comp(int n)
{
	int ans=1;
	if(a[n]>0)return a[n];		//已经计算
	for(int i=1;i<=n/2;i++)
		ans+=comp(i);
	a[n]=ans;				//保存结果
	return ans;
}

整数因子分解

大于1的正整数n可以分解为:
n=X1 * X2 * … * Xm
当n=12时,共有8种不同的分解式:
对于给定的正整数n,编程计算n共有多少种不同的分解式。

  • 输入
    数据有多行,给出正整数n(1≤n≤2000000000)。
  • 输出
    每个数据输出1行,是正整数n的不同的分解式数量。
#include<bits/stdc++.h>
using namespace std;
int total;
void solve(int n) {
	if(n==1){
        total++;
	}
    else{ 
        for(int i=2;i<=n;i++){
            if(n%i==0){
                solve(n/i);
            }
        }
    }
}
int main(){
    int n;
    while(cin>>n)
    {
	total = 0;
	solve(n);
	cout<<total<<endl;
    }
    return 0;
}

在这里插入图片描述

分治

最大子段和

给定由n个整数组成的序列(a1, a2, …, an),最大子段和问题要求该序列形如
在这里插入图片描述
的最大值(1≤i≤j≤n),当序列中所有整数均为负整数时,其最大子段和为0。例如,序列(-20, 11, -4, 13, -5, -2)的
最大子段和为:
在这里插入图片描述
最大子段和问题的分治策略是:
(1)划分:按照平衡子问题的原则,将序列(a1, a2, …, an)划分成长度相同的两个子序列(a1, …, a⌊n/2⌋)和 (a⌊n/2⌋+1, …, an),则会出现以下三种情况:
① a1, …, an的最大子段和=a1, …,a⌊n/2⌋的最大子段和;
② a1, …, an的最大子段和=a⌊n/2⌋+1, …, an的最大子段和;
③ a1, …, an的最大子段和=在这里插入图片描述 ,且 在这里插入图片描述
(2)求解子问题:对于划分阶段的情况①和②可递归求解,情况③需要分别计算 在这里插入图片描述在这里插入图片描述
则s1+s2为情况③的最大子段和。
(3)合并:比较在划分阶段的三种情况下的最大子段和,取三者之中的较大者为原问题的解。
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
int MaxSum(int a[ ],int left,int right){
    int sum=0;
    if(left==right){
        if(a[left]>0){
            sum=a[left];
        }else{
            sum=0;
        }
    }else{
        int center=(left+right)/2;
        int leftsum=MaxSum(a,left,center);                                                         //对应情况①,递归求解
        int rightsum=MaxSum(a,center+1,right);
        int s1=0,lefts=0;
        for(int i=center;i>=left;i--){
            lefts+=a[i];
            if(lefts>s1){
                s1=lefts;
            }
        }
        int s2=0,rights=0;
        for(int j=center+1;j<=right;j++){
            rights+=a[j];
            if(rights>s2){
               s2=rights;
            }
        }
        sum=s1+s2;
        if(sum<leftsum){
            sum=leftsum;
        }
        if(sum<rightsum){
            sum=rightsum;
        }
     }
     return sum;
}
int main(){
    int x,a[1001];
    cin>>x;
    for(int i=1;i<=x;i++){
        cin>>a[i];
    }
    cout<<MaxSum(a,1,x)<<endl;
}

时间复杂性O(nlog2n)

循环赛日程安排

设有n=2k个选手要进行网球循环赛,要求设计一个满足以下要求的比赛日程表:
(1)每个选手必须与其他n-1个选手各赛一次;
(2)每个选手一天只能赛一次。
按此要求,可将比赛日程表设计成一个 n 行n-1列的二维表,其中,第 i 行第 j 列表示和第 i 个选手在第 j 天比赛的选手。
按照分治的策略,可将所有参赛的选手分为两部分,n=2k个选手的比赛日程表就可以通过为n/2=2k-1个选手设计的比赛日程表来决定。递归地执行这种分割,直到只剩下2个选手时,比赛日程表的制定就变得很简单:只要让这2个选手进行比赛就可以了。
显然,这个求解过程是自底向上的迭代过程,其中左上角和左下角分别为选手1至选手4以及选手5至选手8前3天的比赛日程,据此,将左上角部分的所有数字按其对应位置抄到右下角,将左下角的所有数字按其对应位置抄到右上角,这样,就分别安排好了选手1至选手4以及选手5至选手8在后4天的比赛日程,如图©所示。具有多个选手的情况可以依此类推。
在这里插入图片描述
这种解法是把求解2k个选手比赛日程问题划分成依次求解21、22、…、2k个选手的比赛日程问题,换言之,2k个选手的比赛日程是在2k-1个选手的比赛日程的基础上通过迭代的方法求得的。在每次迭代中,将问题划分为4部分:
(1)左上角:左上角为2k-1个选手在前半程的比赛日程;
(2)左下角:左下角为另2k-1个选手在前半程的比赛日程,由左上角加2k-1得到,例如22个选手比赛,左下角由左上角直接加2得到,23个选手比赛,左下角由左上角直接加4得到;
(3)右上角:将左下角直接抄到右上角得到另2k-1个选手在后半程的比赛日程;
(4)右下角:将左上角直接抄到右下角得到2k-1个选手在后半程的比赛日程;
算法设计的关键在于寻找这4部分元素之间的对应关系。

void GameTable(int k, int a[ ][ ])
     {  
        // n=2k(k≥1)个选手参加比赛,
        //二维数组a表示日程安排,数组下标从1开始
         n=2;       //k=0,2个选手比赛日程可直接求得
         //求解2个选手比赛日程,得到左上角元素
         a[1][1]=1; a[1][2]=2;   
         a[2][1]=2; a[2][2]=1;
         for (t=1; t<k; t++)
         //迭代处理,依次处理22, …, 2k个选手比赛日程
         {
        temp=n; n=n*2;   
        //填左下角元素
        for (i=temp+1; i<=n; i++ )
            for (j=1; j<=temp; j++)
                a[i][j]=a[i-temp][j]+temp;
                //左下角元素和左上角元素的对应关系
       //填右上角元素
       for (i=1; i<=temp; i++)       
           for (j=temp+1; j<=n; j++)
              a[i][j]=a[i+temp][(j+temp)% n];
       //填右下角元素
       for (i=temp+1; i<=n; i++)
          for (j=temp+1; j<=n; j++)
             a[i][j]=a[i-temp][j-temp];
    }
}

在这里插入图片描述时间复杂性为O(4k)。

慕课

幂乘算法

  • 输入: a为给定实数,n为自然数
  • 输出: an

传统算法:顺序相乘
乘法次数:Θ(n)

分治算法——划分
an/2*an/2 (n为偶数)
a(n-1)/2*a(n-1)/2 (n为奇数)

算法分析:
以乘法作为基本运算
•子问题规模:不超过n/2
•两个规模近似 n/2的子问题完全一样,只要计算1次
W (n) = W (n/2) + Θ(1)
W (n) = Θ(log n)

改进分治算法的途径1:减少子问题数

减少子问题个数的依据
分治算法的时间复杂度方程
W(n) = aW(n/b) + d(n)
a:子问题数,n/b:子问题规模, d(n):划分与综合工作量.
当 a 较大, b较小, d(n)不大时,方程的解:
W(n) =Θ(nlogba )
减少a是降低函数W(n)的阶的途径.
利用子问题的依赖关系,使某些子问题的解通过组合其他子问题的解而得到.

适用于:子问题个数多,划分和综合工作量不太大,时间复杂度函数W(n) =Θ(nlogba )

改进分治算法的途径2:增加预处理

原算法:
在每次划分时对子问题数组重新排序

改进算法:

  1. 在递归前对 X,Y 排序,作为预处理
  2. 划分时对排序的数组 X,Y 进行拆分,
    得到针对子问题 PL的数组 XL,YL 及针对子问题 PR的数组 XR,YR
    原问题规模为 n,拆分的时间为O(n)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值