bushi
动态规划的几个模板 and 例题
背包问题
01背包
顾名思义,一个东西只有选和不选两种选择。
求体积一定的包里能放的最大质量。
for(int i=1;i<=n;i++)
{
for(int j=m;j>=w[i];j--)//w[i]表示物品 i 的体积
{
f[j]=max(f[j],f[j-w[i]]+v[i]);//v[i]表示物品 i 的质量
//f[j]表示 背包容量为 j 时最多能放的质量
}
}
为什么要逆序
首先,通过上一个问题,我们确认了我们目前一维的dp数组,保存的是确认过的最新一层的数据,即上一层的数据。
当我们计算当前层时,对于二维时的状态转移方程有
\(dp_{i,j} = max\) \(\{dp{i,j}, dp_{i-1,j-v_i} + w_i\}\);
可以看到,\(dp_{i-1,j-v_i}+ w_i\) 使用的上一层的原始数据\(dp_{i-1}\),而我们使用一维的状态转移方程时有
\(dp_{j} = max\) \(\{dp{j}, dp_{j-v_i} + w_i\}\);
当我们从小到大更新是, 因为 \(j - v_i\) 是严格小于 \(j\) 的,所以我们可以举个例子:
\(dp_3 = max\) \(\{dp_3, dp_2 + 1\}\), 因为我们是从小到大更新的,所以当更新到 \(dp_3\) 的时候,\(dp_2\)已经更新过了,已经不是上一层的 \(dp_2\)。
而当我们逆序更新时有,举例 \(dp_8 = max\) \(\{dp_8, dp_6 + 2\}\)。当更新 \(dp_8\) 时,\(dp_6\) 还没有被更新,还是上一层的数据,这样才能保证没有读入脏数据。
以上来自 AcWing
完全背包
与 01 背包的区别就是:同一个物品可以被放多次
for(int i=1;i<=n;i++)
{
for(int j=w[i];j<=m;j++)//这里的枚举顺序要改变一下
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
//f[j]表示总体积是 j 时最大价值是多少
}
}
枚举顺序改变的原因:
每次再使用 \(f_j\) 的时候状态就是已经更新过的
多重背包
在完全背包的基础上,每个物品是有限定数量的。
(1) 可以把每种物品看成 cnt 个物品,只不过他们的质量体积都相同,这样就能转化为 01 背包问题。
(2) 每个物品有 不选、选一个、选两个、………… 选 cnt 个,加一重循环就能实现。
好像两个运行时间差不多?
代码(方法 2 )
for(int i=1;i<=n;i++)
for(int k=1;k<=cnt[i];k++) //物品数量,可以看作一个物品有好多份,01背包
{
for(int j=m;j>=k*w[i];j--)//容量
{
f[j]=max(f[j],f[j-w[i]]+v[i]);
}
}
例题:
这里 https://www.luogu.com.cn/training/572428
子序列问题:
最长上升子序列长度
- 朴素做法
\(dp_i\) 表示 以 \(i\) 点为结尾的最长上升子序列的长度。
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])dp[i]=max(dp[i],dp[j]+1);
}
maxn=max(maxn,dp[i]);
}
- 优化:
如果 a_i 大于原序列末尾,就可以把他直接加上;
如果小于原序列末尾,二分查找到此时序列中第一个小于等于它的值,替换掉。
理由:
如果y在末尾,由于y < a[i],所以y后面能接的不如a[i]多,y让位给a[i]可以让序列更长
如果y不在末尾,那y有生之年都不会再被用到了,直接踹了y就行,y咋样,who care?
p[0]=-1;
for(int i=1;i<=n;i++) //求 最长上升子序列的长度
{
if(a[i]>p[cnt])p[++cnt]=a[i];//直接加在后面
else{ // 查找第一个 >= a[i] 的元素的位置
int left=1,right=cnt;
while(left<right)
{
int mid=left+right>>1;
if(p[mid]>=a[i]) right=mid;
else left=mid+1;
}
p[left]=a[i]; //替换掉
}
}
其他什么上升下降都差不多,就不再说了
最少的不上升子序列的个数
Dilworth 定理
对偏序集 $ \langle A, \le \rangle$,设 \(A\) 中最长链的长度是 \(n\),则将 \(A\) 中元素分成不相交的反链,反链个数至少是 \(n\)。
人话翻译:最少的不上升子序列的个数就是最长上升子序列的长度
具体证明我不会请参考 https://www.luogu.com.cn/article/znt20wu4
for(int i=1;i<=n;i++)//求最长不下降子序列
{
if(a[i]<=d[cnt]) d[++cnt]=a[i];
else{
int left=1,right=cnt;
while(left<right)//查找第一个 < a[i] 的元素的位置 (因为等于的时候不需要替换)
{
int mid=left+right>>1;
if(d[mid]<a[i])right=mid;
else left=mid+1;
}
d[left]=a[i];
}
}
区间DP
性质
- 合并:将两个或者多个部分进行整合,当然也可以反过来;
- 特征:能将问题分解成两两合并的形式;
- 求解:对整个问题设最优解,枚举合并点,将问题分解成左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
定义:
定义 \(f_{i,j}\) 表示将下标位置 i 到 j 的所有元素合并能获得的价值的最大值,那么 \(f_{i,j} = max\) \(\{f_{i,k}+f_{k+1,j}+cost\}\),cost 为将这两组元素合并的价值。
以上来自 OIWIKI
例题:
- 观察到是环,首先
ctrl + c,ctrl + v复制一份接在后面 - 最普通的算法O(n^3):224ms
其中,\(dp_{i,j}\) 代表 \(i\) 到 \(j\) 堆的最优值,\(sum_i\) 代表第 \(1\) 堆到第 \(i\) 堆的数目总和。有:\(dp_{i,j}=min\) \(\{dp_{i,j},dp{i,k}+dp_{k+1,j}\}\) \(+sum_j-sum_{i-1}\)。 - 代码:17ms
#include<bits/stdc++.h>
using namespace std;
int n;
int a[250];
int sum[250];
int dp[250][250];
int dp2[250][250];
int minn=INT_MAX;
int maxn=INT_MIN;
signed main()
{
memset(dp2,0x3f3f3f,sizeof(dp2));
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i];
a[n+i]=a[i];//复制一份接在后面
}
int N=2*n;
for(int i=1;i<=N;i++)
{
sum[i]=sum[i-1]+a[i];
dp2[i][i]=0;
}
for(int k=2;k<=n;k++) //区间长度
{
for(int i=1;i<=N-k+1;i++) //枚举左端点
{
int j=i+k-1;
for(int p=i;p<j;p++) //枚举中间的序号
{
dp[i][j]=max(dp[i][j],dp[i][p]+dp[p+1][j]+sum[j]-sum[i-1]); //最大值
dp2[i][j]=min(dp2[i][j],dp2[i][p]+dp2[p+1][j]+sum[j]-sum[i-1]); //最小值
}
}
}
for(int i=1;i<=n;i++)//开始点
{
int p=i+n-1;//结束点
maxn=max(maxn,dp[i][p]);
minn=min(minn,dp2[i][p]);
}
cout<<minn<<endl;
cout<<maxn<<endl;
return 0;
}
练习题:
直接去洛谷搜标签