【AcWing算法基础课】第五章 动态规划(未完待续)(2)

简介: 给定一个如下图所示的数字三角形,从 顶部 出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点, 一直走到底层 ,要求找出一条路径,使 路径上的数字的和最大。

二、线性DP

线性DP:递推方式为线性的递推式。


1、数字三角形

题目链接: 898. 数字三角形


1.1题目描述

给定一个如下图所示的数字三角形,从 顶部 出发,在每一结点可以选择移动至其左下方的结点或移动至其右下方的结点, 一直走到底层 ,要求找出一条路径,使 路径上的数字的和最大。

7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5


输入格式


第一行包含整数 n,表示数字三角形的层数。

接下来 n 行,每行包含若干整数,其中第 i 行表示数字三角形第 i 层包含的整数。


输出格式

输出一个整数,表示最大的路径数字和。


数据范围


1≤n≤500

−10000≤三角形中的整数≤10000


输入样例:


5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5


输出样例:


30


1.2思路分析

f85549040d6889273797c9c7da9675a7_b9396b13f29342e7915104e23ddb6dfb.png


注:


DP问题下标从0开始还是从1开始

如果递推式中存在f[i-1],即下标存在i-1,下标 从1开始,将f[0]设为了边界值。

否则,下标 从0开始,防止数组下标越界。

动态规划问题时间复杂度的计算

时间复杂度 = 状态数 * 转移的计算量

1)状态表示


注:如图,从 上到下 定义每 行,从 左上到右下 定义每 列


集合:所有从起点走到(i,j)的路径。

属性:f[i][j]表示满足条件集合中的最大路径的数字和。

2)状态计算


集合划分:从左上走到(i,j)和从右上走到(i,j)。

计算:①左上:先走到左上(i-1,j-1),最大路径的数字和为f[i-1][j-1],然后再加上(i,j)点的数字,即f[i-1][j-1]+a[i][j];②右上:先走到右上(i-1,j),最大路径的数字和为f[i-1][j],然后再加上(i,j)点的数字,即f[i-1][j]+a[i][j]。将两种条件综合可得,f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j])。

1.3代码实现

#include <iostream>
using namespace std;
const int N=510,INF=1e9;
int n;
int a[N][N];
int f[N][N];
int main()
{   cin>>n;
    for(int i=1;i<=n;i++){
      for(int j=1;j<=i;j++){
      cin>>a[i][j];
  }
  }
  /*注意:不能初始化f数组全为0,因为a数组中存在负数,所以f数组也有可能是负数
  若初始化全为0,就会将f数组中的负数给忽略掉,换句话说,若都初始化为0,说明
  所有点一开始都存在最大值,我们需要使所有点一开始都不存在最大值,即初始化
  全为负无穷,而对于下面边界处理时,如果该点的左上或者右上不存在时,仍然按
  递推式计算后的值一定是负的,一定小于从存在的上方的点转移过来的最大值,所以
  从不存在的那个点转移过来的就直接被max函数所比下去了,后续处理也比较方便。
  */
  for(int i=1;i<=n;i++){
  /*对于每行的边界,需要用到的左上和右上的数,
   个别数超出了a数组初始化的范围,所以要在f数组中每
  行多初始化两个数,最前和最末两个数*/ 
      for(int j=0;j<=i+1;j++){
      f[i][j]=-INF;
  }
  }
  f[1][1]=a[1][1];
  int res=-INF;
  for(int i=2;i<=n;i++){
  for(int j=1;j<=i;j++){
    f[i][j]=max(f[i-1][j-1]+a[i][j],f[i-1][j]+a[i][j]);
  }
  }
  for(int i=1;i<=n;i++){
  res=max(f[n][i],res);
  }
  cout<<res;
  return 0;
}


2、最长上升子序列

题目链接:895. 最长上升子序列


2.1题目描述

给定一个 长度为 N 的数列,求 数值严格 单调递增的子序列 的 长度最长 是多少。


输入格式


第一行包含整数 N。


第二行包含 N 个整数,表示完整序列。


输出格式


输出一个整数,表示最大长度。


数据范围


1≤N≤1000,

−109≤数列中的数≤109


输入样例:

7
3 1 2 1 8 5 6


输出样例:

4


2.2思路分析

注:状态转移,从 一维 开始来判断能否表示,依次上升维度。

8ab14ca8fb56e845d2313b6fe00460e6_cc7ca1a79a2f4203a1dfecb41c585aeb.png


