<蓝桥杯软件赛>零基础备赛20周--第18周--动态规划初步

报名明年4月蓝桥杯软件赛的同学们,如果你是大一零基础,目前懵懂中,不知该怎么办,可以看看本博客系列:备赛20周合集
20周的完整安排请点击:20周计划
每周发1个博客,共20周。
在QQ群上交流答疑:

在这里插入图片描述

文章目录

  • 1. 动态规划的概念
  • 2. 动态规划的两种编码方法
  • 3. DP设计基础
  • 4. 常见线性DP
  • 5. DP习题

第18周:动态规划初步

  动态规划(Dynamic Programming,DP)是Richard Bellman于1950年代发明的应用于多阶段决策的数学方法。和贪心、分治一样,动态规划是一种解题的思路,而不是一个具体的算法知识点。动态规划是地地道道的“计算思维”,非常适合用计算机实现,可以说是独属于计算机学科的计算理论。动态规划是一种需要学习才能获得的思维方法。像贪心、分治这样的方法,在生活中,或在其他学科中有很多类似的例子,很容易联想和理解。但动态规划不是,它是一种生活中没有的抽象计算方法,没有学过的人很难自发产生这种思路。
  DP是算法竞赛中最常见的考点之一,蓝桥杯大赛的每一场比赛,每次必有DP题目,少则一题,多则数题。以2023年第十四届蓝桥杯省赛为例,
  C/C++:A组“更小的数”、B组“接龙数列”、C组“填充”、研究生组“奇怪的数”。
  Java:A组“高塔”、B组“ 数组分割,蜗牛,合并石子”、C组“填充”、研究生组“奇怪的数”。
  Python:A组“奇怪的数”、B组“松散子序列,保险箱,树上选点”、C组“填充,奇怪的数”、研究生组“填充,高塔”。
  能做DP题目,就有蓝桥杯省赛二等奖的实力。

1. 动态规划的概念

  本节以斐波那契数为例说明DP的概念和编程实现。
  斐波那契数列是一个递推数列,前几个数是1、1、2、3、5、8,第n个数等于第n-1个和第n-2个相加。斐波那契数的递推公式是:
    fib(n) = fib(n-1) + fib(n-2)
  斐波那契数列又称为兔子数列。设一对兔子每月能生一对小兔子,小兔子在出生的第一个月没有生殖能力,第二个月便能生育,且所有兔子都不会死亡。从第一对刚出生的兔子开始,问12个月以后会有多少对兔子。
在这里插入图片描述

  斐波那契数列也常常用楼梯问题来举例。一次可以走一个台阶或者两个台阶,问走到第n个台阶时,一共有多少种走法?要走到第n级台阶,分成两种情况,一种是从n-1级台阶走一步过来,一种是从n-2级台阶走两步过来。这就是斐波那契数列的递推公式。
  计算斐波那契数列,可以直接用递推公式计算。这里为了说明动态规划的思想,用递归来求斐波那契数,代码如下。

int fib (int n){if (n == 1 || n == 2)      return 1;return (fib (n-1) + fib (n-2));  //递归以2的倍数增加
}

  为了解决总体问题fib(n),将其分解为两个较小的子问题fib(n-1)和fib(n-2),这就是DP的应用场景。
  有一些问题有两个特征:重叠子问题、最优子结构。用DP可以高效率地处理具有这2个特征的问题。
  (1)重叠子问题
  首先,子问题是原大问题的小版本,计算步骤完全一样;其次,计算大问题的时候,需要多次重复计算小问题。这就是“重叠子问题”。以斐波那契数为例,用递归计算fib(5),分解为图示的子问题。
在这里插入图片描述
        图1 计算斐波那契数
  其中fib(3)计算了2次,其实只算1次就够了。
  一个子问题的多次重复计算,耗费了大量时间。用DP处理重叠子问题,每个子问题只需要计算一次,从而避免了重复计算,这就是DP效率高的原因。
  (2)最优子结构
  最优子结构的意思是:首先,大问题的最优解包含小问题的最优解;其次,可以通过小问题的最优解推导出大问题的最优解。在斐波那契问题中,把数列的计算构造成fib(n) = fib(n-1) + fib(n-2),即把原来为n的大问题,减小为n-1和n-2的小问题,这是斐波那契数的最优子结构。

