简单动态规划

对于一个可以用动态规划实现的题目来说我们需要有以下步骤:
1.将原来划分为若干个阶段,每个阶段对应若干个子问题,提取子问题的特征(称为状态).
2.找到每个状态下可能得决策或者是各个状态的转移方式(就是寻找状态转移方程式).
3.按顺序求解每个阶段问题.

基础动态规划问题

最长公共子序列

给定一个长度为 n 的序列 a 和一个长度为 m 的序列 b ,求出一个最长的序列,使得该序列既是A的子序列,也是B的子序列.
1.我们首先寻找这个问题的状态, dp[i][j] 就表示序列 a 的前 i 个与序列 b 的前 j 个的最长公共子序列.
2.我们尝试推导状态转移方程式,我们发现当 a[i]=b[j] 时,我们可以直接可以放在最长公共子序列的末尾,但当不等于时,我们可以选择跳过 a[i] 或者 b[j] ,此时我们就可以得出状态转移方程式了:
{dp[i][j]=dp[i1][j1]+1,(a[i]=b[j])dp[i][j]=max(dp[i1][j],dp[i][j1]),(a[i]b[j]).
此题的时间复杂度为 O(nm) .

string a,b;
int n,m,dp[N][N];
int solve(){
for(int i=1;i<=n;i++){
for(int j=1;j<=m;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]);
}
}
}

最长不下降子序列

给定一个长度为 n 的序列 a ,求出一个最长的 a 的子序列,满足该子序列的后一个元素不小于前一个元素.

算法一

用a数组表示原数组, dp[i] 表示前i个数的最长不下降子序列。那么我们就可以遍历第 1i1 个数,如果第 j 个数不大于它就使 dp[i]=dp[j]+1 ,遍历寻找 dp[i] 的最大值,此时我们可以得到状态转移方程式:
dp[i]=max(dp[j]+1)(j<i,a[j]a[i]).
这种解法的时间复杂度为 O(n2) .

int n,a[N],dp[N];
int solve(){
int ans=1;
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[j]+1,dp[i]);
ans=max(ans,dp[i]);
}
}
return ans;
}

算法二

我们发现刚刚的算法是将状态 (j,l1) 在合法的前提下转移到 (i,l) .所以我们只用设法找到 (i,l) 使 l 最大的合法的 (i,l) 就可以求得最长不下降子序列了.
我们设原数组为 a , dp[i] 表示所有长度为 i 的最长不下降子序列的末尾元素的最小值, len 表示子序列的长度,所以我们发现当考虑元素 a[i] 时,若 dp[len] 小于等于 a[i] ,直接将它插入 dp 数组的末尾,如果 dp[len] 大于 a[i] ,我们就可以在 dp 数组中寻找第一个大于它的元素,然后用 a[i] 替换.
若直接用暴力查询,那么此题的时间复杂度仍然是 O(n2) ,但我们发现dp数组是单调的,所以我们可以用二分查找将时间复杂度优化至 O(nlogn) .

int n,a[N],dp[N];
int solve(){
int len=1;
dp[len]=a[1];
for(int i=2;i<=n;i++){
//a[i]>=dp[len]<-->a[i]<dp[len],因此使用upper_bound
if(a[i]>=dp[len]) dp[++len]=a[i];
else dp[upper_bound(dp+1,dp+1+len,a[i])-dp]=a[i];
}
return len;
}

对于最长上升子序列,我们同理可以解决.

区间DP

对于区间DP我们是将问题分为左右两个部分,最后合并两个部分的最优值得到全局的最优值.
例题:在一个环上有 n 个数,进行 n1 次合并操作每次操作将相邻的两堆合并成一堆,能获得新的一堆中的石子数量的和的得分.你需要最大化你的得分.
我们先考虑在链的情况,写出状态转移方程:dp[i][j]=max(dp[i][k]+dp[k+1][j])+r=ija[r].
其中求和部分不难发现可以使用前缀和优化.
因为我们要先知道 dp[i][k]dp[k+1][j] 的值,因为当中 dp[i][k]dp[k+1][j] 中包含的元素数量都小于 dp[i][j] ,所以我们可以使用 len=ji+1 作为一个阶段确保我们可以知道前面我们要求的值.
对于一个环,我们可以将数组复制.放在原数组的后面,要使在新的数组取到的长度不超过原数组的长度.

for(int len=2;len<=n;len++){
for(int l=1;l+len<=2*n){
int r=l+len-1;
for(int k=l+1;k<r;k++) dp[l][r]=max(dp[l][k]+dp[k+1][r],dp[l][r]);
dp[l][r]+=s[r]-s[l-1];//前缀和优化
}
}

背包DP

01背包问题