1)状态表示


集合:所有以第i个数结尾的上升子序列。

属性:f[i]表示满足条件集合中的最长序列长度。

2)状态计算


集合划分:以第i个数前一位数是原序列的第几个元素划分集合。

计算:针对每一种状态,f[j]表示除第i个数后上升子序列的长度,即以第j个数结尾的上升子序列的最大长度。所以再加上第i个数的长度,即加上1,代表以第i个数结尾的最长上升子序列的长度,即f[j]+1。所以可得 f[i]=max(f[j]+1)(j为不超过i-1的数)。

2.3代码实现

#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N],f[N];
int main(){
  cin>>n;
  for(int i=1;i<=n;i++){
  cin>>a[i];
  }
  for(int i=1;i<=n;i++){
  //初始化:只有a[i]一个数,子序列长度为1 
  f[i]=1;
  //子序列中的数大于1,依次枚举第i个数前面的数 
  for(int j=1;j<i;j++){
    //如果第j个数可以作为i的前一个数,才满足条件,进行递推 
    if(a[j]<a[i]){
    f[i]=max(f[i],f[j]+1);
    } 
  }
  }
  int ans=0;
     //找出最长上升子序列的长度 
  for(int i=1;i<=n;i++){
    ans=max(f[i],ans);
  } 
  cout<<ans;
return 0;
}



如何记录最长上升序列各元素为多少,并输出?


//如何记录最长上升子序列并且输出

//方法如下:

#include <iostream>
using namespace std;
const int N=1010;
int n;
int a[N],f[N],g[N];//利用g数组来记录序列中最后一位的前一位数字 
//利用g数组从而可以来记录出最长序列的序列具体为哪几个数 
int main(){
  cin>>n;
  for(int i=1;i<=n;i++){
  cin>>a[i];
  }
  for(int i=1;i<=n;i++){
  f[i]=1;
  g[i]=0;
  for(int j=1;j<i;j++){
    if(a[j]<a[i]){
    if(f[i]<f[j]+1){
        f[i]=f[j]+1;
        g[i]=j;   //用g数组记录i的前一位数字为第几个元素  
    }
    } 
  }
  }
  int k=1;//用k记录以第几个数字结尾 
  for(int i=1;i<=n;i++){
  //找出最长序列是以第几个数字结尾 
  if(f[k]<f[i])
     k=i;
  }
  cout<<f[k]<<endl;
  //从大到小来输出最长上升子序列具体为多少 
  for(int i=0,len=f[k];i<len;i++){
  cout<<a[k]<<" ";
  k=g[k]; 
  }
  return 0;
}


3、最长公共子序列

题目链接:897. 最长公共子序列


3.1题目描述

给定两个长度分别为 N 和 M 的字符串 A 和 B ,求 既是 A 的子序列又是 B 的子序列 的 字符串长度最长 是多少。


输入格式


第一行包含两个整数 N 和 M 。

第二行包含一个长度为 N 的字符串,表示字符串 A 。

第三行包含一个长度为 M 的字符串,表示字符串 B 。

字符串均由小写字母构成。


输出格式


输出一个整数,表示最大长度。


数据范围


1≤N,M≤1000


输入样例:


4 5
acbd
abedc


输出样例:

3


3.2思路分析

cc9dc603279e0415c30fa773b9727cef_1ea8b0cf6ba04339a984f3fb72f99b4a.png


1)状态表示


集合:所有在第一个序列的前i个字母中出现,而且在第二个序列的前j个字母中出现的子序列。

属性:f[i][j]表示满足条件集合中的最长子序列(字符串)长度。

2)状态计算


集合划分:以满足集合条件的集合中是否包含第一个序列的第i个字 a[i] 和第二个序列的第j个字母 b[j] 来划分集合,总共分为四类:不包含a[i]且不包含b[j]、不包含a[i]且包含b[j]、包含a[i]且不包含b[j]、包含a[i]且包含b[j]。

计算:针对每一种状态

1.f[i-1][j-1]。(该状态可省,因为后一种情况涵盖了它)根据集合定义可得。
   2.f[i-1][j]。因为f[i-1][j]包含了第二个序列中第j个字母存在和不存在两种情况,所以范围比较大,与其他情况有重叠,但是对于求f[i][j]的最大值无影响。
   3.f[i][j-1]。因为f[i][j-1]包含了第一个序列中第i个字母存在和不存在两种情况,所以范围比较大,与其他情况有重叠,但是对于求f[i][j]的最大值无影响。
   4.f[i-1][j-1]+1。因为该情况中每一项都包含第一个序列的第i个字母(或者说是第二个序列的第j个字母),所以长度先都减去1,就是转化为第一种情况,最后再加上去掉字母的长度1,即可得到最大长度。


