第十一章 动态规划
11.1 动态规划的递归写法 –斐波那契数列的动态规划 自顶向下也就是自外向边界
dp[n] 保存F(n)
int dp[MAX]={-1};
int F(int n)
{
if(n==1||n==2) return 1;
if(dp[n]!=-1) return dp[n];//不加这一句的话和普通递归没区别
else
{
dp[n]=F(n-1)+F(n-2);
return dp[n];
}
}
11.2 动态规划的递推写法 –数塔问题 自底向上也就是自边界向外
dp[n] [n] 保存第n层第n个数出发到最底层的路径中最大和
/*一共n层,第n层有n个数字,从第一层到第n层经过的数字和*/
//输入数塔(f[MAX][MAX])
//边界(底层)
for(int i=1;i<=n;i++)
dp[n][i] = f[n][i];
//自底向上计算状态转移方程
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];
11.3 最大连续 子序列和 (连续子序列的最大和)
dp[i] 表示以A[i]为结尾的连续子序列最大和
//输入A[MAX]
//边界(第一个数据)
dp[0] = A[0];
//自底向上计算状态转移方程
for(int i=1;i<n;i++)
dp[i]=max(dp[i-1]+A[i],A[i]);
//遍历得到最大和
11.4 最长不下降子序列 LIS 注意子序列不一定连续 不下降子序列最大长度
dp[n] 表示以A[n]结尾的不下降子序列最大长度
//输入A[MAX]
for(int i=0;i<n;i++)
{
//边界
dp[i]=1;
//自底向上计算状态转移方程
for(int j=0;j<i;j++)
if(A[i]>=A[j]) dp[i]=max(dp[j]+1,dp[i]);
}
//遍历dp得到最大长度
11.5 最长公共子序列 LCS (没考过!) 公共子序列最大长度
dp[i][j]表示A的i号位之前和B的j号位之前的公共子序列的最大长度
//输入 A[]B[] 下标从1开始读
gets(A+1);
gets(B+1);
//边界
for(int i = 0;i<=lenA;i++)
dp[i][0]=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][j-1],dp[i-1][j]);
}
}
//dp[lenA][lenB]是答案
11.6 最长回文子串 (没考过!) 回文子串最大长度 (练习里没有暂时不看)
dp[i][j]表示 从S[i]到S[j]之间的子串是否为回文子串
//输入S[]
//边界dp[i][i]=1 S[i]=S[i+1]则dp[i][i+1]=1
for(int i=0;i<lenS;i++)
{
dp[i][i]=1;
ans =1 ; //最小回文子串长度为1
if(i+1<=lenS-1)
{
if(S[i]==S[i+1]) dp[i][i+1]=1;
ans =2 ; //最小回文子串长度为2
}
}
//状态转移
for(int L=3;L<=lenS;L++)//从小到大枚举待测回文子串长度
{
for(int i=0;i+L-1<lenS;i++)//待测回文子串的起点
{
int j = i+L-1;//待测回文子串的终点
if(S[i]==S[j]&&dp[i+1][j-1]==1)
{
dp[i][j]=1;
ans=L;//最小回文子串长度为L
}
}
}
//ans为最大回文子串长度
11.7 DAG最长路(太难了先不看)(没考过)
最长距离1 (终点起点都不固定所能达到的最长距离,从右向左,最短距离则同理) dij是固定起点的最短距离,从左向右
dp[i] 表示从第i个顶点出发能够得到的最长路径长度(也可以是最长时间或者路径权值,注意不是路径步数)(比如a与b相连,a到b的时间是2则路径长度是2不是1)
DP(i) 表示计算从第i个顶点出发能够得到的最长路径长度
//以邻接矩阵G[n][n]方式输入图
//边界 不固定终点和起点
for(int i=0;i<n;i++) dp[i]=0;
//状态转移 从右向左(从终点向起点,所有终点的dp都是0)
for(int i=0;i<n;i++) DP(i);
int DP(int i)
{
if(dp[i]>0) return dp[i];//已经计算过dp
for(int j=0;j<n;j++)//遍历所有出边
{
if(G[i][j]!=INF)//注意这里的递归调用,此时dp[j]未知不能直接用dp[j]
dp[i]=max(dp[i],DP(j)+G[i][j]);
}
return dp[i];
}
//遍历dp得到最大距离
最长距离2 (固定终点所能达到的最长距离,从右向左,最短距离则同理)
//以邻接矩阵G[n][n]方式输入图
//边界
for(int i=0;i<n;i++) dp[i]=-INF;//改动1
//状态转移 从右向左(从终点向起点,只有终点的dp是0,出度为0的点的dp是-INF)
for(int i=0;i<n;i++) DP(i);
int DP(int i)
{
if(visit[i]) return dp[i];//改动2
visit[i]=true;//改动3
for(int j=0;j<n;j++)//遍历所有出边
{
//改动,保存路径
if(G[i][j]!=INF)
{
int temp = DP(j)+G[i][j];
if(temp>dp[i])
{
dp[i]=temp;
choice[i]=j;
}
}
}
return dp[i];
}
//遍历dp得到最大距离
最长路径
在最长路径长度的dp基础上增加一个choice[index]数组保存最长路径中index节点的下一节点(也就是后继节点,有点像dij的pre[]数组存前驱节点)
//以邻接矩阵G[n][n]方式输入图
//边界
for(int i=0;i<n;i++) dp[i]=0;choice[i]=-1;
//状态转移 从右向左(从终点向起点,所有终点的dp都是0)
for(int i=0;i<n;i++) DP(i);
int DP(int i)
{
if(dp[i]>0) return dp[i];//已经计算过dp
for(int j=0;j<n;j++)//遍历所有出边
{
//改动,保存路径
if(G[i][j]!=INF)
{
int temp = DP(j)+G[i][j];
if(temp>dp[i])
{
dp[i]=temp;
choice[i]=j;
}
}
}
return dp[i];
}
//遍历dp得到最大距离以及该最大距离的起始节点i
cout<<i;
while(choice[i]!=-1)
{
cout<<choice[i];
i=choice[i];
}
最长路径有多条的情况
11.8 背包问题 01背包
dp[m][w] 表示最大重量为w的情况下装入前m件商品所能得到的最大价值 可以压缩成dp[w]
//输入重量w[],价值v[],规定最大承重T,物品种类M= len(w[])
//边界(dp[0][v]=0)
for(int i=0;i<=T;i++) dp[i]=0;
//状态转移
for(int i=1;i<=M;i++)//注意下标从1开始(输入也是!),M个物品,M次循环
for(int j=T;j>=w[i];j--)//注意从最大值开始,如果从最小值开始是完全背包问题
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
//遍历dp得到最大价值
例题
2795 金银岛 (金属可以被任意分割,所以本题不属于01背包)
#include <iostream>
#include<cstdio>
#include<vector>
#include<string>
#include<map>
#include <math.h>
#include<algorithm>
#include <string.h>
#define MAX 105
using namespace std;
int k,w,s;
typedef struct Node //使用结构体存重量,价值和单位重量的价值
{
double n,v;
double bi;
}node;
node temp_node;
bool cmp(node a,node b)
{
if(a.bi>b.bi) return 1;
return 0;
}
vector<node > vt; //使用vector和sort排序,以方便从最大单位价值的物品开始装入
int main()
{
//freopen("input.txt","r",stdin);
cin>>k;
for(int i=0;i<k;i++)
{
vt.clear();
cin>>w>>s;
for(int j=1;j<=s;j++)
{
cin>>temp_node.n>>temp_node.v;
temp_node.bi = temp_node.v/temp_node.n;
vt.push_back(temp_node);
}
//排序
stable_sort(vt.begin(),vt.end(),cmp);
//装入
int index=0,label=1;
double ans=0.;
for(int i=0;i<vt.size();i++)
{
//cout<<vt[i].n<<" "<<vt[i].v<<" "<<vt[i].bi<<endl;
if(vt[i].n>w)
{
label=0;
index =i;
break;
}
w-=vt[i].n;
ans+=vt[i].v;
}
if(!label)//注意背包总重量远大于所有重量的情况也就是for中遍历了所有的物品
ans+=w*vt[index].bi;
printf("%.2f\n",ans);//为什么lf不对?
}
return 0;
}
1088 滑雪
dp[x][y]表示从点(x,y)向下滑能划出的最长距离
dfs(x,y)表示计算以(x,y)为起点能滑出的最长距离 (类似DAG的DP(i) 计算从i节点出发能走的最远距离)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#define MAX 102
#define num 32767
#define INF 0x7f7f7f
using namespace std;
//https://wenku.baidu.com/view/4064ef970029bd64783e2cee.html?re=view
int R,C,minx,miny,maxx,maxy,minnum,maxnum;
int dp[MAX][MAX]={0};
int A[MAX][MAX];
int X[]={1,-1,0,0};
int Y[]={0,0,1,-1};
bool check(int x,int y)
{
if(x<0||y<0||x>=R||y>=C) return false;
return true;
}
//边界
memset(dp,sizeof(dp),0);
//状态转移
int dfs(int x,int y)
{
//出口 (x,y)已经遍历过
if(dp[x][y]) return dp[x][y];
//遍历四个方向更新dp
int temp=0;
for(int i=0;i<4;i++)//DAG最长路是遍历所有出边,这里遍历所有能走的方向
{
int newx = x+X[i];
int newy = y+Y[i];
if(check(newx,newy))
if(A[newx][newy]<A[x][y])
temp = max(dfs(newx,newy)+1,temp);//这里递归调用有点像动态规划求DAG最长路的DP(j)
}
dp[x][y]=temp;
return temp;
}
int main()
{
//freopen("input.txt","r",stdin);
cin>>R>>C;
minnum =INF;maxnum=0;
for(int i=0;i<R;i++)
for(int j=0;j<C;j++)
cin>>A[i][j];
//边界(以最小点结尾的子序列最长就是1) [出发点一定是最小点]
//dp[minx][miny]=0; //为什么初始为1不ac? 因为在递归中dp[minx][miny]实际值是0,这也是最后结果要+1的原因
//状态更新,从最高点开始向下搜索最长递减子序列,变搜索边更新dp(记忆化搜索)
//dfs(maxx,maxy);//注意最长路径不一定是从最高点开始下滑的,想求最长路径需要遍历dp求最值
//和DAG类似,对所有点递归一次求,再遍历dp求出最值
int ans =0;
for(int m=0;m<R;m++)
for(int n=0;n<C;n++)
ans = max(ans,dfs(m,n));
cout<<ans+1;//注意最后ans+1
return 0;
}
2727 采药 (01背包问题)
注意
1下标从1开始输入
2使用滚动数组则二层循环从最大值开始
正向枚举完全背包(物品可有无穷个)逆向枚举01背包
dp[i][w] 表示前i个药草恰好消耗w个时间所获得的最大值
#include <iostream>
#include<cstdio>
#include<vector>
#include<string>
#include<map>
#include <math.h>
#include<algorithm>
#include <string.h>
#define eps 1e-10
#define inf 0x3f3f3f
#define MAX 105
using namespace std;
int T,M;
int w[MAX],v[MAX];
int dp[1005]={0};
//dp[i][w] 表示前i个药草恰好消耗w个时间所获得的最大值
int main()
{
//freopen("input.txt","r",stdin);
cin>>T>>M;
for(int i=1;i<=M;i++)
cin>>w[i]>>v[i];
//边界(dp[0][v]=0)
for(int i=0;i<=T;i++) dp[i]=0;
//状态转移
for(int i=1;i<=M;i++)//M个草药
{
for(int j=T;j>=w[i];j--)
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
//遍历dp求最值
int maxnum =0;
for(int i=0;i<=T;i++)
maxnum=max(maxnum,dp[i]);
cout<<maxnum;
return 0;
}
2786 pell 数列 (斐波那契数列问题)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#define MAX 1000002
#define num 32767
using namespace std;
int n,k;
int dp[MAX]={0};
int Pell(int k)
{
if(k==1) return 1;
else if(k==2) return 2;
if(dp[k]!=0) return dp[k];
else
{
dp[k]=(2*Pell(k-1)+Pell(k-2))%num;
return dp[k];
}
}
int main()
{
//freopen("input.txt","r",stdin);
cin>>n;
dp[1]=1;
dp[2]=2;
for(int i=0;i<n;i++)
{
cin>>k;
cout<<Pell(k)<<endl;
}
return 0;
}
2766 最大子矩阵 (连续子序列的最大和)
dp[i]表示压缩后的一维矩阵中以第i节点为结尾的子序列最大和
对矩阵进行纵向压缩(第i行到第j行之间的),得到新的一维矩阵在进行子序列求最大和操作枚举所有的可能压缩方案(i和j两重for循环)
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#define MAX 103
using namespace std;
int n,ans;
//https://blog.csdn.net/lhyer/article/details/48055629
int B[MAX]={0};
int dp[MAX]={0};
int A[MAX][MAX];
int main()
{
//freopen("input.txt","r",stdin);
while(cin>>n)
{
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
cin>>A[i][j];
ans = A[0][0];
//压缩枚举
for(int i=0;i<n;i++)//第i行
{
for(int j=i;j<n;j++)//到第j行之间所有可能的压缩方案
{
memset(B,0,sizeof(B));
//对i-j行之间的数进行纵向压缩
for(int k=0;k<n;k++)//对应k个dp
for(int kk=i;kk<=j;kk++)
B[k]+=A[kk][k];
//对压缩后的一位数组求子序列的最大和
memset(dp,0,sizeof(dp));
//边界
dp[0]=B[0];
//状态转移
for(int m=1;m<n;m++)
{
dp[m]=max(dp[m-1]+B[m],B[m]);
if(dp[m]>=ans) ans=dp[m];
}
}
}
cout<<ans<<endl;
}
return 0;
}
2806:公共子序列
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<cstring>
#include<string>
#define MAX 203
using namespace std;
string A,B,temp;
int dp[MAX][MAX]={0};
int main()
{
//freopen("input.txt","r",stdin);
while(cin>>temp)
{
memset(dp,0,sizeof(dp));
A=" "+temp;//这里采用string作为输入,为了下标从1开始加了前导空格,因为dp结果也要最终减一(前导空格一定构成了公共子序列)
cin>>temp;//如果采用char a[] gets(a+1)方式则可以直接从下标一遍历
B=" "+temp;
//cout<<A<<endl<<B;
//边界
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]==B[j]) 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()]-1<<endl;
//减一是因为为了方便从下标1开始,我们把空格拼到了串前面,这样每个新串至少有一个相同字符(也就是空格)
}
return 0;
}
4118 开餐馆 (类似最长不下降子序列)
这题乍一看有总数n,每个数还有一个位置m和利润p,同时对位置还有限定条件k,非常像01背包,但是仔细想一下其实不是,不过很接近了,这种有明显序列性质的问题大多动态规划.
应该采用最长不下降子序列的方法,dp[i]表示以i餐馆结尾的序列能获得的最大利润
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
#define CLR(a,b) memset(a,b,sizeof(a))
#define INF 0x3f3f3f3f
int dp[111];
//dp [ i ] 表示以第i个餐馆为最后一个餐馆的最大钱数
struct node
{
int pos;//位置
int v;//利润
}data[111];
int main()
{
int u;
int n,l;
scanf ("%d",&u);
while (u--)
{
scanf ("%d %d",&n,&l);
CLR(dp,0);
for (int i = 1 ; i <= n ; i++)
scanf ("%d",&data[i].pos);
for (int i = 1 ; i <= n ; i++)
scanf ("%d",&data[i].v) , dp[i] = data[i].v;
int ans = 0;
for (int i = 1 ; i <= n ; i++)
{
for (int j = 1 ; j < i ; j++)
//非常像最长不下降子序列,遍历之前的节点,只不过这里限定了"上升"的最小距离,上升距离不能再比这小了
//换成lis则是if (data[i].pos - 0 > data[j].pos && dp[j] + data[i].v > dp[i])
if (data[i].pos - l > data[j].pos && dp[j] + data[i].v > dp[i])
dp[i] = dp[j] + data[i].v;
ans = max (ans , dp[i]);
}
printf ("%d\n",ans);
}
return 0;
}
装箱问题 特殊背包1(物品重量=物品价值)
dp[v] 表示最大容量为v的前提下能装的最大重量(与下体相比,这里最大重量可能达不到满状态)
#include <iostream>
#include <cmath>
#include <algorithm>
using namespace std;
int main()
{
int n,m;
cin >> n >> m;
int num[40] = {0};//记录每个物体的体积,要求30就可以,习惯开大点
int dp[20005] = {0};//记录在j下最大体积V,要求是20000
for(int i = 0; i < m; i++)//输入
cin >> num[i];
for(int i = 0; i < m;i++)//将给出的物体放入箱子,防止重复计算
{
for(int j = n; j >= num[i]; j--)//判断物体是否放入箱子
{
dp[j] = max(dp[j],dp[j-num[i]]+num[i]);//注意普通背包是dp[j-num[i]]+value[i]
}
}
cout < <n - dp[n] <<endl;
return 0;
}
4141 砝码称重 特殊背包2
dp[v] 表示最大容量为v的前提下能否恰好完全*填满*v
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int i,n,k,m,ans,v;
int w[1005],f[1005],a[6];
int main()
{
// freopen("weight.in","r",stdin);
//freopen("weight.out","w",stdout);
a[1]=1;a[2]=2;a[3]=3;a[4]=5;a[5]=10;a[6]=20;
for (i=1;i<=6;++i)//将读入的数据进行转化
{
scanf("%d",&k);
while (k>0)
{
n++;
w[n]=a[i];//先把数据转存到一维数组中
m+=w[n];//最大"承重"没有明确给出,这里用所有砝码的组合来模拟最大的承重量
k--;
}
}
f[0]=1;
for (i=1;i<=n;++i)
for (v=m;v>=w[i];--v)
f[v]=f[v]||f[v-w[i]]; //装箱问题f[v]=max(f[v],f[v-w[i]]+w[i])(但是本体f[v]要求能完全填满容量v所以不能这么写)
//这里f[v]表示最大重量为v的情况下能装入的最大重量(实际上是吧重量和价值看做相等的)
//也就是"最大重量为v的情况下能装入的最大**价值**"
//这就是一种物品重量等于物品价值的特殊01背包
for (i=1;i<=m;++i)
if (f[i])
ans++;
printf("Total=%d",ans);
return 0;
}
/*
类似于装箱问题,f[i]表示i这个重量可不可以组合出来,是一个布尔型的数组(这里用int型做了);
先把读入的数据转化一下,存放在一个数组里,然后做一遍简单的背包问题就OK了;
设dp[1000]数组为标记数组。当dp[i]=0时,表示质量为i的情况,目前没有称出;当dp[i]=1时,表示质量为i的情况已经称出。
本题目中有多个砝码,我们顺序处理每一个砝码。当处理第j个砝码,质量为wj时,有下列推导公式:
dp[v-w[i]]=1 => dp[v]=1 也就是当且仅当v-w[i]称出吼,v才能称出