洛谷递归和boos(新手村)

  • 关卡6-1
    先输入一个自然数n(n≤1000),然后对此自然数按照如下方法进行处理:
    在它的左边加上一个自然数,但该自然数不能超过原数的一半;
    加上数后,继续按此规则进行处理,直到不能再加自然数为止.
    输出这样的数有几个

递归的方式求本问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <cstdio>
int n;
int digui[1001];
int main() {
   scanf("%d", &n);
   for(int i = 1; i <= n; i++) { 
      for(int j = 1; j <= i / 2; j++) {
         digui[i] += digui[j];
      }
      digui[i]++;
   }
   printf("%d", digui[n]);
   return 0;
}

方法二:
#include<iostream>
using namespace std;
int s=1;
void function(int &a)
{
    if(a!=0)
    {
        for(int i=1;i<=a;i++)
        {
            s++;
            int b=i/2;
            function(b);
        }
    }
}
int main()
{
    int n;
    cin>>n;
    n=n/2;
    function(n);
    cout<<s;
    return 0;
}
1
2
BOSS战-入门综合练习1

题目描述
又是一年秋季时,陶陶家的苹果树结了n个果子。陶陶又跑去摘苹果,这次她有一个a公分的椅子。当他手够不着时,他会站到椅子上再试试。
这次与NOIp2005普及组第一题不同的是:陶陶之前搬凳子,力气只剩下s了。当然,每次摘苹果时都要用一定的力气。陶陶想知道在s<0之前最多能摘到多少个苹果。
现在已知n个苹果到达地上的高度xi,椅子的高度a,陶陶手伸直的最大长度b,陶陶所剩的力气s,陶陶摘一个苹果需要的力气yi,求陶陶最多能摘到多少个苹果。
输入格式
第1行:两个数 苹果数n,力气s。
第2行:两个数 椅子的高度a,陶陶手伸直的最大长度b。
第3行~第3+n-1行:每行两个数 苹果高度xi,摘这个苹果需要的力气yi。
输出格式
只有一个整数,表示陶陶最多能摘到的苹果数。
输入输出样例
输入
8 15
20 130
120 3
150 2
110 7
180 1
50 8
200 0
140 3
120 2
输出
4

解答:

  1. 搜索的基本思路就是枚举,枚举可能出现的各种情况,然后从中找到满足条件的结果。
    对于这个题,朴素的搜索方法就是DFS搜索每个苹果,每个苹果可分支出两个搜索子树:摘这个苹果或者不摘这个苹果。比如这样的一棵搜索树:
    upload successful
    我们使用递归函数,从第一个苹果开始递归访问,如果这个苹果可以取(高度够得到或者当前剩下的体力还够用),则递归搜索取这个苹果的子树。然后无论能不能取都递归搜索不取这个苹果的子树。

    当搜索到叶子节点也就是第n+1个苹果(这个苹果并不存在)的时候,返回0。对于其他苹果节点,返回两个搜索子树返回值中的较大值(只有一个则返回那个值)。
    那么函数的返回值是什么意思呢?返回值表示这个苹果的搜索子树里的最大能取的苹果数量。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    
    #include<iostream>
    using namespace std;
    int n,s,a,b,ans;
    int xi[5005],yi[5005];
    int dfs(int num,int rest){
        if(num>n) return 0;//如果到了第n+1个苹果,就说明DFS递归到底了
        int maxn=0;
        if(xi[num]<=a+b&&rest>=yi[num]){
            maxn=dfs(num+1,rest-yi[num])+1>dfs(num+1,rest)?dfs(num+1,rest-yi[num])+1:dfs(num+1,rest);//dfs()+1表示取当前搜索到的苹果,因此摘到苹果的总数+1
        }//返回两个搜索子树里最大值的较大值
        return maxn;
    }
    int main(){
        cin>>n>>s>>a>>b;
        for(int i=1;i<=n;i++){
            cin>>xi[i]>>yi[i];
        }
        cout<<dfs(1,s);//从第一个苹果开始递归求解
        return 0;
    }
    

