动态规划DP总结

1.前言

本篇文章是为了准备蓝桥杯而去写的一篇动态规划dp的笔记,包括知识点,模板,和例题。

2.DP简介

动态规划,无非就是利用历史记录,来避免我们的重复计算。而这些历史记录,我们得需要一些变量来保存,一般是用一维数组或者二维数组来保存。用动态规划解决的问题必须具有三大特征:1.具备最优子结构 2.无后效性 3.有重叠子问题

3.DP模板

1.定义状态:定义状态数组dp[i],通常用于问题的求解
2.寻找状态转移方程:我们要计算 dp[n] 时,是可以利用 dp[n-1],dp[n-2]…..dp[1],来推出 dp[n] 的,
也就是可以利用历史数据来推出新的元素值,所以我们要找出数组元素之间的关系式,
例如 dp[n] = dp[n-1] + dp[n-2],这个就是他们的关系式了。
3.初始化:虽然我们知道了数组元素之间的关系式,例如 dp[n] = dp[n-1] + dp[n-2],
我们可以通过 dp[n-1] 和 dp[n-2] 来计算 dp[n],但是,我们得知道初始值啊,
例如一直推下去的话,会由 dp[3] = dp[2] + dp[1]。而 dp[2] 和 dp[1] 是不能再分解的了,
所以我们必须要能够直接获得 dp[2] 和 dp[1] 的值,而这,就是所谓的初始值。

4.相关例题

4.1沼泽问题(一维dp)

4.1.1问题描述
在这里插入图片描述
4.1.2输入输出
在这里插入图片描述
4.1.3解题思路
第一步:定义状态dp[i]为到达第i米处的方案数,本体要求到达第n米处,即求dp[n];
设置v[i],V[i]=1表示第i米处有沼泽
第二步:状态转移方程,易得到达第i米处有两种可能,一种是从第i-1米处跳过来的,一种是从第i-2米处跳过来的。
所以到达第i米处的方案数即为dp[i]=dp[i-1]+dp[i-2]。同时对于是沼泽的点,即v[i]=1的点,不用加上。
第三步:初始化,将v[0],v[1],v[2]的值初始化,注意讨论第0,1米处是否为沼泽的情况。
4.1.4AC代码

#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn],v[maxn],ans,n,m;
int main() {
	cin>>n>>m;
	int temp;
	for(int i=0; i<m; i++) cin>>temp,v[temp]=1;
	dp[0]=0;
	if(!v[0])dp[1]=1;
	if(!v[1]) dp[2]=2;
	else dp[2]=1;
	for(int i=3; i<=n; i++) {
		if(!v[i]) {
			if(!v[i-1]) dp[i]=(dp[i]+dp[i-1])%int(1e9+7);
			if(!v[i-2]) dp[i]=(dp[i]+dp[i-2])%int(1e9+7);
		}
	}
	cout<<dp[n];
}

4.2头疼的数列(二维dp)

4.2.1题目描述
题意为:一串只有0和1的数列,你可以有两种操作:1.将某个位置的1变成0,或者0变成1(称为翻转)。2.将从第一个数到某个数的所有位置上的数都翻转。问最小需要多少步可以把数列变成全0
4.2.2输入输出
在这里插入图片描述
4.2.3解题思路
第一步:定义状态,使用dp[i][0]和dp[i][1]表示到位置i全部元素变为0或1的操作次数。题目转化为找出dp[i][0]与dp[i-1][0]与dp[i-1][1]的关系,求dp[n][0].
第二步:构造状态转移方程,
当a[i]=1时:要使a[1]到a[i]全为0,要么使a[1]到a[i-1]全为0,翻转a[i]。要么使a[1]到a[i-1]全部为1,翻转a[1]到a[i]。
要使a[1]到a[i]全为1,要么使a[1]到a[i-1]全为1,要么使a[1]到a[i]全为0,翻转a[1]到a[i]。
同理可得a[i]=0时:要使a[1]到a[i]全为0,要么使a[1]到a[i-1]全为0,要么使a[1]到a[i]全为1,翻转a[1]到a[i]。
要使a[1]到a[i]全为1,要么使a[1]到a[i-1]全为0,翻转a[1]到a[i-1],要么使a[1]到a[i-1]全为1,翻转a[i]。
第三步:初始化。分为a[i]=1时:dp[i][0]=1,dp[i][1]=0.a[i]=0时,dp[i][0]=0,dp[i][1]=1。
4.2.4ac代码