首先考虑最为简单入门的01背包问题.
有n个物品和一个容量为W的背包,每个物品有重量Wi和价值Vi两种属性,要求选若干物品放入背包使背包中物品的总价值最大且背包中物品的总重量不超过背包的容量.
我们对于一个物品,我们有放入和不放入两个选择,我们定义dp[i][j]为对于前i个物品容量为j的背包能达到的最大总价值,所以我们可以列出状态转移方程式:dp[i][j]=max(dp[i1][j]+dp[i1][jw[i]]+v[i]).
但是我们发现对于一个二维数组来说可能会MLE(内存超限),所以我们考虑一维数组优化.
我们发现对 dp[i] 有影响的只有 dp[i1] ,所以我们可以去掉第一维列出如下方程:
dp[j]=max(dp[j],dp[jw[i]]+v[i]).
我们在写01代码用一维数组优化时需要注意要从后往前更新,这样才会使得 dp[j[w[i]] 的值未被更新,若从前往后遍历的话可能会出现一个物品被取多次的情况(事实上这样正是完全背包的解法).

int n,wmax,w[N],v[N],dp[N];
void solve(){
for(int i=1;i<=n;i++){//考虑每件物品
for(int j=wmax;j>w[i];j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
}

完全背包问题

首先我们考虑朴素的做法,对于当前状态,枚举对每个物体选举多少个,得到状态转移方程式:
dp[i][j]=max(dp[i1][jk×w[i]],dp[jw[i]]+v[i]×k).
但我们可以尝试进行优化,我们发现对于 dp[i][j] 只用考虑 dp[i][jw[i]] 的值,因为 dp[i][jw[i]] 已经是之前考虑的最佳取值,因此可以得到新的状态转移方程式:
dp[i][j]=max(dp[i1][jw[i]],dp[jw[i]]+v[i]).
与前面的01背包问题对比,我们也不难发现这次的更新是正向的,可以对于前面取过该物品得到的最佳取值继续进行选取.

int n,wmax,w[N],v[N],dp[N];
void solve(){
for(int i=1;i<=n;i++){//考虑每件物品
for(int j=w[i];j<=wmax;j++){
dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
}
}
}

多重背包

多重背包与01背包的区别就是,对于每个物品,01背包只能取1次,到那时多重背包可以取k次.
对于多重背包,我们可以想到一种十分朴素的办法,就是将k个同种物品转化为k个物品,每个物品只能选一次,用01背包进行处理,但是我们可以考虑一种二进制分组优化.假设一个物品有 k 件,将这个 k 进行拆分,让他成为 n 个数之和,使得所有小于等于 k 的所有数中,都能在那 n 个数中任选几个数使加和等于那个数的大小.
比如说: 8=1+2+4+1,18=1+2+4+8+3.

不难发现,我们先使 2n 次幂从小到大相加,如果加上后超出原数那么我们就取要求的数与我们已选取的数的加和的差,不难发现这样的选取就可以满足取到所有小于等于 k 的取值。

struct node{
int v,w;
}list[N];
int n,tot;
void changenum(){
cin>>n;
for(int i=1;i<=n;i++){
int v1,w1,num,c=1;
cin>>v1>>w1>>num;
while(num>c){
num-=c;
list[++tot].w=c*w1;
list[tot].v=c*v1;
c*=2;
}
list[++tot].w=num*w1;
list[tot].v=num*v1;
}
}

分组背包

n 件物品和一个大小为 m 的背包,第 i 个物品的价值为 w[i] ,体积为 v[i] .同时,每个物品属于一个组,同组内最多只能选择一个物品.求背包能装载物品的最大总价值.
其实这个问题我们只用对组内进行一个01背包问题就可以解决了.我们可以用 t[k][j] 来表示第 k 组第 j 个的编号.

for(int k=1;k<=ts;k++){//遍历每个背包
for(int i=wmax;i>0;i++){//遍历背包容量
for(int j=1;j<=cnt[k];j++){//遍历每组背包的每个物品
if(w[t[k][j]]<=i){//如果背包容量充足
dp[i]=max(dp[i],dp[i-w[t[k][j]]]+v[t[k][j]]);
}
}
}
}

需要注意的是循环的顺序不能更换,才能保证正确性.

本文作者:bssmessi的博客

本文链接:https://www.cnblogs.com/bssmessi/p/18463301

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   BSS梅者如西  阅读(16)  评论(0编辑  收藏  举报
   
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起
  1. 1 失忆蝴蝶 陈奕迅
失忆蝴蝶 - 陈奕迅
00:00 / 00:00
An audio error has occurred.

失忆蝴蝶 - 陈奕迅 (Eason Chan)

词: 林夕

曲: 陈晓娟

还没有开始 才没有终止 难忘未必永志

还没有心事 才未算相知 难道值得介意

还没有心事 才未算相知 难道值得介意

言尽最好于此 留下什么意思

言尽最好于此 留下什么意思

让大家只差半步成诗

还没有惊艳 才没有考验 才未值得哄骗

还没有惊艳 才没有考验 才未值得哄骗

还没有闪电 才没有想念 才未互相看厌

还没有闪电 才没有想念 才未互相看厌

还未化灰的脸 留在梦中演变

回头就当作初次遇见

并未在一起亦无从离弃

并未在一起亦无从离弃

不用沦为伴侣 别寻是惹非

随时能欢喜亦随时嫌弃

这样遗憾或者更完美

从没有相恋 才没法依恋 无事值得抱怨

从没有相恋 才没法依恋 无事值得抱怨

从没有心愿 才没法许愿 无谓望到永远

从没有心愿 才没法许愿 无谓望到永远

蝴蝶记忆很短 留下什么恩怨

回头像隔世一笑便算

并未在一起亦无从离弃

并未在一起亦无从离弃

不用沦为伴侣 别寻是惹非

随时能欢喜亦随时嫌弃

这样遗憾或者更完美

这样遗憾或者更完美

就像蝶恋花后无凭无记

就像蝶恋花后无凭无记

亲密维持十秒又随伴远飞

亲密维持十秒又随伴远飞

无聊时欢喜在忙时忘记

生命沉闷亦玩过游戏

并未在一起亦无从离弃

并未在一起亦无从离弃

一直无仇没怨 别寻是惹非

随时能欢喜亦随时嫌弃

不用再记起怎去忘记

不用再记起怎去忘记