动态规划基础
一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第3天,点击查看活动详情。
动态规划基础
引入——数字三角形
题目:
给出一个数字三角形。请编一个程序计算从顶至底的某处的一条路径,每一步可沿左斜线向下或右斜线向下走,使该路径所经过的数字的总和最大。
为了防止超时,即避免每一个坐标值被重复计算——很明显的是一道动态规划问题
题解:(1条消息) 【蓝桥杯】 算法训练 数字三角形酱懵静的博客-CSDN博客数字三角形蓝桥杯
正文开始:
动态规划问题既是DP问题
动态规划应用的前提条件
DP能够处理的问题的特征:
-
可以将问题分解为若干个相同形式的子问题
-
可以通过子问题的答案得到问题的答案
-
子问题中有很多是重复的,求解时记录所有可能出现的子问题
的答案,遇到重复的子问题时直接查表得到答案,使得所有出
现的问题只被求解一次。
-
某一阶段的状态只受前一阶段的状态的影响,不受更早阶段的
影响
状态设计
-
名词解释:
状态:描述子问题
转移:通过子问题的答案求出母问题的答案
初始量:最小的子问题(解很显然或可以通过人工计算获得)
最终答案:最终状态对应的答案
-
应用
以引入为例,则问题转化为——从顶层结点沿着路径走到底层结点的权值和的最大值。
坐标表示为:
问题进一步转化——max{f(1),f(2),f(3),f(4),f(5)}
解决:
- 状态:**给f(x)加一维dp[i][j]** :即到达点(i,j)的权值和的最大值
- 转移:**对于不是边界的点(i, j)** 可以由(i-1, j-1)、(i-1, j)到达
即a[i][j]是指点(i, j)的
```
if(j = 1) dp[i][j] = dp[i-1][j] + a[i][j];
else if(j == i) dp[i][j] = dp[i-1][j-1] + a[i][j];
else dp[i][j] = max(dp[i-1][j-1],dp[i-1][j]) + a[i][j];
```
- 初始值:dp[1][1] = a[1][1]
实现方法
-
递推方法:
f[1][1]=a[1][1]; for(int i=2;i<=n;i++){ for(int j=1;j<=i;j++){ if(j==1) f[i][j]=f[i-1][j]+a[i][j]; else if(j==i) f[i][j]=f[i-1][j-1]+a[i][j]; else f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j]; } } -
记忆化搜索(深度优先搜索)
int dfs(int i,int j){ if(j<1||j>i) return 0; if(i==n) return a[i][j]; if(f[i][j]) return f[i][j]; return f[i][j]=max(dfs(i+1,j),dfs(i+1,j+1))+a[i][j]; }
线性动态规划
- 线性动态规划状态是一维的(f[i])
- 正推:第i个元素的最优值只与前i-1个元素的最优值有关
- 倒推:第i个元素的最优值只第i+1个元素之后的最优值有关
- 经典的线性DP题目有最长上升子序列、最大连续子序列和、最长公共子序列等
例子——最长上升子序列
题目:
给定n个元素的数列,求最长上升子序列长度
子序列:从原序列中选出若干个元素,不改变其原有顺序
形成的新序列成为原序列的子序列
上升序列:对于任意1<=i<j<=n , ai<aj
例: 8 2 7 1 9 10 1 4 3
题解
-
正推方法:
• 状态设计:f[i]表示以序列中第i个元素结尾的最长上升子序
列长度
• 初始值:f[1]=1;
• 最终答案:ans=max{f[i]}
• 转移:f[i]=max{f[j]}+1(j<i , a[i]>a[j])
代码:
for(int i = 0; i <= n; i++){ f[i]=1; for(int j = 1; j<i;j++){ if(a[j]<a[i]) f[i]= max(f[i],f[j]+1); } }
-
逆推方法:
-
状态设计:f[i]表示以序列中第i个元素作为开头的最长上升
子序列长度
-
初始值:f[n]=1
-
最终答案:ans=max{f[i]}
-
转移:f[i]=max{f[j]}+1(j>i , a[i]<a[j])
for(int i=n;i>=1;i--){ f[i]=1; for(int j=i+1;j<=n;j++){ if(a[i]<a[j]) f[i]=max(f[i],f[j]+1); } } -
例题2
合唱队形
题目
n 位同学站成一排,音乐老师要请其中的 n−k 位同学出列,
使得剩下的 k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 k 位同学从左到右依次
编号为 1,2, … ,k他们的身高分别为 t1,t2, … ,tk则他们的身
高满足 t1<⋯< ti > ti+1 > … > tk (1≤i≤k)。
你的任务是,已知所有 n 位同学的身高,计算最少需要几
位同学出列,可以使得剩下的同学排成合唱队形
题解
首先清楚合唱需要的队形—
即规则是:队形中间为最高点,左侧为上升子序列,右侧为下降子序列,
当长度和最大时为最优合唱队形
方法:
-
对于某一点为最高点的最优解为左侧选择以该点结尾的最长
上升子序列,右侧选择以该点开始的最长下降子序列
-
整个问题的最优解为枚举每一点最为最高点,取最大值
-
所以,只需正推求一次最长上升子序列(f[i])
-
倒推求一次最长下降子序列(g[i])
-
枚举最高点取最大值,ans=max(f[i]+g[i]-1)
-
最小出队人数:n-ans
01背包问题
有N件物品和一个容量为V的背包,每种物品只有一个,放入第i
件物品消耗的空间是Ci,得到的价值是Wi。求解将哪些物品装入
背包可使价值总和最大
按照动态规划的步骤
每种物品仅有一件,可以选择放或不放
两者取最优fi=max{f[i-1][v],f[i-1][v-Ci]+Wi}
for(int i=0;i<V;i++)
{
f[0][i]=0;}
for(int i=1;i<=n;i++){
for(int j=c[i];j<=v;j++){
f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]);
}
}
在这里的空间复杂度最大是O(n^2),但观察转移方程可以发现,所有i状态只和i-1的状态有关,状态设计可以改为f[0/1][V] 0/1交替表示当前组和上一组。
转移时只与容量更小的状态有关,所以可以考虑直接去掉第一维,枚举重量时从大到小枚举
f[v]=max{f[v],f[v-ci]+wi}等式右侧的f[v]和f[v-ci]+wi都是本次更新前
的,即f[i-1][…]对应的值
即代码:
for(int i=0;i<V;i++) {
f[i]=0;
}
for(int i=1;i<=n;i++){
for(int j=V;j>=c[i];j--){
f[j]=max(f[j],f[j-c[i]]+w[i]);
}
}
完全背包问题
有N种物品和一个容量为V 的背包,每种物品都有无限件可用。
放入第i种 物品的耗费的空间是Ci,得到的价值是Wi。求解:将
哪些物品装入背包,可使 这些物品的耗费的空间总和不超过背包
容量,且价值总和最大
与01背包不同点
- 所有物品都有无限件
- 每一种物品的策略也不再是取或不取两种,而是可以取0,1,2,……,V/Ci种多种策略,按照01背包的思路,只需要再枚举选择几件物品即可
- Fi=max{f[i-1][V-kCi]+kWi}(0<=kCi<=V)
代码
-
先对于深度优先搜索来看
dfs(int now,int v){ if(now==n+1) return 0; if(f[now][v]!=-1) return f[now][v]; int ans=-INF; for(int k=0;k*c[i]<=v;k++){ ans=max(ans,dfs(now+1,v-k*c[i])+k*w[i]); } return f[now][v]=ans; } -
在优化时间复杂度时发现——一次放入3件物品和分3次放入1件物品是相同的
dfs(int now,int v){ if(now==n+1) return 0; if(f[now][v]!=-1) return f[now][v]; int ans=-INF; ans=max(ans,dfs(now+1,v)); if(v>=c[i]) ans=max(ans,dfs(now,v-c[i])+w[i]); return f[now][v]=ans; } -
和01背包优化空间复杂度后的代码只有内层循环顺序不同——利用递推
for(int i=0;i<V;i++) f[i]=0; for(int i=1;i<=n;i++) for(int j=c[i];j<=V;j++) f[j]=max(f[j],f[j-c[i]]+w[i]);
多重背包
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,
每件耗费 的空间是Ci,价值是Wi。求解将哪些物品装入背包可
使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
与完全背包类似,只需要在枚举选择物品数量时将选择数量的上
限改为Mi即可——f[i][j]=max{f[i-1][v-kCi]+kWi}(0<=k<=Mi&& 0<=k*Ci<=V)
- 将第i种物品拆成Mi件物品,就转化成了01背包问题
区间DP
区间DP是通过短区间的答案推出长区间的答案的一种DP。
例题:
有N堆石子(N≤100)排成一排。现要将石子合并 成一堆.规定每次
只能选相临的两堆合并成一堆新的 石子,并将新的一堆的石子数,
记为该次合并的得分.选择一种合并石子的方案,使得做N-1次合并,
得分的总和最少。
1 3 5 2
22
- 状态设计f[i][j]:区间i~j的最优解
- 初始值:f[i][i]=0
- 是先把小区间合并好,再把两个合并后的区间合并
因此对于DP转移——
f[i][j]=min{
f[i][j]+f[i+1][j],
f[i][i+1]+f[i+2][j],
……
f[i][j-1]+f[j][j]}+sum[i][j]
其中sum[i][j]=a[i]+a[i][i+1]+……+a[j]
写成DP方程:
f[i][j]=min{f[i][k],f[k+1][j]}+sum[i][j]
总:
memset(f,0x3f,sizeof(f));
for(int i=1;i<=n;i++) f[i][i]=0;
for(int p=2;p<=n;p++){
for(int i=1;i<=n-p+1;i++){
int j=i+p-1;
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+sum[j]-sum[i-1]); } } }
\

浙公网安备 33010602011771号