综上,f[i][j]=max(f[i-1][j],f[i][j-1],f[i-1][j-1]+1)。

3.3代码实现

string实现


#include <iostream>
#include <string>
using namespace std;
const int N=1010;
int n,m;
string a="1",b="1";
int f[N][N]; 
int main(){
  cin>>n>>m;
  string temp;
  cin>>temp;
  a+=temp;
  cin>>temp;
  b+=temp;
    for(int i=1;i<=n;i++){
      for(int j=1;j<=m;j++){
      f[i][j]=max(f[i-1][j],f[i][j-1]);
      if(a[i]==b[j]){
        f[i][j]=max(f[i][j],f[i-1][j-1]+1);
    }
  }
  }
  cout<<f[n][m];
  return 0;
}


char实现


#include <iostream>
using namespace std;
const int N=1010;
int n,m;
char a[N],b[N];
int f[N][N]; 
int main(){
  cin>>n>>m;
  scanf("%s%s",a+1,b+1);
    for(int i=1;i<=n;i++){
      for(int j=1;j<=m;j++){
      //前两种情况一定存在 
      f[i][j]=max(f[i-1][j],f[i][j-1]);
      //第三种情况只有第一个序列第i个字母和第二个序列第j个字母相同,才存在 
      if(a[i]==b[j]){
        f[i][j]=max(f[i][j],f[i-1][j-1]+1);
    }
  }
  }
  cout<<f[n][m];
  return 0;
}


三、区间DP

石子合并

题目链接:282. 石子合并


1.1题目描述

设有 N 堆石子排成一排,其编号为1,2,3,…,N。每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。

每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。

例如有 4 堆石子分别为 1 3 5 2,我们可以先合并 1、2 堆,代价为 4,得到 4 5 2,又合并 1、2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;

如果第二步是先合并 2、3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。

问题是:找出一种合理的方法,使总的代价最小,输出 最小代价。


输入格式


第一行一个数 N 表示石子的堆数 N。

第二行 N 个数,表示每堆石子的质量(均不超过 1000)。


输出格式


输出一个整数,表示最小代价。


数据范围


1≤N≤300


输入样例:


4
1 3 5 2


输出样例:

22


1.2思路分析


a4ab80b9cb13c94cb24f320edd17eaf1_87f0668f058741ab96c6d50068bad44f.png

1)状态表示


集合:所有将第i堆石子到第j堆石子合并成一堆的合并方式。

属性:f[i][j]表示满足条件集合中的合并的最小代价。

2)状态计算


集合划分:以最后一次合并时的区间位置来划分,如图。


765797f25e3336146c1bdc19fd7e4ac8_746a33ffa1da43b787187fa95c1b46b0.png

计算:针对每一种状态。合并的最小代价为,合并区间左边的最小代价+合并区间右边的最小代价+最后一次合并(合并整个区间的代价),即 f[i][j]=min(f[i][k]+f[k+1][j]+s[j]-s[i-1])。(s[j]-s[i-1]利用前缀和来计算合并整个区间的代价)


1.3代码实现