2. 动态规划的两种编码方法

  处理DP中的大问题和小问题,有两种思路:自顶向下(Top-Down,先大问题再小问题)、自下而上(Bottom-Up,先小问题再大问题)。
  编码实现DP时,自顶向下用带记忆化搜索的递归编码,自下而上用递推编码。两种方法的复杂度是一样的,每个子问题都计算一遍,而且只计算一遍。
  (1)自顶向下与记忆化
  先考虑大问题,再缩小到小问题,递归很直接地体现了这种思路。为避免递归时重复计算子问题,可以在子问题得到解决时,就保存结果,再次需要这个结果时,直接返回保存的结果就行了。这种存储已经解决的子问题的结果的技术称为“记忆化(Memoization)”。
  以斐波那契数为例,记忆化代码如下:

int memoize[N];                                  //保存结果
int fib (int n){if (n == 1 || n == 2)  return 1;if(memoize[n] != 0) return memoize[n]; //直接返回保存的结果,不再递归memoize[n]= fib (n - 1) + fib (n - 2);       //递归计算结果,并记忆return memoize[n];
}

  在这个代码中,一个斐波那契数只计算一次,所以总复杂度是O(n)的。
  (2)自下而上与制表递推
  这种方法与递归的自顶向下相反。这种“自下而上”的方法,先解决子问题,再递推到大问题。通常通过填写表格来完成,编码时用若干for循环语句填表。根据表中的结果,逐步计算出大问题的解决方案。
  用制表法计算斐波那契数,维护一个一维表dp[],记录自下而上的计算结果,更大的数是前面两个数的和。
在这里插入图片描述
代码:

const int N = 255;
int dp[N];
int fib (int n){dp[1] = dp[2] =1;for (int i=3;i<=n;i++)  dp[i] = dp[i-1] +dp[i-2];return dp[n];
}

  把表格dp[]称为DP状态,dp[]的转移方程是dp[i] = dp[i-1] +dp[i-2]。
  代码的复杂度显然也是O(n)的。
  对比“自顶向下”和“自下而上”这两种方法,“自顶向下”的优点是能更宏观地把握问题、认识问题的实质,“自下而上”的优点是编码更直接。两种编码方法都很常见。
  能用DP求解的问题,一般是求方案数,或者求最值

3. DP设计基础

  用下面的例子讲解DP的基本问题:状态设计、状态转移、编码实现。


更小的数 2023年第十四届省赛C/C++大学A组,10分
【题目描述】 有一个长度均为n且仅由数字字符0 ~ 9组成的字符串,下标从0到n-1。你可以将其视作是一个具有n位的十进制数字num。小蓝可以从num中选出一段连续的子串并将子串进行反转,最多反转一次。小蓝想要将选出的子串进行反转后再放入原位置处得到的新的数字numnew满足条件numnew < num。请你帮他计算下一共有多少种不同的子串选择方案。只要两个子串在num中的位置不完全相同我们就视作是不同的方案。注意,我们允许前导零的存在,即数字的最高位可以是0,这是合法的。
【输入描述】输入一行包含一个长度为n的字符串表示num(仅包含数字字符0 ∼9),从左至右下标依次为 0 ∼n−1。对于20%的评测用例,1≤n≤100;对于40%的评测用例,1≤n≤1000;对于所有评测用例,1≤n≤5000。
【输出描述】输出一个整数表示答案。
输入样例:
210102 输出样例:
8


  如果读者没学过动态规划,也能用模拟法做这一题。遍历出每个子串,判断这个子串反转后是否合法,也就是判断是否有numnew < num。统计所有合法的情况,就是答案。代码很容易写。