#include<bits/stdc++.h>
#define maxn 100005
using namespace std;
int dp[maxn][2],a[maxn],ans,n,m;
int main() {
	cin>>n;
	for(int i=1;i<=n;i++) cin>>a[i];
	if(a[1]==1) dp[1][1]=0,dp[1][0]=1;
	if(a[1]==0) dp[1][0]=0,dp[1][1]=1;
	for(int i=2;i<=n;i++){
		if(a[i]==1){
			dp[i][0]=min(dp[i-1][0]+1,dp[i-1][1]+1);
			dp[i][1]=min(dp[i-1][1],dp[i-1][0]+1);
		}
		if(a[i]==0){
			dp[i][0]=min(dp[i-1][0],dp[i-1][1]+1);
			dp[i][1]=min(dp[i-1][0]+1,dp[i-1][1]+1);
		}
	}
	cout<<dp[n][0];
}

4.3机器人(二维dp)

4.3.1问题描述
在这里插入图片描述
4.3.2输入输出
输入一个m和n表示m×n的网格,输出一个值代表不同的路径数
4.3.3解题思路
定义状态为dp[i][j]:代表到达第i行第j列所有的方案数,要求dp[m][n]
构造状态转移方程:对于dp[i][j],有两种方式到达这个点,分别是从dp[i-1][j]和dp[i][j-1],所以dp[i][j]=dp[i-1][j]+dp[i][j-1]
初始化:对于第一行和第一列的所有元素,都只有一种方案可走,此时dp=1
4.3.4AC代码

#include<bits/stdc++.h>
using namespace std;
#define maxn 105
int dp[maxn][maxn],n,m;
int main(){
	cin>>m>>n;
	for(int i=1;i<=m;i++) dp[i][1]=1;//初始化
	for(int i=1;i<=n;i++) dp[1][i]=1;//初始化
	for(int i=2;i<=m;i++)
	for(int j=2;j<=n;j++){
		dp[i][j]=dp[i-1][j]+dp[i][j-1];
	}
	cout<<dp[m][n];
}

4.4地宫寻宝(四维dp)

4.4.1
问题描述
在这里插入图片描述
4.4.2输入输出
在这里插入图片描述
4.2.3解题思路
第一步,定义状态:定义dp[i][j][k][c],代表走到(i,j),手上有k件物品,最大价值为c。
第二步,状态转移方程:
拿物品:dp[i][j][k][c]=dp[i-1][j][k-1][w[i][j]]+dp[i][j-1][k-1][w[i][j]]
不拿物品:dp[i][j][k][c]=dp[i-1][j][k][c]+dp[i][j-1][k][c]
则ap[i][j][k][c]=(拿物品+不拿物品)%mod。
第三步,初始化:当i= =1&&j= =1时,k= =0或者k= =1并且c>a[i][j]时,dp[i][j][k][c]=1
最大时间复杂度:O(T)=O(ijkc)=O(50501313)=O(422500)
闫氏DP分析法:
在这里插入图片描述

4.2.4ac代码

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL dp[51][51][13][13],mod=1000000007;
int a[55][55],n,m,kk;
int main() {
	cin>>n>>m>>kk;
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++) cin>>a[i][j];
	for(int i=1;i<=n;i++) for(int j=1;j<=m;j++)
	for(int k=0;k<=kk;k++) for(int c=0;c<=12;c++){//此处需要枚举出c的所有情况
    LL na=0,buna=0;//每次循环时更新na和buna的情况 
	if(i==1 && j==1){
		if(k==0 || (k==1 && c>a[i][j])) dp[i][j][k][c]=1;
		continue; 
	} 
	if(k && c>a[i][j])na=dp[i-1][j][k-1][a[i][j]]+dp[i][j-1][k-1][a[i][j]];
	buna=dp[i-1][j][k][c]+dp[i][j-1][k][c];
	dp[i][j][k][c]=na+buna;
	dp[i][j][k][c]%=mod;
}
cout<<dp[n][m][kk][12];
}

4.5括号序列(二维dp+数学思维)