#include <iostream>
using namespace std;
const int N=310;
int n;
int s[N];
int f[N][N]; 
int main(){
  cin>>n;
  for(int i=1;i<=n;i++){
  cin>>s[i];
  }
  //计算前缀和 
  for(int i=1;i<=n;i++){
  s[i]+=s[i-1];
  }
  //从大到小枚举所有状态
  //枚举长度  
  for(int len=2;len<=n;len++){
  //枚举起点
  for(int i=1;i+len-1<=n;i++){
    int l=i,r=i+len-1;
    f[l][r]=1e8;//求最小值应初始化成较大的数,否则,可能不经过递推式就输出 
    //枚举分界点 
         for(int k=l;k<r;k++){
          f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]); 
    } 
  } 
  }
  cout<<f[1][n];
  return 0;
}
目录
相关文章
|
1月前
|
算法 开发者 Python
惊呆了!Python算法设计与分析,分治法、贪心、动态规划...这些你都会了吗?不会?那还不快来学!
【7月更文挑战第10天】探索编程巅峰,算法至关重要。Python以其易读性成为学习算法的首选。分治法,如归并排序,将大问题拆解;贪心算法,如找零问题,每步求局部最优;动态规划,如斐波那契数列,利用子问题解。通过示例代码,理解并掌握这些算法,提升编程技能,面对挑战更加从容。动手实践,体验算法的神奇力量吧!
56 8
|
1月前
|
算法 Python
算法不再难!Python分治法、贪心、动态规划实战解析,轻松应对各种算法挑战!
【7月更文挑战第8天】掌握Python算法三剑客:分治、贪心、动态规划。分治如归并排序,将大问题拆解递归解决;贪心策略在每步选最优解,如高效找零;动态规划利用子问题解,避免重复计算,解决最长公共子序列问题。实例展示,助你轻松驾驭算法!**
43 3
|
5天前
|
机器学习/深度学习 算法 Java
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
这篇文章介绍了基于贪婪技术思想的Prim算法和Dijkstra算法,包括它们的伪代码描述、Java源代码实现、时间效率分析,并展示了算法的测试用例结果,使读者对贪婪技术及其应用有了更深入的理解。
算法设计(动态规划应用实验报告)实现基于贪婪技术思想的Prim算法、Dijkstra算法
|
5天前
|
算法 Java 测试技术
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
这篇文章介绍了基于动态规划法的三种算法:解决背包问题的递归和自底向上实现、Warshall算法和Floyd算法,并提供了它们的伪代码、Java源代码实现以及时间效率分析。
算法设计(动态规划实验报告) 基于动态规划的背包问题、Warshall算法和Floyd算法
|
1月前
|
算法 Python
Python算法高手进阶指南:分治法、贪心算法、动态规划,掌握它们,算法难题迎刃而解!
【7月更文挑战第10天】探索Python算法的精华:分治法(如归并排序)、贪心策略(如找零钱问题)和动态规划(解复杂问题)。通过示例代码揭示它们如何优化问题解决,提升编程技能。掌握这些策略,攀登技术巅峰。
40 2
|
1月前
|
算法 程序员 Python
算法小白到大神的蜕变之路:Python分治法、贪心、动态规划,一步步带你走向算法巅峰!
【7月更文挑战第9天】探索算法之旅,以Python解锁编程高手之路。分治法如二分查找,将复杂问题拆解;贪心算法解决活动选择,每次选取局部最优;动态规划求斐波那契数列,避免重复计算,实现全局最优。每一步学习,都是编程能力的升华,助你应对复杂挑战,迈向算法大师!
29 1
|
1月前
|
存储 算法 Python
Python算法界的秘密武器:分治法巧解难题,贪心算法快速决策,动态规划优化未来!
【7月更文挑战第9天】Python中的分治、贪心和动态规划是三大关键算法。分治法将大问题分解为小问题求解,如归并排序;贪心算法每步选局部最优解,不保证全局最优,如找零钱;动态规划存储子问题解求全局最优,如斐波那契数列。选择合适算法能提升编程效率。
41 1
|
1月前
|
存储 算法 Python
震撼!Python算法设计与分析,分治法、贪心、动态规划...这些经典算法如何改变你的编程世界!
【7月更文挑战第9天】在Python的算法天地,分治、贪心、动态规划三巨头揭示了解题的智慧。分治如归并排序,将大问题拆解为小部分解决;贪心算法以局部最优求全局,如Prim的最小生成树;动态规划通过存储子问题解避免重复计算,如斐波那契数列。掌握这些,将重塑你的编程思维,点亮技术之路。
39 1
|
1月前
|
存储 算法 大数据
Python算法高手的必修课:深入理解分治法、贪心算法、动态规划,让你的代码更智能!
【7月更文挑战第9天】在Python算法学习中,分治法(如归并排序)将大问题分解为小部分递归解决;贪心算法(如货币找零)在每步选择局部最优解尝试达到全局最优;动态规划(如斐波那契数列)通过存储子问题解避免重复计算,解决重叠子问题。掌握这三种方法能提升代码效率,解决复杂问题。
32 1
|
1月前
|
算法 索引 Python
逆袭算法界!Python分治法、贪心算法、动态规划深度剖析,带你走出算法迷宫!
【7月更文挑战第8天】分治法,如快速排序,将大问题分解并合并解;贪心算法,选择局部最优解,如活动选择;动态规划,利用最优子结构避免重复计算,如斐波那契数列。Python示例展示这些算法如何解决实际问题,助你精通算法,勇闯迷宫。
23 1