学习目标:
通过本篇学习,可以提高你对动态规划的理解。
学习内容
目录
数字三角形模型
摘花生
样例:
2
2 2
1 1
3 4
2 3
2 3 4
1 6 5
运行结果:
8
16
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int dp[N][N];//表示走到第i行第j列拿到花生粒数
int map[N][N];//这个表示第i行第j列花生的颗粒数
int t,row,col;
int main(){
cin>>t;
while(t--){
cin>>row>>col;
for(int i=1;i<=row;i++)
for(int j=1;j<=col;j++)
cin>>map[i][j];//将数据读入
/*
因为每次到(i,j)这个格子,只能是从(i-1,j)或者(i,j-1)这两个格子上其中一个赖的,所有
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+map[i][j]
*/
for(int i=1;i<=row;i++)
for(int j=1;j<=col;j++)
dp[i][j]=max(dp[i-1][j],dp[i][j-1])+map[i][j];
//dp[row][col]就是我们想要的值
cout<<dp[row][col]<<endl;
}
return 0;
}
方格取数(传纸条问题)
测试数据:
8
2 3 13
2 6 6
3 5 7
4 4 14
5 2 21
5 6 4
6 3 15
7 2 14
0 0 0
运行结果:
67
解析:
这个题我想到了两种思路:
第一种思路就是分两种情况,先求出去的时候的最大值,然后再倒着找到路径,将这个路径上的所有数都置为0,然后再进行第二次的去。
第二种思路就是去和回来看作是两次都去且是同时进行,这样就是一个三维的dp问题,(i1,j1,i2,j2)但是我们可以发现,可以同时进行,只要知道走的总步数和第1,2次的横坐标,就能确定两次行走当前的位置在哪,然后就可以简化成三维(k,i,j)。当两个趟走到同一个格子的时候,只需要加一次这个格子的值。最后在分四种情况进行讨论即可
#include <iostream>
using namespace std;
const int N=12;
int dp[2*N][N][N];//这个分别表示走了k步,i表示去时的横坐标,j表示回来时的横坐标。但我们可以直接看成两次都是是去B
int map[N][N];
int main() {
int n;//n表示有n组数据
cin>>n;
int a,b,c;
while(cin>>a>>b>>c,a||b||c) map[a][b]=c;
//下表直接从1开始,这样就不用再进行预处理了
for(int k=2;k<=2*n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
int i1=k-i,j1=k-j;
if(i1<1||i1>n||j1<1||j1>n) continue;//判断下标是否越界,如果越界了就不再进行判断了,直接进行下一次操作
int t=map[i][i1];
if(i!=j) t+=map[j][j1];//如果两次横坐标相同,就相当于两个人在同一个未知,所以加一次就行了,第二个人去的时候该位置的=权值就是0
int &w=dp[k][i][j];
w=max(w,dp[k-1][i-1][j]+t);//两个人现在的未知都有可能是从上面或者左边赖的,所以总共有四种情况,列举下来求最大值即可
w=max(w,dp[k-1][i][j-1]+t);
w=max(w,dp[k-1][i][j]+t);
w=max(w,dp[k-1][i-1][j-1]+t);
}
cout<<dp[2*n][n][n]<<endl;
return 0;
}
最长上升子序列模型
最长上升子序列
测试数据:
7
3 1 2 1 8 5 6
运行结果:
4
题目解析:
dp[i]是指以i结尾的最长上升子序列的长度。
每次增加一个数,就要从前往后遍历一下,找
到小于当前数的所有数,求出这些数的dp的最
大值再加一就是以当前数结尾的最长上升子序
列。要注意求的是最长上升子序列,所以你不
知道最长上升子序列是以那个数结尾的,你要
将以所有数为结尾的最大值求出来就是本题的
答案。
注意:最长上升子序列并不一定是在这一组数中连续的
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int dp[N];
int s[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=1;i<=n;i++) dp[i]=1;//因为有本身,所以dp的所有初始值都为1
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++){
if(s[j]<s[i]){
//这就证明第j个大于当前数字,而dp[j]表示的就是前i的最长单调递增子序列,dp[i]=max(dp[j]+1,dp[i])
dp[i]=max(dp[i],dp[j]+1);
}
}
int cmax=-1;
//因为最长子序列的结尾不确定,所以我们要从头遍历一遍,找到那个位置是最长子序列的结尾
for(int i=1;i<=n;i++)
cmax=max(cmax,dp[i]);
cout<<cmax<<endl;
return 0;
}
登山
测试数据:
8
186 186 150 200 160 130 197 220
测试结果:
4
解题思路:
因为是在登山,所以从那个海拔开始下山是不确定的,
需要找到在那个点开始下山。因为上山既有上又有下的过程,所以我们就可以采取一个策略,
就是先求一遍从头开始最长上升子序列,再求一段从尾
部开始的最长上升子序列,然后这两段结合,求出最佳答案即可
//
// Created by 小魏 on 2023/5/31.
//
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int s[N],dp1[N],dp2[N];
int main(){
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=1;i<=n;i++){
dp1[i]=1;
dp2[i]=1;//因为自身,所以最长上升子序列的最小长度位1
}
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
if(s[i]>s[j]) dp1[i]=max(dp1[i],dp1[j]+1);
for(int i=n;i>=1;i--)
for(int j=n;j>i;j--)
if(s[i]>s[j]) dp2[i]=max(dp2[i],dp2[j]+1);
//因为最后求得是浏览经典最多,所以要找的是从那个位置下山
int cmax=-1;
for(int i=1;i<=n;i++){
int x=dp1[i]+dp2[i]-1;//因为下山位置上的景点只能算一次,而两次dp都用了该点,所以要减去一个景点
cmax=max(cmax,x);
}
cout<<cmax<<endl;
return 0;
}
背包问题
完全背包问题
测试数据:
4 5
1 2
2 4
3 4
4 5
运行结果:
10
题解:
下面这个试子进行推导:
dp[i][j]=max(dp[i-1][j](这个的意思就是不拿物品),dp[i-1][j-v]+w,dp[i-1][j-2v]+2w,dp[i-1][j-3v]+3w....)
dp[i][j-v]=max(dp[i-1][j-v],dp[i-1][j-2v]+w,dp[i-1][j-3v]+2w,dp[i-1][j-4v]+3w......)
综上:dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w);由于j>j-v,所以正着便利即相当于是第i组,这样就可以用滚动数组优化成以下
由01背包问题我们可以得到
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i]);
完全背包问题得到的结论
dp[i][j]=max(dp[i-1][j],dp[i][j-v]+w);
所以优化后,正着循环就相当于dp[i]
//
// Created by 小魏 on 2023/5/31.
//
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=1010;
int dp[N];//表示拾取前i个物品之后背包剩余的体积为j(但是可以优化成一维,因为上一排的数据全都是依赖于下面一排)
int num,vol;//分别表示
int w[N],v[N];
int main(){
cin>>num>>vol;
int a,b;
for(int i=1;i<=num;i++){
cin>>a>>b;
v[i]=a;
w[i]=b;//将体积和价值放到数组当中
}
for(int i=1;i<=num;i++)
for(int j=v[i];j<=vol;j++)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
cout<<dp[vol]<<endl;
return 0;
}
多重背包问题Ⅱ
测试数据:
4 5
1 2 3
2 4 1
3 4 3
4 5 2
运行结果
10
题解:
多重背包问题就是在0-1背包问题上加了数量,因为数量是明确的,
所以只需要加一层循环(即数量的循环),试出拿多少个物品最后的价值最大。
0-1背包问题的时间复杂度是 数量*背包体积。多重背包问题的时间复杂度就
是 数量*体积*物品数量.
以上方法在本题当中的时间复杂度为:1000*2000*2000,很明显会超时,
所以我们要使用二进制来优化
二进制优化思路:
我们发现,将任意数N分成 1,2,4 ... x ,x-N这样一串数字,这一串
数字就能表示1-N当中的任意一个数,所以我们可以把每个物品都进行"分堆操作"
,将其分成1,2,4 ... x ,x-N这几堆,然后再求一个0-1背包问题。这样的时
间复杂度为 数量*体积*以二为低(物品数量)的对数.就不会再超时了
//
// Created by 小魏 on 2023/5/31.
//
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=20010;
int dp[N];
int w[N],v[N];
int num,vol;
int main(){
int a,b,c;
cin>>num>>vol;
int n=0;//表示当前物品分到第n堆
for(int i=1;i<=num;i++){
cin>>a>>b>>c;
int k=1;//当前物品分的当前堆的数量
while(c>=k){
n++;
v[n]=k*a;
w[n]=k*b;
c-=k;
k=k<<1;
}
//接下来还有最后一部分没有分堆,将最后一部分分堆
if(c){
n++;
v[n]=c*a;
w[n]=c*b;
}
}
//数据预处理完毕,接下来就是求一下0-1背包
for(int i=1;i<=n;i++)
for(int j=vol;j>=v[i];j--)
dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
cout<<dp[vol]<<endl;
return 0;
}
背包问题求具体方案
测试数据:
4 5
1 2
2 4
3 4
4 6
运行结果:
1 4
解体思路:
先求一下0-1背包问题,然后再逆着寻找一遍即可;
注意:求0-1背包的时候不能简化成一维,要记录整个dp的状态
//
// Created by 小魏 on 2023/5/31.
//
#include <iostream>
#include <algorithm>
#include <algorithm>
using namespace std;
const int N=1010;
int dp[N][N];
int w[N],v[N];
int num,vol;
int main(){
cin>>num>>vol;
for(int i=1;i<=num;i++) cin>>v[i]>>w[i];
for(int i=num;i>=1;i--)//倒着求,方便后面的逆向操作
for(int j=0;j<=vol;j++)
{
dp[i][j]=dp[i+1][j];
if(j>=v[i])
dp[i][j]=max(dp[i][j],dp[i+1][j-v[i]]+w[i]);
}
cout<<dp[num][vol]<<endl;
int n=vol;//n表示背包内现在还有多少容量
for(int i=1;i<=num;i++)
if(n>=v[i]&&dp[i][n]==dp[i+1][n-v[i]]+w[i]){
cout<<i<<" ";
n-=v[i];
}
return 0;
}
题目来自acwing