动态规划的递归写法
1、应理解动态规划是如何记录子问题的解,来避免下次遇到相同的子问题时的重复计算的。又称记忆化搜索。
2、为了避免重复计算,可以开一个一维数组,用以保存已经计算过的结果,其中dp[n]记录F[n]的结果,并用dp[n]=-1表示F[n]当前还没有计算过。
例如斐波拉契数列,一个问题必须要有重叠子问题,才能使用动态规划去解决。
int F(int n)
{
if(n==0||n==1)
return 1;
if(dp[n]!=-1)
return dp[n];
else
{
dp[n]=F(n-1)+F(n-2);//计算F(n),并保存至dp[n];
return dp[n];//返回F[n]的结果
}
}
动态规划的递推写法
1、经典数塔问题:dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j],把dp[i][j]称问题的状态,把这个式子称为状态转移方程
状态dp[i][j]只与第i+1层的状态有关,而与其他层的状态无关。数塔的最后一层的dp值总是等于元素本身,即dp[n][j]=f[n][j],把这种可以直接确定其结果的部分称为边界,而动态规划的递推写法总是从这些边界出发,通过状态转移方程扩散到整个dp数组
从最底层各位置的dp值开始,不断往上求出每一层各位置的dp值,最后就可以得到dp[1][1]
#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1000;
int f[maxn][maxn],dp[maxn][maxn];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=i;j++)
{
scanf("%d",&f[i][j]);//输入数塔
}
}
//边界
for(int j=1;j<=n;j++)
{
dp[n][j]=f[n][j];
}
//从第n-1层不断往上计算出dp[i][j]
for(int i=n-1;i>=1;i--)
for(int j=1;j<=i;j++)
{
//状态转移方程
dp[i][j]=max(dp[i+1][j],dp[i+1][j+1])+f[i][j];
}
cout <<dp[1][1] << endl;
return 0;
}
区别:使用递推写法的计算方式是自底向上,即从边界开始,不断向上解决问题,直到解决了目标问题;
使用递归写法的计算方式是自顶向下,即从目标问题开始,将它分解成子问题的组合,直到分解到边界为止
一个问题必须拥有重叠子问题和最优子结构(一个问题的最优解可以由其子问题的最优解有效地构造出来),才能使用动态规划去解决
最大连续子序列和
1、给定一个数字序列A1,A2。。。An,求i,j(1<=i<=j<=n),使得Ai+Aj最大,输出这个最大和。
步骤1:令状态dp[i]表示以A[i]作为末尾的连续序列的最大和
步骤2:因为dp[i]要求是必须以A[i]结尾的连续数列,那么只有两种情况:
①这个最大和的连续数列只有一个元素,即以A[i]开始,以A[i]结尾
②这个最大和的连续数列有多个元素,即从前面某处A[p]开始(p<i),一直到A[i]结尾
所以,状态转移方程为: dp[i]=max(A{i],dp[i-1]+A[i]).
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=10010;
int A[maxn],dp[maxn];//A[i]存放序列,dp[i]存放以A[i]结尾的最大数列
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
//读入序列
scanf("%d",&A[i]);
}
//边界
dp[0]=A[0];
for(int i=1;i<n;i++)
{
//状态转移方程
dp[i]=max(A[i],dp[i-1]+A[i]);
}
//dp[i]存放以A[i]结尾的连续数列最大和,需要遍历i得到最大的才是结果
int k=0;
for(int i=1;i<n;i++)
{
if(dp[i]>dp[k])
k=i;
}
printf("%d\n",dp[k]);
return 0;
}
最长不下降子序列(LIS)
例如:现有序列A={1,2,3,-1,-2,7,9}(下标从1开始),它的最长不下降子序列是{1,2,3,7,9},长度为5。另外,还有一些子序列是不下降子序列,比如{1,2,3},{-2,7,9}等,但都不是最长的
条件:A[i]>=A[j]&&dp[j]+1>dp[i] 则dp[i]=dp[j]+1
状态转移方程: dp[i]=max(1,dp[j]+1) (j=1,2,3,,,,i-1&&A[j]<A[i])
#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N=100;
int A[N],dp[N];
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&A[i]);
}
int ans=-1;//记录最大的dp[i]
for(int i=1;i<=n;i++)//按顺序计算出dp[i]的值
{
dp[i]=1;//边界初始条件(即假设每个元素自成一格子序列)
for(int j=1;j<i;j++)
{
if(A[i]>=A[j]&&(dp[j]+1>dp[i]))
{
dp[i]=dp[j]+1;//状态转移方程,用以更新dp[i]
}
}
ans=max(ans,dp[i]);
}
printf("%d\n",ans);
return 0;
}
最长公共子序列(LCS)
状态转移方程: ①dp[i]=dp[i-1][j-1]+1, 当A[i]=B[j]
②max(dp[i-1][j],dp[i][j-1]),当A[i]!=B[j]
边界:dp[i][0]=dp[0][j]=0(0<=i<=n,0<=j<=m)
#include <iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100;
char A[N],B[N];
int dp[N][N];
int main()
{
gets(A+1);//从下标为1开始读入
gets(B+1);
int lenA=strlen(A+1);//由于读入时从下标1开始,因此读取长度也从+1开始
int lenB=strlen(B+1);
//边界
for(int i=0;i<=lenA;i++)
{
dp[i][0]=0;
}
for(int j=0;j<=lenB;j++)
{
dp[0][j]=0;
}
//状态转移方程
for(int i=1;i<=lenA;i++)
{
for(int j=1;j<=lenB;j++)
{
if(A[i]==B[j])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
//dp[lenA][lenB]是答案
printf("%d\n",dp[lenA][lenB]);
return 0;
}
最长回文子串
例如:字符串“PATZJUJZTACCBCC”的最长回文子串为“ATZJUJZTA”,长度为9
状态转移方程: dp[i][j]=dp[i+1][j-1],当S[i]=S[j]
dp[i][j]=0,当S[i]!=S[j]
边界:dp[i][j]=1,dp[i][i+1]=(S[i]==S[i+1])?1:0
#include <iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int maxn=1010;
char S[maxn];
int dp[maxn][maxn];
int main()
{
gets(S);
int len=strlen(S),ans=1;
memset(dp,0,sizeof(dp));//dp数组初始化为0,是回文子串则为1,不是则为0
//边界
for(int i=0;i<len;i++)
{
dp[i][i]=1;
if(i<len-1)
{
if(S[i]==S[i+1])
{
dp[i][i+1]=1;
ans=2;//初始化时注意当前最长回文子串长度
}
}
}
//状态转移方程
for(int L=3;L<=len;L++)//枚举子串的长度
{
for(int i=0;i+L-1<len;i++)//枚举子串的起始端点
{
int j=i+L-1;//子串的右端点
if(S[i]==S[j]&&dp[i+1][j-1]==1)
{
dp[i][j]=1;
ans=L;//更新最长回文子串长度
}
}
}
printf("%d\n",ans);
return 0;
}
01背包问题
1、问题是这样的:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包物品的总价值最大,其中每种物品都只有一件。
令dp[i][v]表示前i件物品(1<=i<=n,0<=v<=V)恰好装入容量为v的背包中所能获得的最大价值
①不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v];
②放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,也即dp[i-1][v-w[i]]+c[i]
则状态转移方程 dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)
for(int i=1;i<=n;i++)
{
for(int v=w[i];v<=V;v++)
{
dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
}
}
2、因为dp[i][v]总是只需要dp[i-1][v]的数据,不妨开一个一维数组dp[v](即把一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),这样状态转移方程改变为: dp[v]=max(dp[v],dp[v-w[i]]+c[i])
for(int i=1;i<=n;i++)
{
for(int v=V;v>=w[i];v--)//逆序枚举v
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
}
特别说明:如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓,如果使用一维数组存放,则v的枚举必须是逆序。
完整的求解01背包问题
#include <iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=100;//物品最大件数
const int maxv=1000;//V的上限
int w[maxn],c[maxn],dp[maxn];
int main()
{
int n,V;
scanf("%d%d",&n,&V);
for(int i=0;i<n;i++)
{
scanf("%d",&w[i]);
}
for(int i=0;i<n;i++)
{
scanf("%d",&c[i]);
}
//边界
for(int v=0;v<=V;v++)
{
dp[v]=0;
}
for(int i=1;i<=n;i++)
{
for(int v=V;v>=w[i];v--)
{
//状态转移方程
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
}
}
//寻找dp[0...V]中最大的即为答案
int max=0;
for(int v=0;v<=V;v++)
{
if(dp[v]>max)
max=dp[v];
}
printf("%d\n",max);
return 0;
}
对于能够划分阶段的问题来说,都可以尝试把阶段作为状态的一维。
完全背包问题
1、问题叙述如下:有n种物品,每种物品的单位重量为w[i],价值为c[i],现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大,其中每种物品都有无穷件
状态转移方程:dp[i][v]=max(dp[i-1][v],dp[i][v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)
边界:dp[0][v]=0;(0<=v<=V)
同样可以改写成一维形式,即状态转移方程为:dp[v]=max(dp[v],dp[v-w[i]]+c[i]) (1<=i<=n,w[i]<=v<=V)
边界:dp[v]=0(0<=v<=V)
完全背包与01背包问题类似,但这里v的枚举顺序是正向枚举
for(int i=1;i<=n;i++)
{
for(int v=w[i];v<=V;v++)//正向枚举v
{
dp[v]=max(dp[v],dp[v-w[i]]+c[i]);
}
}