dp一般用于解决决策问题,比如说你的每一步都有好几种处理方式,怎么选择使得最后的结果满足或者接近于你的预期是需要考虑的问题。
所以dp问题实际上也就是最优解的问题
一般采用的方式就是将问题拆分成若干个子问题(有点像分治?),然后对每个子问题进行最优解寻找,保存结果,重复上述过程(有点像贪心?)。
根据网上大佬们所说,dp与分治的不同之处在于dp处理的是有可能相互影响的子问题,而分治处理的是毫无干系的子问题。而dp与贪心不同之处在于dp每次的结果并不是直接输出而是保存下来看有没有更优秀的解法,而贪心做出的都是不可撤回的决策(即每次选取最优解,达成局部最优)。可以认为dp判断是否是最优解根据的是它的子序列,而贪心则是每一步取最优解。
那么知道了这些,我们对dp思想已经有了一个基本的了解。
要解决dp问题,实际上也就是通过定义状态和状态转移两个步骤进行求解。
那么接下来我们就进行一些练习
接下来也就是介绍我学习到的第一个dp问题
也就是数字三角形
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
像这样一种三角形,从顶部向下走,每次只能走下面或者右下,走完全程,问你怎么走使得权值最大。
这就是很经典的dp问题
那么自然是两步,定义状态与状态转移
定义状态:mymax[i][j]表示从(1,1)出发走到mymax[i][j]所有路径中的最大值
那么mymax[3][2]也就是它的左上和正上比较,即mymax[3][2]=max(7+3+1,7+8+1)=16
然后,确定了这一个状态(把它当成已知),那么我们就会想要知道,我们通过这个状态可以得出什么?
首先,我们需要考虑哪些状态会对mymax【i】【j】这个状态产生影响
那么自然是
【i-1】【j】->【i】【j】
【i-1】【j-1】->【i】【j】
这两种情况会对其产生影响,那么可以编写程序
mymax[i][j]=max(mymax[i-1][j],mymax[i-1][j-1])+a[i][j]
通过这两个影响量我们可以转移到mymax【i】【j】去。
那么造表,对最后一行进行遍历,就解决啦~
对了,记得初始化的时候把mymax初始化为负数哈~
当然,造表求解必然是一种解法,但还有一种思考的角度就是
如果我已知第i行的最大值,那我便可以转移到第i-1行的最大值,即自底向上去寻找最优解。
mymax[i][j]是第i行第j列到最底下路径和的最大值,那么就可以将mymax[i][j]和mymax[i][j+1]中的较大者,作为第i-1行的转移项。
for(i=N;i>1;i--)
for(j=1;j<i;j++)
{
if(mymax[i][j]>mymax[i][j+1])//比较两边数据大小
mymax[i-1][j]=a[i-1][j]+mymax[i][j];//更新上一行最大权值的和
else
mymax[i-1][j]=a[i-1][j]+mymax[i][j+1];//更新上一行最大权值的和
}
那么mymax[0][0]就是我们要的结果。
还有也可以自上而下递推
#include <iostream>
#include <math.h>
using namespace std;
int a[105][105];
int b[105][105];
int n;
int main()
{
cin>>n;
for(int i=1;i<=n;i++){
for(int j=1;j<=i;j++){
cin>>a[i][j];
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
b[i][j]=-9999999;
}
}
b[1][1]=a[1][1];
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
b[i][j]=max(b[i-1][j],b[i-1][j-1])+a[i][j];
}
}
int t=-1;
for(int i=1;i<=n;i++){
if(t<=b[n][i])t=b[n][i];
}
cout<<t;
return 0;
}
继续看例题,上升序列,即在一串序列中找到最大的上升序列的长度。
问题描述
一个数的序列bi,当b1 < b2 < … < bS的时候,我们称这个序列是上升的。对于给定的一个序列(a1, a2, …, aN),我们可以得到一些上升的子序列(ai1, ai2, …, aiK),这里1 <= i1 < i2 < … < iK <= N。比如,对于序列(1, 7, 3, 5, 9, 4, 8),有它的一些上升子序列,如(1, 7), (3, 4, 8)等等。这些子序列中最长的长度是4,比如子序列(1, 3, 5, 8)。你的任务,就是对于给定的序列,求出最长上升子序列的长度。
输入数据
输入的第一行是序列的长度N(1<=N<=1000)。第二行给出的序列中的N个整数,这些整数的取值范围都是0-10000。
输出要求
最长上升子序列的长度。输入样例
7
1 7 3 5 9 4 8
输出样例
4
那自然也是两步,明确状态:dp[i]表示的是以i为终点上的最大上升序列长度
状态转移:以i为终点的最大长度是由什么转移的?显然是由它前面i-1个位子决定的,也就是以它为终点(自然有i)那么首先就要有(a[j]<a[i])的条件,如果满足,则以i为终点的也就是以j为终点加上i本身。最后对每个位子进行比较。
for(int i=1;i<=N;i++){
for(int j=1;j<i;j++){
if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]+1);
}
mymax=max(mymax,dp[i]);
}
还有经典的LCS问题
问题描述
输入两个字符串, 要你求出两个字符串的最长公共子序列长度。样例输入
abcfbc
abfcab
样例输出
4
并不一定要连续挑,而是从前面挑保证连续相同即可,可以跳着选。
定义状态dp[i][j]表示S1中前i个字符和S2中前j个字符时得到的最大的长度,当i=0或j=0时dp初始化为0
转移:首先i,j要相同,那么自然是它的前面的位子加1,如果不同,此时第i,j个位子自然是前面dp[i-1][j]和dp[i][j-1]中的较大值。即在这两种情况下不会因为你的i或者j增加了一个长度而发生改变。
int len1 = s1.length();
int len2 = s2.length();
for (int i = 0; i <= len1; i++)
dp[i][0] = 0;
for (int j = 0; j <= len2; j++)
dp[0][j] = 0;
for(int i=0;i<=len1;i++){
for(int j=0;j<=len2;j++){
if(s1[i]==s2[j])dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
另一类经典的DP问题就是背包问题,详情见我之前的文章,这里就不多赘述了哈
把链接附上刷题之路:背包问题的探索_m0_52157848的博客-CSDN博客
刷题记录
1、印章
问题描述
共有n种图案的印章,每种图案的出现概率相同。小A买了m张印章,求小A集齐n种印章的概率。
#include<stdio.h>
#include<math.h>
#include<algorithm>
#include<string.h>
#include<iostream>
using namespace std;
double dp[30][30];//表示i张印章中有j种类型
int main(){
int n,m;
cin>>n>>m;
double p=1.0/n;
memset(dp,0,sizeof(dp));
for(int i=1;i<=m;i++){//i张印章
for(int j=1;j<=n;j++){//j种印章
if(i<j)dp[i][j]=0;//购买数小于种类数不可能发生
else if(j==1)dp[i][j]=pow(p,i)*n; //这里的意思不是说总共只有一种类型的印章,而是拿到手上只有一种类型印章的概率
else dp[i][j]=dp[i-1][j]*(1.0*j/n)+dp[i-1][j-1]*(1.0*(n-j+1)/n);
}
}
printf("%.4lf",dp[m][n]);
return 0;
}
2、拿金币
极其罕见能让我一遍过的题目,很适合刚接触dp的人做
问题描述
有一个N x N的方格,每一个格子都有一些金币,只要站在格子里就能拿到里面的金币。你站在最左上角的格子里,每次可以从一个格子走到它右边或下边的格子里。请问如何走才能拿到最多的金币。
#include<iostream>
#include<string.h>
using namespace std;
typedef long long ll;
ll dp[1005][1005];
ll a[1005][1005];
int main(){
ll n;
scanf("%lld",&n);
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n;j++){
scanf("%lld",&a[i][j]);
}
}
memset(dp,0,sizeof(dp));
dp[1][1]=a[1][1];
for(ll i=1;i<=n;i++){
for(ll j=1;j<=n;j++){
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+a[i][j];
}
}
printf("%lld",dp[n][n]);
return 0;
}
难在看出这题是用dp思想,在不考虑马存在的情况下,dp[i][j]=dp[i-1][j]+dp[i][j-1]
#include <stdio.h>
#include <iostream>
#include<algorithm>
#include<string.h>
#include <map>
using namespace std;
typedef long long ll;
ll dp[100][100];
ll p[100][100];
int main()
{
int x1,y1,x2,y2;
cin>>x1>>y1>>x2>>y2;
x1+=2; x2+=2; y1+=2; y2+=2;
p[x2-1][y2-2]=1;
p[x2-2][y2-1]=1;
p[x2+1][y2+2]=1;
p[x2+2][y2+1]=1;
p[x2-1][y2+2]=1;
p[x2-2][y2+1]=1;
p[x2+1][y2-2]=1;
p[x2+2][y2-1]=1;
p[x2][y2]=1;
memset(dp,0,sizeof(dp));
int flag;
dp[2][2]=1;
for(int i=2;i<=x1;i++){
for(int j=2;j<=y1;j++){
if(p[i][j])continue;
if(i==2&&j==2)continue;
else dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
cout<<dp[x1][y1];
return 0;
}
dp加上一个选择
#include <bits/stdc++.h>
using namespace std;
int dp[505][505];
int d[505],a[505];
int main(){
int n,l,k;
cin>>n>>l>>k;
for(int i=1;i<=n;i++)cin>>d[i];
for(int i=1;i<=n;i++)cin>>a[i];
d[++n]=l;
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++){
dp[i][j]=1e9+5;
}
}
dp[1][1]=0;
for(int i=2;i<=n;i++){
for(int j=1;j<=i;j++){
for(int k=1;k<i;k++){
dp[i][j]=min(dp[i][j],dp[k][j-1]+(d[i]-d[k])*a[k]);
}
}
}
int temp=1e9;
for(int i=0;i<=k;i++){
temp=min(temp,dp[n][n-i]);
}
cout<<temp;
return 0;
}