4.5.1题目描述
在这里插入图片描述
4.5.2输入输出样例
在这里插入图片描述
4.5.3解题思路
本题较难,首先,弄清楚对于括号序列的合法性:对于一个括号序列,在任意位置处,只有他当前的及其前面的所有左括号大于等于右括号,序列才有可能合法。
然后就是对于需要添加的括号,有两种可能,一种是添加左括号,一种是右括号,这两个可以分别添加,是相互独立事件,两种方案数相乘即为最终方案数。
两个方案思想相同,先来看第一个方案(第二个也同理),对于需要添加的左括号,我们如果遇到一个右括号,就给他添加一个左括号,这样就能保证当前合法
定义状态:dp[i][j],表示走到第i个括号,左括号比右括号多j个使之序列合法。
状态转移方程:如果遇到左括号,则不做处理,当前第i个位置比第i-1个位置j多了1。
即pdp[i][j]=dp[i-1][j-1]。
如果遇到了右括号,如果这个加上这个右括号的序列本身合法,那么我们仅需添加0个左括号就能使其合法,如果不合法就需要添加刚好使得其合法的左括号甚至可以更多。
dp[i][j] = dp[i-1][0] + dp[i-1][1] + … + dp[i-1][j] + dp[i-1][j+1]
显然超时,但易得:dp[i][j-1] = dp[i-1][0] + dp[i-1][1] + … dp[i-1][j]
所以dp[i][j] = dp[i-1][j+1] + dp[i][j-1]
初始化:dp[0][0]=1 这是显而易见的
4.5.4AC代码

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int MOD=1000000007;
const int N=5010;
LL dp[N][N];
char str[N];
int len;
LL func()
{
    memset(dp,0,sizeof(dp));
    dp[0][0]=1;
    for(int i=1;i<=len;i++)//一个括号一个括号的判断
    {
        if(str[i]=='(')
        {
            for(int j=1;j<=len;j++)
            {
                dp[i][j]=dp[i-1][j-1];//不用考虑dp[i][0] 因为dp[i-1][-1]是不合法的情况 不存在 为0
            }
        }
        else
        {
            dp[i][0]=(dp[i-1][0]+dp[i-1][1])%MOD;//特判防止越界 这里数据短,用的是优化前的推断
            for(int j=1;j<=len;j++)
            {
                 dp[i][j]=(dp[i-1][j+1] + dp[i][j-1])%MOD;
            }
        }
    }
    for(int i=0;i<=len;i++)
        if(dp[len][i]) return dp[len][i];//我们需要的就是长度为len添加括号的合法情况,而从前往后遍历出现的第一个有可能的情况就是需要括号数最少的情况,因为左括号可以加很多个,我们仅需添加最少的情况
        return -1;
}
int main()
{
    scanf("%s",str+1);//从下标为1开始
    len=strlen(str+1);
    LL l=func();
    reverse(str+1,str+len+1);
    for(int i=1;i<=len;i++)
    {
        if(str[i]=='(') str[i]=')';
        else str[i]='(';
    }
    LL r=func();
    cout<<l*r%MOD;
    return 0;
}

4.6砝码称重(二维dp)

4.6.1问题描述
在这里插入图片描述
4.6.2输入输出
在这里插入图片描述
4.6.3解题思路
定义状态:dp[i][j]代表当前添加了i个砝码,能否称出为j的重量,若能,则dp[i][j]=1,反之为0
状态转移方程:分两种情况,第一种是当前放的砝码在之前组合出的值的不同侧(即向天平高处放砝码),也就是说,若前i-1个砝码,能称出j+a[i]的重量,那么当添加上第i个砝码在天平高处时,此时称出的重量就为j,即a[i][j]=1。
第二种当前砝码放在之前组合出的值的同侧(即向天平低处放砝码),也就是说,若前i-1个砝码,能称出abs(j+a[i])的重量值,那么当添加上第i个砝码在天平低处时,此时称出的重量为j,即a[i][j]=1。
初始化:若ap[i][j]本身为0,若j==a[i],
4.6.4AC代码

#include <bits/stdc++.h>
using namespace std;
bool dp[106][200006];
int a[106];
int main()
{
    int i,j,n,sum = 0; cin>>n;
   for(i = 1;i <= n;i++){
      cin>>a[i];
      sum+= a[i];
   }
   for(i = 1;i <= n;i++){
       for(j = 1;j <= sum;j++){
           dp[i][j] = dp[i-1][j];
           if(!dp[i][j]){
               if(j == a[i]) dp[i][j] = 1; 
                //判定的值与当前选择的砝码相等
               if(dp[i-1][j + a[i]])  dp[i][j] = 1;
      //检查当前i砝码与上一个状态(之前组合出的值)放在不同侧,是否可以组成j。
               if(dp[i-1][abs(j-a[i])]) dp[i][j] = 1;
      //检查当前i砝码与上一个状态(之前组合出的值)放在同一侧,是否可以组成j。
           }
       }
   }
   long long ans = 0;
    for(j = 1;j <= sum;j++) if(dp[n][j]) ans++;
    cout<<ans;
    return 0;
}

5.总结

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

chase__young

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值