蓝桥杯模拟赛 摆动序列 详细题解 多种解法

蓝桥杯模拟赛 摆动序列 详细题解 多种解法

  大家好,我叫亓官劼(qí guān jié ),在CSDN中记录学习的点滴历程,时光荏苒,未来可期,加油~博客地址为:亓官劼的博客亓官劼的博客2

本文原创为亓官劼,请大家支持原创,部分平台一直在盗取博主的文章!!!

博主目前仅在CSDN中写博客,唯一博客更新的地址为:亓官劼的博客亓官劼的博客2


第十一届 蓝桥杯 省 模拟赛 完整题目+题解地址为:第十一届 蓝桥杯 省 模拟赛 试题+题解


有很多小伙伴反应说在题解中写的这题的动态规划的代码看不懂,不知道为什么要这样写,所以本文来详细的解释一下这题的解法,以及动态规划如何进行优化的过程。

题目

问题描述

如果一个序列的奇数项都比前一项大,偶数项都比前一项小,则称为一个摆动序列。即 a[2i]<a[2i-1], a[2i+1]>a[2i]。
  小明想知道,长度为 m,每个数都是 1 到 n 之间的正整数的摆动序列一共有多少个。

输入格式

输入一行包含两个整数 m,n。

输出格式

输出一个整数,表示答案。答案可能很大,请输出答案除以10000的余数。

样例输入

3 4

样例输出

14

样例说明

以下是符合要求的摆动序列:
  2 1 2
  2 1 3
  2 1 4
  3 1 2
  3 1 3
  3 1 4
  3 2 3
  3 2 4
  4 1 2
  4 1 3
  4 1 4
  4 2 3
  4 2 4
  4 3 4

评测用例规模与约定

对于 20% 的评测用例,1 <= n, m <= 5;
  对于 50% 的评测用例,1 <= n, m <= 10;
  对于 80% 的评测用例,1 <= n, m <= 100;
  对于所有评测用例,1 <= n, m <= 1000。

题解一:暴力+dfs

  这题也可以使用暴力+dfs的方法进行一个暴力的搜索符合要求的序列,但是在本题的数量级中,此种解法必然超时,所以在此就不贴代码了,无意义。

题解二:动态规划(一)基本动态规划

  这题既然暴力是无法过所有样例的话,那我们就需要寻求其他的解法了。这里先写一个动态规划的解法,我们来分析我们的目前的问题。问题为:如果一个序列的奇数项都比前一项大,偶数项都比前一项小,则称为一个摆动序列。即 a[2i]<a[2i-1], a[2i+1]>a[2i]。小明想知道,长度为 m,每个数都是 1 到 n 之间的正整数的摆动序列一共有多少个。

  我们使用dp[i][j]来表示在第i位数选择j时,可以选择的数量。那么对于任意的dp[i][j],我们可以根据题意分为两种情况,即i为奇数和i为偶数。

  当i为奇数时,根据题意,奇数项是要比前一项大,则我们的第i位数的选择数为j时,我们可以组成的序列的数量为:第i-1位时选择,数为1到j-1的和,即dp[i][j] = dp[i-1][1] + dp[i-1][2] + ...... + dp[i-1][j-1]
  当i为偶数时,根据题意,偶数项要比前一项小,则我们的第i位数为j时,我们可以组成的序列的数量为:第i-1位时,数为j+1到n的和,即dp[i][j] = dp[i-1][j+1] + dp[i-1][j+2] + ....... + dp[i-1][n]

  这样我们动态规划的状态转移方程就出来了:

  • 当i为奇数时:dp[i][j] = dp[i-1][1] + dp[i-1][2] + ...... + dp[i-1][j-1]
  • 当i为偶数时:dp[i][j] = dp[i-1][j+1] + dp[i-1][j+2] + ....... + dp[i-1][n]

  下面我们来对dp数组进行初始化,我们发现我们每一项都需要使用到i-1位时的数据,即上一位的数据,同时我们发现当i = 1时,dp[1][j] = 1是显然成立的。即我们第一位数选择j时,只有一种序列。

  这样我们整个的算法的分析就完成了,下面我们只需要将我们的想法使用程序来进行实现即可,完整的题解代码为:

#include <iostream>
using namespace std;
int dp[1004][1004];
int main() {
    // m为长度,n为数的范围
    int m,n;
    cin>>m>>n;

    for(int i = 1; i <= n; i++)
        dp[1][i] = 1;
    for(int i = 2; i <= m; i++){
        for(int j = 1; j <= n; j++){
            if(i&1){
                int temp = 0;
                for(int k = 1; k <= j-1; k++){
                    temp = (temp + dp[i-1][k]) % 10000;
                }
                dp[i][j] = temp;
            } else{
                int temp = 0;
                for(int k = j+1; k <= n; k++){
                    temp = (temp + dp[i-1][k]) % 10000;
                }
                dp[i][j] = temp;
            }

        }
    }
    int ans = 0;
    for(int i = 0; i <= n; i++){
        ans += dp[m][i];
    }
    cout<<ans;
    return 0;
}

  需要注意的是,由于我们dp[i][j]表示的是第i为选择数j时的序列的数量,所有我们最终m位数最大值为n时,我们可以组成的序列数量为dp[m][1] + dp[m][2] + ......... + dp[m][n]

  此种解法的时间复杂度为O(mn2),即O(N3)的效率,对于本题的数据规模,我们发现只可以过80%的数据,因为题目要求对于所有评测用例,1 <= n, m <= 1000。所以我们本种动态规划解法只可以得到80%的分数,如果想要得到满分,我们还得进行进一步的优化。

