一、dp介绍
dp适用于解决“多阶段决策问题”的最优解问题,即问题根据时间或空间特征可以划分阶段,每个阶段都需要做出决策。但不是所有此类问题都可用dp,只有满足“最优化原理”和“无后效性”才可用dp解决。
dp中主要有以下四个概念:
1.阶段:dp首先会对问题划分阶段,阶段的划分一般根据问题的时间和空间自然特征进行。
2.状态:对问题的描述称为状态,通常用数组表示。
3.决策:如何由当前阶段推出下一阶段状态的策略。
4.状态转移方程:描述由当前阶段的状态推出下一个阶段状态的方程。
分析dp类问题时先根据时间或空间划分阶段,将问题描述成多阶段决策问题。然后用一个数组来表示状态,分析当前阶段的状态如何转移到下一个阶段的状态,写出状态转移方程,确定转移边界。
二、例题
1.P1048 [NOIP2005 普及组] 采药 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
解析:
dp中的01背包问题。有限的时间中挑出价值最大的草药,以时间和草药总价值为阶段,用一个二维数组res[i][t]表示状态,存储在前i个草药中t时间内能采到的最大价值。
用t[i]存储每个的时间,v[i]存储每个的价值,下面来推转移方程。当前为res[i][t]时,如果i物品一定选不上,即当前时间t<t[i],那此时价值res[i][t]和res[i-1][t]相同。如果可以选上,那res[i][j]=max(res[i-1][j],res[i-1][j-t[i]]+v[i])。
代码:
#include<cstdio> #include<iostream> using namespace std; int t[110],v[110]; int res[110][1010]; int main() { int T,M; scanf("%d%d",&T,&M); for(int i=1;i<=M;++i) scanf("%d%d",&t[i],&v[i]); for(int i=1;i<=M;++i) { for(int j=0;j<=T;++j) { if(t[i]>j) res[i][j]=res[i-1][j]; else res[i][j]=max(res[i-1][j],res[i-1][j-t[i]]+v[i]); } } printf("%d",res[M][T]); return 0; }
2.B3637 最长上升子序列 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
解析:
用f[i]表示以第i个数字结尾的最长上升子段的最大长度。
如果构成最长上升子段,那子段其中一个数i的状态f[i]一定比上一个数f[j]多1,上一个数j又在原段中是在i之前的、比i小的最大f[j]。我们由此建立状态转移方程。
代码:
#include<cstdio> #include<iostream> using namespace std; int f[5010]; int a[5010]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;++i) scanf("%d",&a[i]); f[1]=1; for(int i=2;i<=n;++i) { int k=0; for(int j=1;j<i;++j) { if(a[j]<a[i]&&k<f[j]) k=f[j];//找出最大 } f[i]=k+1;//转移 } int ans=0; for(int i=1;i<=n;++i) ans=max(ans,f[i]); printf("%d",ans); return 0; }
3.P1115 最大子段和 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
解析:
此题我采用了非dp方法。对于一个数,如果它之前的一个区间和为正,那么自身就归于这个序列,如果不是,那自成一个新区间。令a[1]为初始区间。
代码:
#include<cstdio> #include<iostream> using namespace std; int ans=-1e9; int a[200010]; int main() { int n; scanf("%d",&n); for(int i=1;i<=n;++i) { scanf("%d",&a[i]); } ans=a[1]; int sum=a[1]; for(int i=2;i<=n;++i) { if(sum+a[i]>=a[i]) sum+=a[i]; else sum=a[i]; ans=max(sum,ans); } printf("%d",ans); return 0; }
4.LCS - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
解析:
用位置作为阶段,但有两个序列,是坐标类的,两个位置一直构成状态。
设f[i][j]表示两序列s的前i个元素、t的前j个元素的最长公共子序列的长度。
状态转移:
s[i]不在公共子序列中,f[i][j]=f[i-1][j]
t[j]不在公共子序列中,f[i][j]=f[i][j-1]
s[i]=t[j],s[i]与t[j]在公共子序列中,f[i][j]=f[i-1][j-1]+1
f[i][j]取三种情况最大值,第三种要满足s[i]=t[j]
反向查找路径
代码:
#include<cstdio> #include<iostream> #include<cstring> #include<cmath> using namespace std; char a[5000],b[5000]; int f[5000][5000]; char ans[5000]; int main() { scanf("%s%s",a+1,b+1); int l1=strlen(a+1),l2=strlen(b+1); for(int i=1;i<=l1;++i) { for(int j=1;j<=l2;++j) { f[i][j]=max(f[i][j-1],f[i-1][j]); if(a[i]==b[j]) { f[i][j]=max(f[i][j],f[i-1][j-1]+1); } } } int i=l1,j=l2; while(f[i][j]>0) { if(a[i]==b[j]) { ans[f[i][j]]=a[i]; --i,--j; } else { if(f[i][j]==f[i-1][j]) --i; else --j; } } printf("%s",ans+1); return 0; }