但是这个代码如果你提交的话,就会完美的TLE掉。我们看一下这个题的数据范围,n最大有5000个,每个苹果有取或不取两个选择,那么搜索完所有的子树差不多就是2^5000次运算。而计算机一秒只能运算大约10^7次左右,当然会TLE了。
这种最朴素的搜索算法时间复杂度差不多是O(2^n)的……几乎绝大部分的题目的较大的数据都过不去……那么我们有什么好办法吗?确实存在方法——剪枝和记忆化搜索。

1_1   **所谓剪枝,就是将对答案没有贡献的搜索子树剪去,从而减少搜索次数,提高运行速度的一种搜索优化方法。剪枝对于DFS的优化效果比较明显**,上面一版代码的运行时间跑到了3500多毫秒,但是优化后113毫秒就跑完了(虽然这样还不是最快)。那这90%左右的时间是怎么优化掉的呢?  
``maxn=dfs(num+1,rest-yi[num])+1>dfs(num+1,rest)?dfs(num+1,rest-yi[num])+1:dfs(num+1,rest);``   
我们要知道,如果想得到一个函数的返回值,首先就要运行一遍这个函数。复杂度如此高的函数在比较和调用值的时候分别运算了一次,就相当于给搜索树凭空加了好多无用的搜索子树。那么我们能不能用两个个变量来存储dfs(num+1,rest−yi[num])和dfs(num+1,rest)的值呢?这样经过测试可以节省大约30%的时间。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    #include<iostream>
using namespace std;
int n,s,a,b,ans;
int xi[5005],yi[5005];
int dfs(int num,int rest){
    if(num>n) return 0;
    int maxn=dfs(num+1,rest);
    if(xi[num]<=a+b&&rest>=yi[num]){
        int t=dfs(num+1,rest-yi[num])+1;
        maxn=t>maxn?t:maxn;
    }
    return maxn;
}
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>xi[i]>>yi[i];
    }
    cout<<dfs(1,s);
    return 0;
}


1_2  **所谓记忆化搜索,就是将每个不同参量的函数的返回值存在一个数组里,当再次调用这个函数的时候,就不用再次费时间计算这个函数的返回值了。**这里我们还是结合代码讲一下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    #include<iostream>
using namespace std;
int n,s,a,b,ans;
int xi[5005],yi[5005];
bool visit[5005][1001];//存储是否访问过调用这两个参量的函数
int mem[5005][1001];//存储调用这两个参量的函数的返回值
int dfs(int num,int rest){
    if(num>n) return 0;
    if(visit[num][rest]) return mem[num][rest];//如果调用这两个参量的函数已经被访问过,那么直接返回之前存储的值即可
    visit[num][rest]=true;
    int maxn=dfs(num+1,rest);
    if(xi[num]<=a+b&&rest>=yi[num]){
        int  t=dfs(num+1,rest-yi[num])+1;
        maxn=t>maxn?t:maxn;
    }
    return mem[num][rest]=maxn;//返回值的同时存储这次运算的返回值
}
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>xi[i]>>yi[i];
    }
    cout<<dfs(1,s);
    return 0;
}

1_3  我们不难发现,对于那些不能够采到的苹果,我们搜索它们只会白白浪费时间,那我们就可以将这些苹果排除掉,这样就使得搜索子树被缩小了。这就是剪枝。
   我们怎么才能避免搜索到这些苹果被搜索到呢?我们可以将所有的苹果按照高度从矮到高排序(排序的知识在第一弹里讲过,不会的同学可以去看一下)。在这种情况下当我们搜索到一个够不到的苹果时,无论我们再往下搜索多久,我们都不会再搜索到可以够得到的苹果了。这时候我们就可以 return 0 了。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include<iostream>
#include<algorithm> 
using namespace std;
int n,s,a,b,ans;
bool visit[5005][1001];
int mem[5005][1001];
struct apple{
    int xi,yi;
}ap[5005];
int dfs(int num,int rest){
    if(num>n||ap[num].xi>a+b) return 0;//当搜索到够不到的苹果后,就不再继续向下搜索了
    if(visit[num][rest]) return mem[num][rest];
    visit[num][rest]=true;
    int maxn=dfs(num+1,rest);
    if(ap[num].xi<=a+b&&rest>=ap[num].yi){
        int  t=dfs(num+1,rest-ap[num].yi)+1;
        maxn=t>maxn?t:maxn;
    }
    return mem[num][rest]=maxn;
}
int cmp(apple x,apple y){
    return x.xi<y.xi;
}
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>ap[i].xi>>ap[i].yi;
    }
    sort(ap+1,ap+n+1,cmp);//按照高度从矮到高排序
    cout<<dfs(1,s);
    return 0;
} 
  //这种方法比起剪枝前,又快了大约15%。

