重叠子问题:如果一个问题能分成多个子问题,而且这多个子问题是会重复出现的,便是重叠子问题。如求解斐波那契额数列,求F(4)与F(5)都要调用F(3),F(3)这个子问题出现多次
最优子结构:如果一个问题的最优解是由其子问题的最优解组成的话,则这个问题便具有最优子结构
状态的无后效性:一旦当前状态被确定就不会再改变
动态规划便是解决这种拥有重叠子问题和最优子结构的问题的方法,动态规划的重点是找到状态转移方程。
注:动态规划问题还需要注意边界的选取,边界是预先已经确定了结果的一系列子问题。对于动态规划可解的问题,并不是所有的状态都有无后效性,需要找到一系列无后效性的状态并建立相应的状态转移方程。
例题:有{-2,11,-4,13,-5,-2}序列,求连续子序列加和的最大值。
注,连续子序列就是形如“-2,11”、“11,-4,13”的子序列。
预先设序列数组为num数组
,num[0]=-2,num[1]=11。设以第i个数字结尾的子序列的最大值数组为dp数组
,dp[i]代表以第i个数字结尾的子序列和的最大值。
可以使用暴力枚举左右端点,也可以计算前缀和然后计算子序列和。使用动态规划,我们可以这样分解问题:所有的子序列可以划分为“以-2结尾的子序列”、“以11结尾的子序列”、“以-4结尾的子序列”等一共6种子序列,最优解就是这六大种中的一个,即问题的最优解由子问题最优解组成——最优子结构。同时在计算“以11结尾的子序列和的最大值”的时候需要考虑“以-2结尾的子序列和的最大值”,即重叠子问题。同时我们发现“以11结尾的子序列和的最大值”要么等于11自己要么等于“以-2结尾的子序列和的最大值”加上11,即dp[1]=max(dp[0]+num[1],num[1])
。由此我们可以归纳出状态转移方程dp[n]=max(dp[n-1]+num[n],num[n])
。边界也显而易见,就是dp[0]=num[0]
。
#include
#include
using namespace std;
int main(){
int n;
cin>>n;
int num[n],dp[n],maxSum;
for(int i=0;i<n;i++) cin>>num[i];
maxSum=dp[0]=num[0];//边界
for(int i=1;i<n;i++){
dp[i]=max(num[i],num[i]+dp[i-1]);//状态转移方程
if(dp[i]>maxSum) maxSum=dp[i];
}
cout<<maxSum;
return 0;
}
习题链接,AC代码:
/*
num数组存储读入的序列
dp数组存储以第i个数字结尾的子序列和最大值
start数组存储最大和序列的第一个元素
end数组存储最大和序列的最后一个元素
maxIndex存储最大序列和的下标
*/
#include
using namespace std;
int main(){
int n;
while(true){
cin>>n;
if(n==0) break;
int num[n],dp[n],start[n],end[n],maxIndex=0;
for(int i=0;i<n;i++) cin>>num[i];
start[0]=end[0]=dp[0]=num[0];//边界
for(int i=1;i<n;i++){
if(num[i]>num[i]+dp[i-1]){
dp[i]=num[i];
start[i]=num[i];
}
else{
dp[i]=num[i]+dp[i-1];
start[i]=start[i-1];
}
end[i]=num[i];
if(dp[i]>dp[maxIndex]) maxIndex=i;
}
if(dp[maxIndex]<0) cout<<"0 "<<num[0]<<" "<<num[n-1]<<endl;
else cout<<dp[maxIndex]<<" "<<start[maxIndex]<<" "<<end[maxIndex]<<endl;
}
return 0;
}
例题:有{-2,11,-4,13,-5,-1}序列,求最长不下降序列的长度,序列元素可以不连续但必须保持相对顺序。
注,不下降序列就是形如“-2,11”、“11,13”的序列。
预先设序列数组为num数组
,num[0]=-2,num[1]=11。设以第i个数字结尾的不下降序列的最大长度数组为dp数组
,dp[i]代表以第i个数字结尾的不下降序列的最大长度。
可以使用暴力枚举左右端点,也可以计算前缀和然后计算子序列和。使用动态规划,我们可以这样分解问题:所有的序列可以划分为“以-2结尾的子序列”、“以11结尾的子序列”、“以-4结尾的子序列”等一共6种子序列,最优解就是这六大种中的一个,即问题的最优解由子问题最优解组成——最优子结构。同时在计算“以11结尾的不下降序列的最大长度”的时候需要考虑“以-2结尾的不下降序列的最大长度”,即重叠子问题。
同时我们发现“以-1结尾的不下降序列的最大长度”等于所有在-1前面且不比-1大的元素对应的dp值加一的最大值,即位于第5个元素-1前面且小于等于-1有第0个元素-2、第2个元素-4、第4个元素-5,则dp[5]=max{dp[0]+1,dp[2]+1,dp[4]+1}
,即下面这段代码:
for(int j=0;j<i;j++)
if(num[i]>=num[j])//所有在第i个元素前面的元素且不比第i个元素大的元素
dp[i]=max(dp[i],dp[j]+1);//找到在第i个元素前面且不比第i个元素大的元素的dp值加1的最大值
上述代码就是状态转移方程。边界也显而易见,就是dp[0]=1
。
#include
#include
using namespace std;
int main(){
int n;
cin>>n;
int num[n],dp[n],maxLen=0;
for(int i=0;i<n;i++) cin>>num[i];
for(int i=0;i<n;i++){
dp[i]=1;//边界
//状态转移方程
for(int j=0;j<i;j++)
if(num[i]>=num[j]) dp[i]=max(dp[i],dp[j]+1);
if(dp[i]>maxLen) maxLen=dp[i];
}
cout<<maxLen;
return 0;
}
习题链接,AC代码(与上面的例题代码相同):
#include
#include
using namespace std;
int main(){
int n;
cin>>n;
int num[n],dp[n],maxLen=0;
for(int i=0;i<n;i++) cin>>num[i];
for(int i=0;i<n;i++){
dp[i]=1;//边界
//状态转移方程
for(int j=0;j<i;j++)
if(num[i]>=num[j]) dp[i]=max(dp[i],dp[j]+1);
if(dp[i]>maxLen) maxLen=dp[i];
}
cout<<maxLen;
return 0;
}
给定两字符串sadstory、adminsorry,求一个字符串,使这个字符串是前面两字符串的最长公共部分
注,子序列可以不连续。
设置二维数组dp,dp[i][j]
表示前一个字符串i号位和字符串j号位(字符串从1开始编号)之前的最长公共序列长度。
我们分析dp[i][j]
:
说明:因为我读取字符串的时候是直接读取,所以字符串下标从0开始,但dp中字符串从1开始,所以下面取字符串单个字符时,相应的下标要减一。
当A[i-1]=B[j-1]
时,说明当前位匹配,则在原先最长公共序列长度基础上加一,即dp[i][j]=dp[i-1][j-1]+1
当A[i-1]!=B[j-1]
时,说明当前位不匹配,则说明现最长公共序列等于dp[i-1][j]
与dp[i][j-1]
中的最大值(在计算dp[i][j]
之前,dp[i-1][j]
与dp[i][j-1]
已经计算完成),即dp[i][j]=max(dp[i-1][j],dp[i][j-1])
。可以得到如下状态转移方程:
if(a[i-1]==b[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
上述代码就是状态转移方程。边界也显而易见,就是dp数组任意维度为0的值都为0。
#include
#include
using namespace std;
int main(){
string a,b;
cin>>a>>b;
int dp[a.length()+1][b.length()+1];
for(int i=0;i<=a.length();i++) dp[i][0]=0;//边界
for(int i=0;i<=b.length();i++) dp[0][i]=0;//边界
for(int i=1;i<=a.length();i++)
for(int j=1;j<=b.length();j++)
//状态转移方程
if(a[i-1]==b[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
cout<<dp[a.length()][b.length()];
return 0;
}
/*
字符串A:sadstory
字符串B:adminsorry
dp数组如下:
0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 1 1 1 1 1
0 1 1 1 1 1 1 1 1 1 1
0 1 2 2 2 2 2 2 2 2 2
0 1 2 2 2 2 3 3 3 3 3
0 1 2 2 2 2 3 3 3 3 3
0 1 2 2 2 2 3 4 4 4 4
0 1 2 2 2 2 3 4 5 5 5
0 1 2 2 2 2 3 4 5 5 6
*/
习题链接,AC代码(与上面的例题代码相同):
#include
#include
using namespace std;
int main(){
string a,b;
while(cin>>a>>b){
int dp[a.length()+1][b.length()+1];
for(int i=0;i<=a.length();i++) dp[i][0]=0;
for(int i=0;i<=b.length();i++) dp[0][i]=0;
for(int i=1;i<=a.length();i++)
for(int j=1;j<=b.length();j++)
if(a[i-1]==b[j-1]) dp[i][j]=dp[i-1][j-1]+1;
else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
cout<<dp[a.length()][b.length()]<<endl;
}
return 0;
}
给定字符串aabbaa31dadeymasdas121ccbbcc
求最长回文子串的长度与最长回文子串
注:如果有多个最长回文子串,则优先打印左端点最靠左的子串
设原始字符串为s,设置二维数组dp,dp[i][j]
表示起始字符为第i个字符,结尾字符为第j个字符的字符串(字符下标从0开始)是否是回文串,0为否,1为是。
我们分析dp[i][j]
:
若s[i]==s[j]
,说明从i到j的字符串有可能是回文串,此时需要判断除了首尾字符的子串是否是回文串,所以此时dp[i][j]=dp[i+1][j-1]
,从i+1到j-1是回文串的话则从i到j的字符串也是回文串,否则就不是。
若s[i]!=s[j]
,说明从i到j的字符串一定不是回文串,因为首尾字符就已经不相同了。
综上所述,可以得到如下状态转移方程:
if(s[i]==s[j]) dp[i][j]=dp[i+1][j-1];
else dp[i][j]=0;
上述代码就是状态转移方程。
我们分析边界:
1.一个字符也是回文串,所以dp[i][i]=1
2.因为dp[i][j]
代表从i到j的字符串,若i>j,则该字符串不存在,所以当i>j,dp[i][j]=0
3.因为上述状态转移方程dp[i][j]
与dp[i+1][j-1]
有关,看一个例子,当计算dp[0][1]
或dp[3][4]
时,我们发现需要计算dp[1][0]
与dp[4][3]
。此时dp[1][0]
与dp[4][3]
这种情况并不存在,但dp[0][1]
或dp[3][4]
都可能为1,如“aa”。可以看出来当序号相邻时即我们需要让i+1<=j-1推得j>=i+2,所以在序号相邻的情况下,在状态转移中是无法计算的,所以需要做为边界。上述思路即if(s[i]==s[i+1]) dp[i][i+1]=1
。
此时,需要注意一个地方就是状态转移方程中的dp[i][j]
与dp[i+1][j-1]
,在一般的按行优先遍历计算中,无法保证dp[i+1][j-1]
在计算dp[i][j]
之前已经计算完毕。但我们发现dp[i+1][j-1]
位于dp[i][j]
的右下角,即按列计算可以满足dp[i+1][j-1]
在计算dp[i][j]
之前已经计算完毕。故下面代码均采用按列优先遍历计算。
在遍历计算中需要保证右端点大于等于左端点,并且dp值为1的元素值不再改变,所以在状态转移之前需要判断dp值是否为0,为0才可以进行改变。
#include
#include
using namespace std;
int main(){
string s;
cin>>s;
int dp[s.length()][s.length()],left,right;
memset(dp,0,sizeof(dp)); //边界2
//初始化边界
for(int i=0;i<s.length();i++){
dp[i][i]=1; //边界1
if(s[i]==s[i+1]) dp[i][i+1]=1; //边界3
}
//状态转移方程
for(int i=0;i<s.length();i++) //列序号,按列优先
for(int j=0;j<s.length();j++) //行序号
if(i>j&&dp[j][i]!=1){ //注意此时j是行号,i是列号
if(s[i]==s[j]) dp[j][i]=dp[j+1][i-1]; //注意此时j是行号,i是列号
else dp[j][i]=0; //注意此时j是行号,i是列号
//记录最长回文序列的左右端点下标
if(dp[j][i]==1&&i-j+1>right-left+1){
left=j;
right=i;
}
}
cout<<"最大回文序列长度为"<<right-left+1<<",对应回文串为"<<s.substr(left,right-left+1);
return 0;
}
/*
输入:aabbaa31dadeymasdas121ccbbcc
输出:最大回文序列长度为6,对应回文串为aabbaa
*/
思路:先把原始字符串pre处理为没有符号没有空格的字符串processed,处理过程中记录processed的字符对应pre中的下标,便于最后从processed的字符下标变换到pre的字符下标。回文串的运算同上述过程。
习题链接,AC代码:
#include
#include
#include
using namespace std;
int main(){
string s;
getline(cin,s);//不能使用cin,因为cin读取字符串遇到空白字符就会停止,而getline能读取一整行
string processed_str="",str=s;
int arr[str.length()];
for(int i=0;i<str.length();i++)
if(isalnum(str[i])){
processed_str+=str[i];
arr[processed_str.length()-1]=i;
}
transform(processed_str.begin(),processed_str.end(),processed_str.begin(),::tolower);
int dp[processed_str.length()][processed_str.length()],left=0,right=0;
memset(dp,0,sizeof(dp));
for(int i=0;i<processed_str.length();i++){
dp[i][i]=1;
if(processed_str[i]==processed_str[i+1]) dp[i][i+1]=1;
}
for(int i=0;i<processed_str.length();i++)//列序号
for(int j=0;j<processed_str.length();j++)//行序号
if(i>j&&dp[j][i]!=1){
if(processed_str[i]==processed_str[j]) dp[j][i]=dp[j+1][i-1];
else dp[j][i]=0;
if(dp[j][i]==1&&i-j+1>right-left+1){
left=j;
right=i;
}
}
cout<<s.substr(arr[left],arr[right]-arr[left]+1);
return 0;
}
给定一个有向无环图,求图中的最长路径
设dp[i]
表示从第i
个点出发的路径的最大长度。根据逆向拓扑排序的顺序计算,若第i
个点拓扑顺序后面的所有结点dp值已经确定,则dp[i]=max{dp[j](j是i拓扑排序后面的点,且i到j有边)}+length(i,j)
,为了避免求解拓扑序列,可以使用递归的方法求解。
求解dp[i],也就是求解所有从i可以到达的点的最大值再加上i到j的边长。可以按照结点序号的顺序进行求解,递归的过程中会修改路径中的结点的dp值,这种方法的缺点就是对于dp值本来就是0的结点会浪费一些时间在for循环内。
for(int j=0;j<n;j++)
if(G[i][j]!=INF&&G[i][j]!=0){
int temp=DP(j)+G[i][j];
if(temp>dp[i]) dp[i]=temp;
}
只求最长路径长度版
#include
#include
#include
#define INF 999
using namespace std;
int G[INF][INF],dp[INF]={0},n,maxLen=0;
void init(){
memset(G,INF,sizeof(G));
}
int DP(int i){
if(dp[i]>0) return dp[i];
for(int j=0;j<n;j++)
if(G[i][j]!=INF&&G[i][j]!=0){
int temp=DP(j)+G[i][j];
if(temp>dp[i]) dp[i]=temp;
}
return dp[i];
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++) cin>>G[i][j];
for(int i=0;i<n;i++){
dp[i]=DP(i);
if(dp[i]>maxLen) maxLen=dp[i];
}
cout<<<<maxLen<<endl;
return 0;
}
/*
输入:
5
0 1 1 999 999
999 0 999 2 999
999 999 0 2 999
999 999 999 0 3
999 999 999 999 0
输出:
6
*/
求最长路径经过的点与长度版
#include
#include
#include
#include
#define INF 999
using namespace std;
int G[INF][INF],dp[INF]={0},n,maxLen=0,maxIndex=0;
vector<int> after[INF];
void init(){
memset(G,INF,sizeof(G));
}
int DP(int i){
if(dp[i]>0) return dp[i];
for(int j=0;j<n;j++)
if(G[i][j]!=INF&&G[i][j]!=0){
int temp=DP(j)+G[i][j];
if(temp>dp[i]){
dp[i]=temp;
after[i].clear();
after[i].push_back(j);
}
else if(temp==dp[i]){
after[i].push_back(j);
}
}
return dp[i];
}
int main(){
cin>>n;
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>G[i][j];
for(int i=0;i<n;i++){
dp[i]=DP(i);
if(dp[i]>maxLen){
maxLen=dp[i];
maxIndex=i;
}
}
cout<<maxIndex<<","<<maxLen<<endl;
return 0;
}
/*
输入:
5
0 1 1 999 999
999 0 999 2 999
999 999 0 2 999
999 999 999 0 3
999 999 999 999 0
输出:
0,6
*/
思路:将矩形看作一个结点,结点之间的边代表矩形是否能嵌套,小矩形代表的结点指向大矩形代表的结点。求最大序列长度也就转化为了求DAG中的路径最大长度
习题链接,AC代码:
#include
#include
#include
using namespace std;
int G[999][999],dp[999]={0},n,num,maxLen=0;
void init(){
memset(G,0,sizeof(G));
memset(dp,0,sizeof(dp));
maxLen=0;
}
int DP(int i){
if(dp[i]>0) return dp[i];
for(int j=0;j<num;j++)
if(G[i][j]!=0){
int temp=DP(j)+G[i][j];
if(temp>dp[i]) dp[i]=temp;
}
return dp[i];
}
bool judge(int first1,int second1,int first2,int second2){
if((first2>first1&&second2>second1)||(first2>second1&&second2>first1))
return true;
return false;
}
int main(){
cin>>n;
while(n--){
cin>>num;
int rec[num][2];
init(); //不要忘记每一轮都要进行相关变量的初始化
for(int i=0;i<num;i++)
cin>>rec[i][0]>>rec[i][1];
for(int i=0;i<num;i++)
for(int j=0;j<num;j++)
if(judge(rec[j][0],rec[j][1],rec[i][0],rec[i][1]))
G[j][i]=1;
for(int i=0;i<num;i++){
dp[i]=DP(i);
if(dp[i]>maxLen) maxLen=dp[i];
}
cout<<maxLen+1<<endl;
}
return 0;
}
n中物品,每种物品只有一件,背包总重量为V,求能放进背包物品的总价值的最大值
物品重量数组weights,物品价值数组values,将物品从下标1开始编号
使用dp[x][y]
表示前 x 件物品,在不超过重量 y 的时候的最大价值。则有前x件物品重量不超过y的最大价值要么等于前x-1件物品重量不超过y的最大价值dp[x-1][y]
,要么等于前x-1件物品重量不超过y-weights[x]的最大价值加第x件物品的价值dp[x-1][y - weights[x]] + values[x]
。
所以可得:
dp[x][y] = Math.Max{dp[x-1][y], dp[x-1][y-weights[x]]+values[x]}
边界也显而易见,当没有物品或背包容量为0时,dp值都为0,所以可以将矩阵dp全部元素初始化为0
#include
#include
#include
using namespace std;
int main(){
int n,v;
cin>>v>>n;
int weights[n+1],values[n+1],dp[n+1][v+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++) cin>>weights[i]>>values[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=v;j++){
dp[i][j]=dp[i-1][j];
if(j>=weights[i])
dp[i][j]=max(dp[i][j],dp[i-1][j-weights[i]]+values[i]);
}
cout<<dp[n][v]<<endl;
return 0;
}
n中物品,每种物品有无数件,背包总重量为V,求能放进背包物品的总价值的最大值
物品重量数组weights,物品价值数组values,将物品从下标1开始编号
使用dp[x][y]
表示前 x 件物品,在不超过重量 y 的时候的最大价值。则有前x件物品重量不超过y的最大价值要么等于前x-1件物品重量不超过y的最大价值dp[x-1][y]
,要么等于前x件物品重量不超过y-weights[x]的最大价值加第x件物品的价值dp[x-1][y - weights[x]] + values[x]
,因为物品有无数件,所以状态转移方程与01背包有不同。
所以可得:
dp[x][y] = Max(dp[x-1][y], dp[x][y-weights[x]]+values[x])
//注意01背包为Max(dp[x-1][y], dp[x-1][y-weights[x]]+values[x])
边界也显而易见,当没有物品或背包容量为0时,dp值都为0,所以可以将矩阵dp全部元素初始化为0
dp[i][j]
代表前i种物品在背包容量为j时的最小空闲容量,根据递推关系,dp[i][j]
要么等于前i-1种物品在背包容量为j时的最小空闲容量,要么等于前i-1种物品在背包容量为j-volumes[i]的最小空闲容量再减去volumes[i]。即状态转移方程dp[i][j]=dp[i-1][j];
if(j>=volumes[i])
dp[i][j]=min(dp[i][j],dp[i-1][j-volumes[i]]-volumes[i]);
AC代码:
#include
#include
using namespace std;
int main(){
int n,v;
cin>>v>>n;
int volumes[n+1],dp[n+1][v+1];
for(int i=0;i<=n;i++) dp[i][0]=v;//边界
for(int i=0;i<=v;i++) dp[0][i]=v;//边界
for(int i=1;i<=n;i++) cin>>volumes[i];
for(int i=1;i<=n;i++)
for(int j=1;j<=v;j++){
dp[i][j]=dp[i-1][j];
if(j>=volumes[i])
dp[i][j]=min(dp[i][j],dp[i-1][j-volumes[i]]-volumes[i]);
}
cout<<dp[n][v]<<endl;
return 0;
}
#include
#include
#include
using namespace std;
int main(){
int totalTime,totalNum;
cin>>totalTime>>totalNum;
int dp[totalTime+1][totalNum+1],value[totalNum],time[totalNum];
memset(dp,0,sizeof(dp));
for(int i=0;i<totalNum;i++) cin>>time[i]>>value[i];
for(int i=1;i<=totalTime;i++)
for(int j=1;j<=totalNum;j++){
dp[i][j]=dp[i][j-1];
if(i>=time[j-1])
dp[i][j]=max(dp[i-time[j-1]][j-1]+value[j-1],dp[i][j-1]);
}
cout<<dp[totalTime][totalNum];
return 0;
}
#include
#include
using namespace std;
int main(){
int n,v;
cin>>n>>v;
long long values[n+1],dp[n+1][v+1];
memset(dp,0,sizeof(dp));
for(int i=1;i<=n;i++){
cin>>values[i];
dp[i][0]=1;
}
dp[1][0]=1;
for(int i=1;i<=n;i++)
for(int j=1;j<=v;j++){
dp[i][j]=dp[i-1][j];
if(j>=values[i])
dp[i][j]+=dp[i][j-values[i]];
}
cout<<dp[n][v]<<endl;
return 0;
}
滚动数组解法,这个解法是AC代码
#include
#include
#include
using namespace std;
int w[28];
long long dp[10008];
int main(){
int m,n,i,v;
while(cin>>n>>m){
memset(dp,0,sizeof(dp));
dp[0]=1;
for(i=0;i<n;i++) cin>>w[i];
for(i=0;i<n;i++){
for(v=w[i];v<=m;v++)
dp[v]=dp[v]+dp[v-w[i]];
cout<<dp[m]<<endl;
}
}