动态规划
背包
快熄灯了,写得有点急
01背包
给m个物品让你装进最大载重为t的背包,每个物品重量和价值,每种物品只能放一次,问最大价值
朴素做法
设dp[i][j]为只取前i个物品中并且容量为j的最佳情况
可以想到两种情况1.不选当前物体,则dp[i][j]=dp[i-1][j]
2.选当前物体则需要为当前物体腾出来wi的位置,方程为dp[i][j]=dp[i-1][j-w[i]]+v[i].
for(int i=1;i<=m;i++)
{
for(int j=0;j<=t;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
}
}
printf("%d",dp[m][t]);
}
优化做法
由于我们每次选择时只涉及到i和i-1,我们可以将二维压缩至一维,至于怎么体现原来的i和i-1,就需要我们在内层循环反着枚举一遍容量。原理是如果我们如果正着枚举那么在我们肯定会先更新dp[j-w[i]]再更新dp[j],也就是说在更新dpj时必然会收到本次i循环前面的结果的影响,但如果反过来枚举的话就不会收到本次i循环影响,也就是说使用的dp[j-w[i]]都是在i为i-1时的结果,也就是在i-1的基础上更新。
for(int i=1;i<=m;i++)
{
for(int j=t;j>=w[i];j--)
{
dp[j]=max(dp[j-w[i]]+v[i],dp[j]);
}
}
完全背包
和01背包的不同是每个物品可以放多次
最朴素做法是加一层循环看放入多少个i物品
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
for(int k=0;k*w[i]<=j;k++)
{
dp[i][j]=max(dp[i][j],dp[i-1][j-k*w[i]]+v[i]*k);
}
}
}
这个是二维优化版本
把所有k拆开我们可以得到dp方程大概是这么个结果,然后我们发现后面的一大坨就等于f[i,j-w[i]]+vi
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
dp[i][j]=dp[i-1][j];
if(j>=w[i]) dp[i][j]=max(dp[i][j],dp[i][j-w[i]]+v[i]);
}
}
代码和01背包的唯一不同是dp[i][j-w[i]]中在01背包是dp[i-1][j-w[i]].
原因在于01背包只能由上一个物品转移来,而完全背包可以从当前物品转移多次而来.
优化版本
和01背包唯一不同是内层循环是正着枚举
刚才说过如果正着循环会导致当前i影响到结果,但在完全背包中我们一个i需要取多次所以正好需要结果根据当前i进行变动
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=m;j++)
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
多重背包
每个物品不是只能取一次也不是无限次而是指定次数,并且每个物品的次数可以不同。
朴素版本
和完全背包最朴素版本基本类似,不过增加一个数量限制不能超过si
for(int i=1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
for(int k=0;k<=s[i]&&k*w[i]<=j;k++)
dp[i][j]=max(dp[i][j],dp[i-1][j-w[i]*k]+v[i]*k);
}
}
优化版本
二进制优化法,因为我们种物品都有多个,我们可以将同种物品分为好多堆,采用二进制分法。假如a物品有7个那么就分为1+2+4,如果是8就是1+2+4+1,是15就是1+2+4+8,以此类推。然后我们就将n个a种物品分成了logn个,然后将所有物品都这样分一起做01背包即可。(n以内的每个数都可以被不同堆之间相加所得并且不重复)
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
int sum=1;
while(sum<=c)
{
cnt++;
v[cnt]=sum*b;
w[cnt]=sum*a;
c-=sum;
sum*=2;
}
if(c>0)
{
cnt++;
v[cnt]=c*b;
w[cnt]=c*a;
}
}
for(int i=1;i<=cnt;i++)
{
for(int j=m;j>=w[i];j--)
{
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
cout<<dp[m];
分组背包问题
将物品分为多个组,相同组的物品只能取一次,我们采用三重循环,第二层是由大到小枚举,原因是同组物品只能取一次因此要像01背包一样,大概像是对每一组分别进行01背包的感觉。并且由于每一组内的物品质量不一定相同所以要枚举到0并且要判断当前j是否大于等于物品重量
for(int i=1;i<=n;i++)
{
for(int j=m;j>=0;j--)//同组不能重复取
{
for(int k=1;k<=s[i];k++)
{
if(j>=w[i][k]) dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
}
}
}
线性dp
最长上升子序列
给定一个序列让你求他的最长上升子序列的长度,子序列的数字可以不相邻,比如1,6,2,9,5,最长上升子序列就是1,2,5或者1,6,9等等
朴素做法
我们设dp[i]表示以a[i]为结尾的最长上升子序列的长度,那么很显然这个长度初始值应该设为1,因为对于每个数字而言都至少可以以它本身作为一个序列。
那么在什么样的情况下我们可以增加序列的长度呢?显然,当a[i]作为一个上升子序列的结尾,并且有j>i,且a[j]>a[i]的情况下,可以更新dp[j]=dp[i]+1。
那么答案就比较显然了,我们对于每个i都寻找他后面的大于ai的数aj并且更新答案,然后最后扫一遍dp数组看看以哪个数结尾的长度最大即可。对于每个i要更新(n-i)次,n×(n-i),总体复杂度仍然是n方
int n,cnt=0;
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),dp[i]=1;
for(int i=1;i<=n;i++)
{
for(int j=i;j<=n;j++)
{
if(a[j]>a[i]) dp[j]=max(dp[j],dp[i]+1);
}
}
int ans=0;
for(int i=1;i<=n;i++)
ans=max(ans,dp[i]);
cout<<ans;
优化做法
我们将dp[i]定义为长度为i的上升序列的最后一个数的最小值,从1到n循环并每次更新dp数组。这其实是运用了一种贪心的思想。
我们用cnt数组来记录dp数组的长度。如果一个数比cnt数组的末尾要大,说明它可以放在整个序列的后面,序列长度+1
如果ai比dp数组的末尾要小,我们就去找dp数组的前面的部分,找到第一个大于等于ai的数并且替换掉它。理由:因为dp[i]表示的是一定长度序列的末尾值,我们找到的第一个大于等于ai的数,假设是a[j],那么a[j-1]一定是小于ai的,也就是说ai一定可以接在a[j-1]的后面并且令长度+1,也就是(j-1)+1=j。然后又因为aj是大于等于ai的,并且i在j的后面,所以ai可以替换掉aj在序列中的地位并且比aj更优。因为序列的末尾越小,后续新加的数的空间就越大
代码如下
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
int dp[maxn];
int n,cnt=0;
int find(int x,int l,int r)
{
if(l==r) return l;
if(l>r) return 0;
int mid=(l+r)>>1;
if(dp[mid]>=x) return find(x,l,mid);
else return find(x,mid+1,r);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(x>dp[cnt]) dp[++cnt]=x;
else
{
int t=find(x,1,cnt);
dp[t]=min(dp[t],x);
}
}
cout<<cnt;
return 0;
}
最长公共子序列
给定两个长度分别为n和m的序列,问两个序列的最长公共子序列。子序列的选取法则和最长上升子序列相同,即不一定相邻,相对顺序对即可
通用版本/朴素版本
适用于一般情况。我们可以想一下在什么情况下可以令公共子序列的长度增加,即在a和b字符串中(数字也可,不过为了普适性用字符串)存在ai==bj的情况,并且a子序列之前的部分下标均小于i,b的均小于j。
我们设dp[i][j]表示a字符串选取前i个字符,b选择前j个字符的情况下子序列的最大长度
然后对于每个ai和bj我们都有如下情况
1.ai和bj都不加入子序列
2.ai加入子序列bj不加入子序列
3.ai不加入子序列bj加入子序列
4.ai和bj均加入子序列
4情况比较好判断,当ai ==bj时我们就可以将子序列答案更新,即dp[i][j]=dp[i-1][j-1]+1.
1情况我们只需要把dp[i-1][j-1]继承过来即可
而对于2情况,bj不加入子序列,说明使用的仍然是前j-1个字符,因此dp[i][j]=dp[i][j-1],3情况同理
有一点注意的是在找最大值时dp[i-1][j]和dp[i][j-1]均包含了dp[i-1][j-1]的情况,因此不必要单独列举1
所有答案就明了了,我们先循环i到n,然后内层j从1到m循环更新答案即可
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int a[maxn],b[maxn];
int n;
int f[1005][1005];
int main()
{
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
int t=f[i][j];//这里其实没有必要,因为f[i][j]只会遍历一次,不必担心答案继承
f[i][j]=max(f[i-1][j],f[i][j-1]);
f[i][j]=max(t,f[i][j]);
if(a[i]==b[j]) f[i][j]=max(f[i-1][j-1]+1,f[i][j]);
}
}
cout<<f[n][n];
return 0;
}
优化做法
这个与其说是优化做法不如说是特殊情况的适用做法,具体就是适用当两个序列都是从1到n的数的时候。
或者就是两个序列的值都能一一对应的时候
具体做法特别类似优化版本的最长上升子序列。具体就是,我们先列一个数组c来表示a数组的数对应的位置
假如a是3 1 2 4 5
那么c是2 3 1 4 5
然后我们新建一个数组dp和cnt表示长度,将b数组从1到n扫一遍,如果c[b[i]]大于dp[cnt],也就是b[i]在a中对应的位置要大于dp数组末尾在a中的位置,就令cnt++并将c[b[i]]加进去。这样显然可以维护c中的数是单调递增的,并且由于我们b是从1到n扫,b的位置也是递增的,也就是能保证a和b各自的子序列中数值的相对位置不被打乱。这时候cnt的值就是目前最长的公共子序列的长度。
如果c[b[i]]小于dp[cnt]我们就找出dp数组中第一个大于等于c[b[i]]的值并将其用c[b[i]]替换。这里可以用最长上升子序列的思想理解,我们维护的dp数组表示的是长度为i的子序列的末尾下标是多少。如果一个下标在能维护相同长度的情况下要更小那么显然更优。
或者我们可以直接把这道题这么理解:两个序列中的数完全相同,只不过顺序不同,以a数组的数的下标为标准,则a数组的数下标单调递增。
那么在过来看b,用a的标准来表示b的下标,不难发现,如果b和a要有公共子序列,那么b的下标也一定需要是单调递增的,因为a的下标全部是单调递增的。
因此我们的问题就转化为了:在b的下标用a表示的情况下,b的下标的最长上升子序列的长度是多少
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int a[maxn],b[maxn],c[maxn];
int dp[maxn];
int main()
{
int n,cnt=0;
cin>>n;
for(int i=1;i<=n;i++)
scanf("%d",&a[i]),c[a[i]]=i;
for(int i=1;i<=n;i++)
scanf("%d",&b[i]);
for(int i=1;i<=n;i++)
{
if(c[b[i]]>dp[cnt]) dp[++cnt]=c[b[i]];
else
{
int k=lower_bound(dp+1,dp+cnt+1,c[b[i]])-dp;
dp[k]=c[b[i]];
}
}
cout<<cnt;
return 0;
}
区间dp
区间dp一般用于从较小区间的状态推到较大区间的状态,和普通dp的不同之处是普通dp一般是从前往后进行状态转移,而区间dp是由小区间转移到大区间
洛谷p1880石子合并
每次可以合并相邻两堆石子,根据合并的顺序不同最终的答案也不同。比如现在有三堆石子重量分别为abc,
如果先合并前两堆再合并所有,答案就是(a+b)+(a+b+c)=2a+2b+c,如果先合并后两堆就是(b+c)+(a+b+c)=a+2b+2c
那么我们如何得到将整个区间合并的最小值?可以试想,如果我们想要把两个区间合并,那么合并后的答案就是两个区间之前合并的答案再加上两个区间的总和,可以表示为dp[i,k]+dp[k+1,j]+sum(i,j).而我们的目的就是令这个值最小。
对于(1,n)这个区间,区间总和是固定的,我们就需要找到最小的dp[i,k]+dp[k+1,j]。不难想到我们可以枚举k来达到这个目的。但显然如果我们想知道一个大区间的答案,我们必然要先知道小区间的答案,所以我们最外层循环区间长度,第二层循环i和j,第三层在i和j之间循环k寻找答案。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int maxn=305;
int n,dp2[maxn][maxn],dp1[maxn][maxn],a[maxn],sum[maxn];
signed main()
{
cin>>n;
memset(dp1,0x3f,sizeof dp1);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]),sum[i]=sum[i-1]+a[i],dp1[i][i]=0;
for(int len=2;len<=n;len++)
{
for(int i=1,j=i+len-1;j<=n;j++,i++)
{
for(int k=i;k<j;k++)
{
dp1[i][j]=min(dp1[i][j],dp1[i][k]+dp1[k+1][j]+sum[j]-sum[i-1]);
//dp2[i][j]=max(dp2[i][j],dp2[i][k]+dp2[k+1][j]+sum[j]-sum[i-1]);
}
}
}
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
// printf("%ld %ld %ld %ld\n",i,j,dp1[i][j],dp2[i][j]);
}
}
printf("%ld",dp1[1][n]);
return 0;
}
另外,这道题还有进阶版的,简单来说就是让石子变成按环排列,并且求最大值和最小值。
基本思路和这个一样,多开一个dp数组,然后数组长度加长一倍模拟环,再把初始化更新一下就好了
比较容易漏的是sum数组需要单独更新,因为输入ai时只从1到n,n+1到2n的sum数组也需要更新。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· winform 绘制太阳,地球,月球 运作规律
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人