动态规划: 将一个复杂的问题分解成若干个子问题,通过综合子问题的最优解来得到原问题的最优解。需要注意的是,动态规划会将每个求解过的子问题的解记录下来,这样当下一次碰到同样的子问题的时候就可以不用重复计算。
以斐波拉契数列为例:
int F(int n){
if(n == 0 || n == 1)
return 1;
else
return F(n-1)+F(n-2);
}
事实上,这个递归过程中会涉及很多的重复计算。比如说F(5) = F(4)+F(3),接下来F(4) = F(3)+F(2)。这个时候,如果不采取措施,那么F(3)就会计算两次。为了避免重复计算,可以开一个一维数组dp,用来保存已经计算过的结果。并且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);
return dp[n];
}
}
如果一个问题可以被分解成为多个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题。动态规划通过记录重叠子问题的解,使得下次碰到相同的子问题时可以直接使用之前记录的结果,依次避免大量的重复计算。因此,一个问题必须要有重叠子问题,才能用动态规划去解决。
以如图的数塔问题为例,现在从最顶层走到最底层,每次只能走向下一层连接数字中的一个,请问最后将路径上的数字相加之后得到的数值最大是多少?
用一个二维数组f,其中f[ i ] [ j ] 表示第i层的第j个数字,比如f[1][1]=13。首先,从第一层的13开始,按照13->11->7的路径到达7后,之后要枚举7到达底层的最大和。从13->8->7到达7之后,也要枚举7到达底层的最大和。因此,不妨设一个二维数组dp,dp[3][2]表示7到达底层的最大和
从顶部开始求解,那么其实dp[1][1] = f[1][1]+max{dp[2][1],dp[2][2]}
因此,可以得到递推公式:dp[i][j] = f[i][j]+max{dp[i+1][j],dp[i+1][j+1]}。而dp[n][j] = f[n][j]是递推的边界。而递推的方法总是从这些边界开始,然后不断向上求出每一个dp的值,最后得到想要的答案。
#include
#include
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层开始,不断向上计算
for(int i = n-1 ; i >= 1 ; i--){
for(int j = 1 ; j <= i ; j++){
dp[i][j] = f[i][j] + max(dp[i+1][j],dp[i+1][j+1]);
}
}
printf("%d",dp[1][1]);
return 0;
}
显然,也可以用递归来实现。区别在于:递推的计算方式是自底而上,而递归是自顶向下。
如果一个问题的最优解可以由其子问题的最优解有效构造出来,那么称这个问题拥有最优子结构。最优子结构保证动态规划中元有问题的最优解可以由子问题的最优解推导出来。因此,一个问题必须要有最优子结构才能够用动态规划去解决问题。
综上所述,一个问题必须要有重叠子问题和最优子结构,才能够用动态规划去解决。
分治与动态规划:都是将问题分解为子问题,但是分治并没有重叠子问题。
贪心与动态规划:贪心并不要求等待子问题全部求解完毕之后再选择使用哪一个,而是用一次策略去选择一个子问题去求解,没有被选择到的子问题就被抛弃了。
题目:给定一个数字序列A1,A2,…,An,求i,j,使得Ai+…+Aj最大,输出这个最大和
样例:-2 11 -4 13 -5 -2 显然11+(-4)+13=20位最大和
思路:
dp[0]=-2
dp[1]=11
dp[2]=7(11-4)
sp[3]=20(11-4+13)
dp[4]=15(11-4+13-5)
dp[5]=13(11-4+13-5-2)
因为dp[i]要求必须以A[i]为结尾,因此就有两种情况:
(1)这个最大和的连续序列只有一个元素:A[i]
(2)这个最大和的连续序列有多个元素,最大和就是dp[i-1]+A[i]
因此,dp[i]=max{A[i],dp[i-1]+A[i]}
#include
#include
using namespace std;
const int maxn = 10010;
int A[maxn],dp[maxn];
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]);
}
int k = 0;
for(int i = 1 ; i < n ; i++){
if(dp[i]>dp[k]){
k=i;
}
}
printf("%d",dp[k]);
return 0;
}
题目:在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降的(非递减)
例如:现有序列A={1,2,3,-1,-2,7,9},下标从1开始,它的最长不下降子序列是{1,2,3,7,9},长度为5
令dp[i]表示以A[i]结尾的最长不下降子序列的长度,这样对于A[i]来说就会有两种情况:
比如说有一个序列A={1,5,-1,3},下标从1开始,现在如何直到以A[4]结尾的最长子序列呢:依次判断A[1]、A[2]、A[3],经判断,A[4]可以跟在A[1]或者A[3]后面形成LIS,长度为2。又如序列{1,2,3,-1},这里的A[4]并不能跟在任何一个元素后面,因此LIS就为-1一个元素,长度为1
代码:
#include
#include
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;//标记最大的子序列长度
for(int i = 1 ; i <= n ; 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;
}
ans = max(ans , dp[i]);
}
printf("%d",ans);
return 0;
}
题目:给定两个字符串或者数字序列A,B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续
如:字符串"sadstory"和"adminsorry"的最长公共子序列为"adsory"
思路:令dp[i][j]表示字符串A的i号位和字符串B的j号位之间的LCS长度,下标从1开始,如dp[4][5]表示"sads"和“admin”的LCS长度,那么可能就会有两种情况:
代码:
#include
#include
#include
using namespace std;
const int N = 100;
char A[N],B[N];
int dp[N][N];
int main(){
int n;
gets(A+1);//从下标为1开始读
gets(B+1);
int lenA = strlen(A+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]);
}
}
printf("%d",dp[lenA][lenB]);
return 0;
}
题目:给出一个字符串S,求出S的最长回文子串的长度
例如:字符串“PATZJUJZTACCBCC”的最长回文子串为"ATZJUJZTA",长度为9
思路:令dp[i][j]表示S[i]置S[j]的子串是否是回文子串,是则为1,不是则为0。这样就会有两种情况:
代码:
#include
#include
const int maxn = 1010;
char S[maxn];
int dp[maxn][maxn];
int main(){
gets(S);
int len = strlen(S);
int ans = 1;
memset(dp,0,sizeof(dp));
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;
}
题目:有n件物品,每件物品的重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内的总价值最大。其中每件物品只有一件
样例:
5 8//n=5 V=8
3 5 1 2 2 //w[i]
4 5 2 1 3 //c[i]
思路:令dp[i][v]表示前i件物品恰好装入容量为v的背包中所得的最大价值。对于每件物品:
代码:
#include
#include
using namespace std;
const int maxn = 100;
const int maxv = 1000;
int w[maxn],c[maxn],dp[maxv];
int main(){
int n,V;
scanf("%d%d",&n,&V);
for(int i = 1; i <= n ; i++)
scanf("%d",&w[i]);
for(int i = 1; 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]);
}
}
int max = 0 ;
for(int v = 0 ; v <= V ; v++){
if(dp[v]>max){
max = dp[v];
}
}
printf("%d\n",max);
return 0;
}
和0-1背包问题相区别的就是,每种物品都有无穷件
那么对于每件物品:
即:dp[i][v] = max(dp[i-1][v],dp[i][v-w[i]]+c[i])