一、入门
以斐波那契数列为例,它的第一项为1,第二项为1,从第三项开始,每一项的值都是前面两项的和。让我们求第n项的是多少。对于这个问题,我们从最开始的递归思想来看。
int fib(int n)
{
if (n == 1 || n == 2) return 1;
return fib(n - 1) + fib(n - 2);
}
这个递归的时间复杂度是O(2^n),随着n的增大,呈指数级别增长。递归的时间复杂度之所以高,是因为它涉及了很多重复性计算。比如在计算fib(5)的时候,fib(5) = fib(4) + fib(3)。接下来计算fib(4),fib(4) = fib(3) + fib(2)。fib(4)计算完后就会再计算fib(3)。然而我们之前在计算fib(4)的时候已经算出fib(3)了。fib(3)被重复计算了两次。当n很大的时候。重复计算的工作就越来越多。
为了避免重复计算,我们可以在计算的过程中将已经计算过的值保存下来,等到再一次遇到的时候就可以直接使用。开一个dp[]数组,dp[i]存储fib(i)的值。dp[i] = 0表示还没有计算过
int dp[20];
int fib(int n)
{
if (n == 1 || n == 2) return 1;
if (dp[n]) return dp[n];//计算过,直接返回dp[n]
return dp[n] = fib(n - 1) + fib (n - 2);
}
使用数组储存状态是动态规划必备的要素。
动态规划问题解决的基本思想:
1、根绝问题所求的那一项和变量的个数,确定是一维数组,二维数组或者多维数组。
2、写出初始值,一般是某个变量为1或者0 的特殊情况时候的解。
3、通过循环,一般是两个循环中间每一层循环表示一个变量的递增情况。
4、在循环中间写出对应的递推关系表达式,求出问题的每一项。
5、根据题意得到答案,一般是数组的最后一项。
二、DP思想入门
1、01背包问题https://www.lanqiao.cn/problems/1174/learning/
#include <stdio.h>
#define max(x,y) (x>y?x:y)
int main(void)
{
int n,V;
scanf("%d %d",&n,&V);
int w[105]={0},v[105]={0};//分别储存重量和价值的数组
int dp[105][1005]={0};
for(int i=1;i<=n;i++)
scanf("%d %d",&w[i],&v[i]);
for(int i=1;i<=n;i++)//商品数量上限是n
for(int j=1;j<=V;j++)//物品体积和不能超过背包容量V
{
if(j<w[i])
dp[i][j]=dp[i-1][j];
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
printf("%d",dp[n][V]);
return 0;
}
该情况下的递推关系,x不选,v选该物品
顶部f(4,8)是一开始的状态,按层数往下依次是编号4、3、2、1的物品选不选的问题
下面这张图:(别把横纵坐标搞反了,第一行是纵坐标,第一列是横坐标)
纵坐标是背包容量;横坐标是物品可以拿的数量 值是最大价值
5 10
w v数组
1 6
2 5
3 8
5 15
3 3
dp数组
0 0 0 0 0 0 0 0 0 0 0
0 6 6 6 6 6 6 6 6 6 6
0 6 6 11 11 11 11 11 11 11 11
0 6 6 11 14 14 19 19 19 19 19
0 6 6 11 14 15 21 21 26 29 29
0 6 6 11 14 15 21 21 26 29 29
29
2、最长上升子序列
https://www.dotcpp.com/oj/problem1557.html
#include <stdio.h>
#define max(x,y) (x>y?x:y)
int main()
{
int n,m=0;
int a[1000]={0};//储存原数组
int dp[1000]={0};//储存从不同位置开始最长长度
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<=a[i])
dp[i]=max(dp[j]+1,dp[i]);
}
m=max(m,dp[i]);
printf("%d ",m);
}
printf("\n%d",m);
return 0;
}
思路分析:因为最短也是1,所以说dp数组赋值为1,
从第一个数开始,依次找到以该数为尾的数组的最长上升子序列长度储存在dp数组中
内循环中,依次从第一个数开始遍历,找到比a[i]小的a[j],并且dp[j]最大的数,然后+1赋值给dp[i].
其方程为dp[i]=max(dp[j])+1,下面为输出结果,第三行为dp数组
10
3 18 7 14 10 12 23 41 16 16
1 2 2 3 3 4 5 6 6 6
6
3、数字三角形
https://www.lanqiao.cn/problems/505/learning/
#include<stdio.h>
#include <stdlib.h>
#define max(x,y) (x>y?x:y)
int main()
{
int n;
scanf("%d", &n);
int An[n][n];//储存三角形每个位置的值
for(int i = 0; i < n; i ++)//三角形的行i的循环
{
for(int j = 0; j < i + 1; j ++)//三角形列j的循环,列最大等于该行的行数
scanf("%d", &An[i][j]);
}
for(int i = 0; i < n; i ++)//三角形的行i的循环
{
for(int j = 0; j < i + 1; j ++)//三角形列j的循环,列最大等于该行的行数
{
if(i >= 1)//不是第一列只有一个数的情况下
{
if(j == 0)//数组第一列没有左上角的值,直接加上面的值就是最大值
An[i][j] += An[i -1][j];
else if(j == i)//即数组每列最后一个没有正上方的值,直接加左上值即可
An[i][j] += An[i - 1][j - 1];
else//其余情况加其左上右上的最大值
{
int max1 =Max(An[i - 1][j - 1],An[i - 1][j]);
An[i][j] += max1;
}
}
}
}
//上面执行完后,An数组每个值表示顶点到该位置路程最大值
//向左下走与向右下走的次数相差不能超过 1,即输出第n行中间值,分行数的奇偶
if(n%2==1)
printf("%d\n",An[n-1][(n-1)/2]);
else
printf("%d\n",Max(An[n-1][(n-1)/2],An[n-1][(n-1)/2+1]));
return 0;
}
思路分析:从顶点开始,遍历输入的二维数组,将顶点到该点路径最大值重新给数组该点赋值,题目中说向左下走的次数与向右下走的次数相差不能超过 1。即规定终点只能是最后一排的中间(最后一行为奇数则最中间,偶数则中间两个都可)。
顶点的值就是该点的值,所以说未讨论,如果是像上面几个题一样重新开创新数组顶点要单独赋值,注意本题是将三角形全部靠左对齐,所以说左边一列无左上角值,右边一列无右上角值的下标和原三角形比起来略有差异。
4、跳跃
https://www.lanqiao.cn/problems/553/learning/
#include <stdio.h>
int b[105][105];//每个点的权值
int find_max(int x,int y){
int max=0;
for(int i=x;i>=1;i--){//从下往上倒推
for(int j=y;j>=1;j--){//从右往左倒推
if(!(x==i&&y==j)&&(x-i+y-j)<=3&&max<b[i][j]){
//同时满足三个条件
//1.不原地踏步2.每次跳跃在距离范围之内3.比当前max更大时才更新max
max=b[i][j];
}
}
}
return max;
}
int main(){
int n,m;
scanf("%d %d",&n,&m);
int i,j;
for(i=1;i<=n;i++){
for(j=1;j<=m;j++)
scanf("%d",&b[i][j]);
}
for(i=1;i<=n;i++){
for(j=1;j<=m;j++)
b[i][j]+=find_max(i,j);
}
printf("%d",b[n][m]+b[1][1]);
}
思路分析:第三题的衍生题,三角形变为矩阵,每次移动的选择更多,但思路相同,都需要找出顶点到每个点路径的最大值,并且储存在数组中。最后输出数组终点的值即可。
数字三角形是本身加上旁边两个相邻位置的最大值,此处也一样,一个点能到的是其右下角半径<=3的圆内的点,所以说一个点的值应该是该点值加上其左上角扇形内的最大值。所以说在find-max函数内是倒序遍历