#include<bits/stdc++.h>
using namespace std;
int main() {string s;  cin >> s;int ans = 0;for (int i = 0; i < s.size(); i++) {for (int j = i + 1; j < s.size(); j++) {string tmp = s;reverse(tmp.begin()+i, tmp.begin()+j+1);  //反转子串s[i,j]if (tmp < s)  ans++;}}cout << ans << endl;return 0;
}

java代码

import java.util.Scanner;
public class Main {public static void main(String[] args) {Scanner sc = new Scanner(System.in);String s = sc.next();int ans = 0;for (int i = 0; i < s.length(); i++) {for (int j = i + 1; j < s.length(); j++) {StringBuilder tmp = new StringBuilder(s);tmp.replace(i, j + 1, new StringBuilder(s.substring(i, j + 1)).reverse().toString());if (tmp.toString().compareTo(s) < 0) ans++;                }}System.out.println(ans);}
}

python

s = input()
ans = 0
for i in range(len(s)):for j in range(i + 1, len(s)):tmp = list(s)tmp[i:j+1] = reversed(tmp[i:j+1])if ''.join(tmp) < s:ans += 1
print(ans)

  用两种for循环遍历所有的子串。用库函数reverse()反转子串,如果不会用这个函数,也可以自己写一个反转子串的函数。
  代码的计算复杂度是多少?两重for循环是 O ( n 2 ) O(n^2) O(n2),reverse()是O(n)的,总复杂度为 O ( n 3 ) O(n^3) O(n3)。只能通过40%的测试。
  下面用DP求解本题,复杂度为 O ( n 2 ) O(n^2) O(n2),通过100%的测试。

1、DP状态设计
  本题可以用DP吗?它有DP的重叠子问题和最优子结构吗?
  在模拟法中,需要检查每个子串,为了应用DP,考虑这些子串之间有没有符合DP要求的关系,请读者思考。下面的DP状态设计和DP转移方程体现了子串之间的DP关系。
  DP状态:定义二维数组dp[][],dp[i][j]表示子串s[i]~s[j]反转之后是否大于反转前的子串。dp[i][j]=1表示反转之后变小,符合要求;dp[i][j]=0表示反转之后没有变小。
  在DP题目中,建议把状态命名为dp,这有利于与队友的交流。队友看到dp这个关键字,用不着解释,就知道这是一道DP题,dp是定义的状态,而不是别的意思。

2、DP转移方程
  对于每个子串,比较它的首尾字符s[i]和s[j],得到状态转移方程。
  (1)若s[i] > s[j],说明反转后的子串肯定小于原子串,符合要求,赋值dp[i][j] = 1。
  (2)若s[i] < s[j],说明反转后的子串肯定大于原子串,赋值dp[i][j] = 0。
  (3)若s[i] = s[j],需要继续比较s[i+1]和s[j-1],有dp[i][j] = dp[i+1][j-1]。
  第(3)条的dp[i][j] = dp[i+1][j-1]是自顶向下的思路,例如dp[1][6] = dp[2][5],dp[2][5] = dp[3][4],等等。
  计算这个递推公式时,需要先算出较小子串的dp[][],再递推到较大子串的dp[][]。例如先要计算出dp[2][5],才能递推到dp[1][6]。最小子串的dp[][],例如dp[1][1]、dp[1][2]、dp[2][2]、dp[2][3]等,它们不再需要递推,因为dp[1][1]=0,dp[1][2]根据(1)、(2)计算。

3、代码
  根据上述思路,读者可能很快就写出了以下代码。

#include<bits/stdc++.h>
using namespace std;
int dp[5010][5010];
int main() {string s;   cin >> s;int ans = 0;for (int i = 0; i < s.length(); i++) {         //子串从s[i]开始for (int j = i+1; j < s.length(); j++) {   //子串末尾是s[j]if (s[i] > s[j])  dp[i][j] = 1;if (s[i] < s[j])  dp[i][j] = 0;if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1];if (dp[i][j] == 1) ans++;}}cout << ans;
}

  代码的计算复杂度:两重for循环 O ( n 2 ) O(n^2) O(n2),优于前面模拟法代码的 O ( n 3 ) O(n^3) O(n3)
  代码看起来逻辑很清晰,但它其实是错误的。问题出在第7、8行的for循环。例如第7行i=0,第8行j=8时,递推得dp[0][8]=dp[1][7],但是此时dp[1][7]已经计算过吗?并没有。
  递推的时候,根据DP的原理,应该先算出小规模问题的解,再递推大规模问题的解。计算应该这样进行:
  (1)初始化:dp[][]=0,其中的dp[0][0]=0、dp[1][1]=0、…、dp[1][0]、…,在后续计算中有用。
  (2)第一轮递推:计算长度为2的子串的dp[][],即计算出dp[0][1]、dp[1][2]、dp[2][3]、…。例如计算dp[0][1],若s0>s1,则dp[0][1]=1;若s0<s1,则dp[0][1]=0;若s0=s1,则dp[0][1]=dp[1][0]=0,这里dp[1][0]=0是初始化得到的。
  (3)第二轮递推:计算长度为3的子串的dp[][],即计算出dp[0][2]、dp[1][3]、dp[2][4]、…。例如计算dp[0][2],若s0=s2,则有dp[0][2]=dp[1][1]=0,这时用到了前面得到的dp[1][1]。
  (4)第三轮递推:计算长度为4的子串的dp[][],即计算出dp[0][3]、dp[1][4]、dp[2][5]、…。例如计算dp[0][3],若s0=s3,则有dp[0][3]=dp[1][2],这时用到了前面得到的dp[1][2]。
  (5)继续递推,最后得到所有的dp[][]。
  代码应该这样写,用循环变量k表示第k轮递推,或者表示递推长度为k+1的子串:
C++代码:

#include<bits/stdc++.h>
using namespace std;
int dp[5010][5010];                     //全局数组,初始化为0
int main() {string s; cin >> s;int ans = 0;for (int k = 1; k < s.length(); k++) {        //第k轮递推。k=j-ifor (int i = 0; i+k < s.length(); i++) {  //子串从s[i]开始int j = i+k;                          //子串末尾是s[j]if (s[i] > s[j])  dp[i][j] = 1;if (s[i] < s[j])  dp[i][j] = 0;if (s[i] == s[j]) dp[i][j] = dp[i + 1][j - 1];if (dp[i][j] == 1)  ans++;}}cout << ans;
}

java代码

import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner sc = new Scanner(System.in);String s = sc.next();int[][] dp = new int[5010][5010];int ans = 0;for (int k = 1; k < s.length(); k++) {for (int i = 0; i + k < s.length(); i++) {int j = i + k;if (s.charAt(i) > s.charAt(j)) dp[i][j] = 1;                if (s.charAt(i) < s.charAt(j)) dp[i][j] = 0;if (s.charAt(i) == s.charAt(j))dp[i][j] = dp[i + 1][j - 1];                if (dp[i][j] == 1)  ans++;}}System.out.println(ans);}
}

python代码

s = input()
dp = [[0] * 5010 for _ in range(5010)]
ans = 0
for k in range(1, len(s)):for i in range(len(s) - k):j = i + kif s[i] > s[j]:    dp[i][j] = 1if s[i] < s[j]:    dp[i][j] = 0if s[i] == s[j]:   dp[i][j] = dp[i + 1][j - 1]if dp[i][j] == 1:  ans += 1
print(ans)

4、对比DP代码和模拟代码
  DP代码和模拟代码的相同处:它们都需要计算所有的子串,共 O ( n 2 ) O(n^2) O(n2)个子串。
  为什么DP代码的效率更高呢?
  (1)模拟代码对每个子串的计算是独立的。每个子串的计算和其他子串无关,不用其他子串的计算结果,自己的计算结果对其他子串的计算也没有用。每个子串需要计算O(n)次, O ( n 2 ) O(n^2) O(n2)个子串的总计算量是 O ( n 3 ) O(n^3) O(n3)的。
  (2)DP的子串计算是相关的。长度为2的子串计算结果,在计算长度为3的子串时用到;长度为3的子串计算结果,在计算长度为4的子串时用到;…等等。所以一个子串的计算量只有O(1), O ( n 2 ) O(n^2) O(n2)个子串的总计算量是 O ( n 2 ) O(n^2) O(n2)的。这就是DP利用“重叠子问题”得到的计算优化。

4. 常见线性DP

  线性DP是蓝桥杯省赛最常考核的题型。
  本博客写过类似的博文,请参考:DP概述和常见DP面试题

  非线性DP,蓝桥杯省赛可能考到的有:树形DP、状态压缩DP、数位DP。这属于较难的知识了,初学者以后再学。见专辑:DP专题

5. DP习题

  2023年第14届省赛的DP题很多,大多是线性DP,大家可以作为练习题:

  C/C++:A组“更小的数”、B组“接龙数列”、C组“填充”、研究生组“奇怪的数”。

  Java:A组“高塔”、B组“ 数组分割,蜗牛,合并石子”、C组“填充”、研究生组“奇怪的数”。

  Python:A组“奇怪的数”、B组“松散子序列,保险箱,树上选点”、C组“填充,奇怪的数”、研究生组“填充,高塔”。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/434000.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

HCS-华为云Stack-FusionSphere

HCS-华为云Stack-FusionSphere FusionSphere是华为面向多行业客户推出的云操作系统解决方案。 FusionSphere基于开放的OpenStack架构&#xff0c;并针对企业云计算数据中心场景进行设计和优化&#xff0c;提供了强大的虚拟化功能和资源池管理能力、丰富的云基础服务组件和工具…

栈和队列的动态实现(C语言实现)

✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅✅ ✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨ &#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1f33f;&#x1…

【LeetCode】112. 路径总和(简单)——代码随想录算法训练营Day18

题目链接&#xff1a;112. 路径总和 题目描述 给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径&#xff0c;这条路径上所有节点值相加等于目标和 targetSum 。如果存在&#xff0c;返回 true &#xff1b;否则&…

ENVI下基于知识决策树提取地表覆盖信息

基于知识的决策树分类是基于遥感影像数据及其他空间数据,通过专家经验总结、简单的数学统计和归纳方法等,获得分类规则并进行遥感分类。分类规则易于理解,分类过程也符合人的认知过程,最大的特点是利用的多源数据。 决策树分类主要的工作是获取规则,本文介绍使用CART算法…

第17章_反射机制(理解Class类并获取Class实例,类的加载与ClassLoader的理解,反射的基本应用,读取注解信息,体会反射的动态性)

文章目录 第17章_反射机制本章专题与脉络1. 反射(Reflection)的概念1.1 反射的出现背景1.2 反射概述1.3 Java反射机制研究及应用1.4 反射相关的主要API1.5 反射的优缺点 2. 理解Class类并获取Class实例2.1 理解Class2.1.1 理论上2.1.2 内存结构上 2.2 获取Class类的实例(四种方…

每日一题——LeetCode1346.检查整数及其两倍数是否存在

方法一 循环查找 用indexOf查找每个元素的两倍是否存在在数组中&#xff0c;找到了就直接return true&#xff0c;循环结束还没找到就return false var checkIfExist function(arr) {for(let i0;i<arr.length;i){let index arr.indexOf(arr[i]*2)if(index>0 &&…

小土堆pytorch学习笔记003 | 下载数据集dataset 及报错处理

目录 1、下载数据集 2、展示数据集里面的内容 3、DataLoader 的使用 例子&#xff1a; 结果展示&#xff1a; 1、下载数据集 # 数据集import torchvisiontrain_set torchvision.datasets.CIFAR10(root"./test10_dataset", trainTrue, downloadTrue) test_set …

CAD-autolisp(二)——选择集、命令行设置对话框、符号表

目录 一、选择集1.1 选择集的创建1.2 选择集的编辑1.3 操作选择集 二、命令行设置对话框2.1 设置图层2.2 加载线型2.3 设置字体样式2.4 设置标注样式&#xff08;了解即可&#xff09; 三、符号表3.1 简介3.2 符号表查找3.2 符号表删改增 一、选择集 定义&#xff1a;批量选择…

基于springboot+vue的校园资料分享平台(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战 主要内容&#xff1a;毕业设计(Javaweb项目|小程序等)、简历模板、学习资料、面试题库、技术咨询 文末联系获取 项目背景…

VS如何打包环境

以VS2005为例子,做好的软件需要发给客户现场升级,有时候总是因为系统,环境变量不同导致软件不能正常运行打开,这也是程序员非常头疼的问题,今天我们就一起看下打包环境变量. 这样我们的环境变量就打包到setup中了,目标机台安装即可!!!

Neo4j 国内镜像下载与安装

Neo4j 5.x 简体中文版指南 社区版&#xff1a;https://neo4j.com/download-center/#community 链接地址&#xff08;Linux版&#xff09;&#xff1a;https://neo4j.com/artifact.php?nameneo4j-community-3.5.13-unix.tar.gz 链接地址&#xff08;Windows&#xff09;&#x…

Unity中URP下额外灯角度衰减

文章目录 前言一、额外灯中聚光灯的角度衰减二、AngleAttenuation函数的传入参数1、参数&#xff1a;spotDirection.xyz2、_AdditionalLightsSpotDir3、参数&#xff1a;lightDirection4、参数&#xff1a;distanceAndSpotAttenuation.zw5、_AdditionalLightsAttenuation 三、A…