1_4  动态规划——背包
我们做题不能只靠搜索,世界上解决这类问题也不是只有搜索这一种算法,要不然还要OI干吗?

于是,有一些特殊的搜索问题就被优化了,并且形成了一类新的算法:动态规划。动态规划的种类很多:有线性动态规划、多维动态规划、区间动态规划等。我们这里先只讲一类特殊的多维动态规划:背包问题。

动态规划最核心的思想就是状态转移。即任何状态的解都可以建立在已知状态的基础上快速求出。这是不是和记忆化搜索有些类似呢?事实上动态规划和记忆化搜索真的差不多,记忆化搜索需要一个数组来存储不同参量的递归函数的返回值,动态规划需要一个状态转移数组来存储已求解的状态的解。而动态规划相比搜索的高明之处就在于,动态规划可以将搜索有向化,而不是漫无目的地遍历搜索每一个可能的状态,相当于自动剪枝。

我们前面讲过,动态规划实质上是对搜索的优化,之前我们搜索,是从前向后递归搜索,根据之后状态返回的值来决定向前返回什么值。而动态规划用的方法恰恰相反,是从前向后递推搜索,根据之前状态传递来的值决定向后传递什么值。这就是动态规划省时的主要原因。

那么接下来我们来讲背包问题。背包问题解决的一类问题是:对于空间有限的背包,和一些有着已知体积与价值的物品,优化取物品策略使得背包里物品的价值总和最大。对于这个题,总力气就是背包的空间,摘每个苹果所需要的力气就是苹果占背包的空间,每个苹果的价值都是1,我们只需让背包里的苹果价值和最大即可。

上面讲到,动态规划的核心是状态的转移,与此相对应的,每类动态规划问题也都有它的状态转移方程,比如背包问题,它的状态转移方程就是:dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]) 。这之中,dp[i][j] 表示对于空间大小为j的背包考虑前i个物品所能得到的最大总价值, v[i]表示第i个物品的占空间大小, w[i]表示第i个物品的价值。

不过讲了这么多,大家估计也听烦了,我们先上代码,然后再根据代码讲一下背包问题的解法。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<iostream> 
using namespace std;
int dp[5005][1001];
int xi[5005],yi[5005],n,s,a,b;
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>xi[i]>>yi[i];
    }
    for(int i=1;i<=n;i++)//枚举考虑每一个苹果
    for(int j=0;j<=s;j++){//枚举背包大小
        dp[i][j]=dp[i-1][j];//不能取就直接转移考虑之前苹果的最大值
        if(xi[i]<=a+b&&j>=yi[i])//如果能够取
        dp[i][j]=dp[i-1][j-yi[i]]+1>dp[i][j]?dp[i-1][j-yi[i]]+1:dp[i][j];//这个就是动态转移方程。max函数运行太慢,我们这里选择三目运算符取较大值
    }
    cout<<dp[n][s];//因为是从前向后递推,因此接收最终答案的位置也从最前面转到了最后面
    return 0;
}

为什么背包问题的状态转移方程是dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]) 呢?我们还是回到之前讲过的知识点:动态规划实质上是对搜索的优化。回想一下我们当时是怎么搜索的?对于搜索到的每个苹果,都有两种选择:取或者不取。不取则对应返回不取这个苹果后剩下的苹果最多能取的个数,取则对应返回取这个苹果后剩下的苹果最多能够取的个数加1。那么我们该如何将这个转化为递推关系呢?

首先我们考虑,当我们取了一个苹果,就相当于把背包里容纳这个苹果的空间分给了这个苹果,剩下的空间再由这个苹果之前(为什么是之前等一下会讲到)的苹果来分。那么不就相当于把背包缩小后再考虑前面的苹果了吗?那么不取就是背包大小不变,由之前的苹果来分这个背包的空间。基于这样的思想,我们就可以比较在当前背包大小下,是取这个苹果更值还是不取这个苹果更值。而缩小背包考虑之前的苹果的工作,在考虑上一个苹果时就已经求解过了。这样就可以不断地考虑一个又一个的苹果,不断地递推下去,最终得到最终的结果。