题解三:动态规划(二) 进一步优化算法

  我们在题解二中发现,即使我们使用动态规划算法,也只能有O(mn^2)的时间效率,在本题的数据规模中,只可以过前80%的数据,得到80%的分数,如果想要拿全分的话,那我们就只能进一步的优化我们的算法了。

  我们分析题解二的算法,发现我们每次求解dp[i][j]的时候都进行了一次重复的计算,就是计算第1到j-1项的和或者第j+1到n项的和,这里的计算我们是可以通过优化将它省略的。这样我们就可以将我们的时间复杂度从O(n3)降到O(n2),直接降低一个数量级。如果我们要省略这里的重复的求和计算,我们就需要改变我们这里dp[i][j]存储的信息。这里我们改为使用dp[i][j]表示的信息,当i为偶数时,dp[i][j]表示第i个数选择小于等于j时的数列数;当i为奇数时,dp[i][j]表示第i个数选择大于等于j是的数列数。

  那么对于任意的dp[i][j],当i为奇数的时候,奇数项要比前一项大,所以我们当前的dp[i][j]的值为dp[i-1][j-1]+dp[i][j+1]。即dp[i][j] = dp[i-1][j-1] + dp[i][j+1],由于我们此时的dp[i][j]的值需要使用到dp[i][j+1],所以我们这里遍历倒着遍历。当i为偶数时,偶数项比前一项小,所以我们dp[i][j]的值为dp[i-1][j+1] + dp[i][j-1]。即dp[i][j] = dp[i-1][j+1] + dp[i][j-1],这里我们就需要进行正向的遍历。

在这种解法中,我们发现我们在dp[i][j]更新的遍历中,代替了累加的计算。在i为奇数的时候,我们的j从n到1进行遍历,dp[i][j] = dp[i-1][j-1] + dp[i][j+1];当i为偶数时,我们的j从1到n进行遍历,dp[i][j] = dp[i-1][j+1] + dp[i][j-1]。在dp[i][j]的计算中直接记录了累加的值,避免了大量的计算。

这时我们的状态转移方程为:

  • 当i为奇数时:dp[i][j] = dp[i-1][j-1] + dp[i][j+1]

  • 当j为偶数是:dp[i][j] = dp[i-1][j+1] + dp[i][j-1]

  这时我们就需要对dp数组进行初始化,在这种解法中,我们由于dp[i][j]与解法二中表示的意义是不一样的,所以我们这的初始化也不同。在这里我们的dp[1][j] = n - i +1

  这时我们的m如果为奇数时,所能组成的序列数量为dp[m][1],如果n为偶数,所能组成的序列数量为dp[m][n]

这样就将我们的算法的时间复杂度从O(N3)优化到了O(N2)。完整的题解代码为:

#include <iostream>
using namespace std;
int dp[1004][1004];
int main() {
    // m为长度,n为数的范围
    int m,n;
    cin>>m>>n;

    for(int i = 1; i <= n; i++)
        dp[1][i] = n - i + 1;

    for(int i = 2; i <= m; i++)
        if(i & 1)
            for(int j = n; j >= 1; j--)
                dp[i][j] = (dp[i-1][j-1] + dp[i][j+1]) % 10000;
        else
            for(int j = 1; j <= n; j++)
                dp[i][j] = (dp[i-1][j+1] + dp[i][j-1]) % 10000;

    int ans = m & 1 ? dp[m][1] : dp[m][n];

    cout<<ans;

    return 0;
}



  大家好,我叫亓官劼(qí guān jié ),在CSDN中记录学习的点滴历程,时光荏苒,未来可期,加油~博客地址为:亓官劼的博客亓官劼的博客2

本文原创为亓官劼,请大家支持原创,部分平台一直在盗取博主的文章!!!

博主目前仅在CSDN中写博客,唯一博客更新的地址为:亓官劼的博客亓官劼的博客2

亓官劼 CSDN认证博客专家 Python 全栈 数据结构与算法
大家好,我是亓官劼(qí guān jié),在博客中分享数据结构与算法、Python全栈开发、Java后端开发、前端、OJ题解及各类报错信息解决方案等经验。一起加油,用知识改变命运,未来可期。
若有事项需联系博主,可通过微信:qiguanjie2015 进行联系,有空会回复。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页
实付 99.00元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值