那么实现思路就是这样的:首先用一个循环枚举每个苹果,然后在考虑每个苹果时,再用一个循环枚举背包大小(为什么要枚举背包大小?——比较最大值时不是要缩小背包吗?这里就是为下一步递推比较做准备),对于每个大小的背包,如果能够取这个苹果(够得着而且当前背包大小装得下这个苹果),就比较取这个苹果和不取这个苹果哪个更值;如果不能取(够不到或者当前背包大小装不下这个苹果),就将这个大小的背包装之前的苹果的最大值传递给这个dp值。这样就得到了上面的代码。现在再回去看看之前的代码,是不是就明白了呢?

1_5 贪心算法
有的同学可能会注意到,我们给的状态转移方程里有一个变量w[i] ,但是在这个题里我们没有用w[i] ,而是直接用1表示,让人对照起来看难免有些别扭。如果你觉得别扭的话,那我要夸奖你,因为一眼就看出了这个问题其实动态规划并不是最优解。

我们最开始用无优化搜索跑了3500多毫秒才跑完,之后记忆化搜索直接提升到130多毫秒,然后剪枝又优化到110多毫秒,之后动态规划直接60多毫秒解决问题。那你以为这就是这个题的极限通过时间了吗?接下来介绍的贪心算法15毫秒就通过了这个题。

贪心算法其实比动态规划算法要低级不少,因为它适用面实在是有点窄。但是对于某些问题,贪心法却能跑出比动态规划快不少的成绩。

那么为什么这个题可以用贪心算法解决呢?我们看一下,在这个题里,所有苹果费力气也就是占背包空间不同,但是价值都是1。背包问题主要是为了解决拿得多却不一定价值最大,拿价值大的却可能装不下其他有价值的东西而使人陷入两难才被发明的算法。对于价值相同体积却不同的物品,我们每次只取体积最小的,不就能在取得当前价值的情况下,最大化剩余空间,从而拿更多苹果了吗?

讲到这里,大家可能就有点明白贪心算法的适用范围了。我之所以先引例,就是因为下面这段话实在有点晦涩难懂:

百度百科定义:贪心算法是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。贪心算法不是对所有问题都能得到整体最优解,关键是贪心策略的选择,选择的贪心策略必须具备无后效性,即某个状态以前的过程不会影响以后的状态,只与当前状态有关。

我语言功底有限,就不再用我贫瘠的语言来给大家详细解释这段话了。总之,这个题,用贪心确实是最优解。这点从时间复杂度上就能看出来:

搜索的基础复杂度(不加优化)是O(k^n) (k指每个节点的选择分支的个数)的,动态规划的基础复杂度是O(nm) (n,m分别指的是物品数量和背包大小),而贪心却只有O(n) 。搜索适用范围最广,同样地时间复杂度也最高;动态规划适用范围有所缩小,但是时间复杂度也相应地提高了;贪心算法适用范围极窄,但却拥有极优的时间复杂度。万事万物都是这样,既有长处,又有短处,长短互补,向来如此。

咳咳……扯远了。下面放上贪心算法的代码。这里就不加注释了,大家借此机会锻炼一下自己的读代码能力吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include<iostream>
#include<algorithm> 
using namespace std;
int n,s,a,b,x_,y_,can,rest,ans;
struct apple{
    int xi,yi;
}ap[50005];
int cmp(apple x,apple y){
    return x.yi<y.yi;
}
int main(){
    cin>>n>>s>>a>>b;
    for(int i=1;i<=n;i++){
        cin>>x_>>y_;
        if(x_<=a+b){
            can++;
            ap[can].xi=x_;
            ap[can].yi=y_;
        }
    }
    sort(ap+1,ap+can+1,cmp);
    rest=s;
    ans=0;
    for(int i=1;rest>=ap[i].yi&&i<=can;i++){
        ans++;
        rest-=ap[i].yi;
    }
    cout<<ans;
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值