DP 学习笔记(一):DP 基础知识,基础 DP 类型
基本概念
动态规划是一种非常常见的算法,它将大问题划分为与它一样但数据规模更小的小问题,而大问题的的最优解决方案又来自于小问题的最优解决方案。简称为 DP (Dynamic Programming)
动态规划优于暴力枚举的原因是它对于每一个问题,不是从头开始解决,而是基于之前解决的规模更小问题计算得来,这可以大大降低时间复杂度,而思维难度则上升了不止一个难度,而且它可以与数学 (概率 DP)、字符串 (自动机 DP)、图论 (最短路算法)、数据结构 (数据结构优化 DP) 等多种信息竞赛中的重要版块进行深度融合,因此需要我们认真学习。
一些定义
状态:当前所求问题的信息;
函数:当前所求问题的答案,一般叫做 DP 值;
状态转移方程:如何通过当前所求问题的状态,找到它可以由哪几个小问题推出,并通过那几个小问题的函数推出当前问题的函数。一般用一个递推式子表示。
时间复杂度:
DP 能解决的问题一般具有以下
斐波那契数列是一种具有递推关系的数列,它的每一个数字都是前两个数字的和:
重叠子问题
简单来说,就是求解大问题的最优解决方案时,需要将大问题拆分成若干个小问题,小问题会被拆分成更小的问题,这些拆分出的小问题可能会有重复。比如求解斐波那契数列的第五项
可以发现,
最优子结构
首先,大问题的最优解包含小问题的最优解,也就是当大问题取得最优解时,小问题也取得最优解。其次,小问题的最优解可以推出大问题的最优解,这就是最优子结构。
在斐波那契数列的求解过程中,求
无后效性
简单来说,就是当我们求出某个问题的最优解时,我们就不再关心这个最优解是如何得到的,也就不再改变这个值了,而是将这个解作为已知继续推出其它问题的最优解。
求解斐波那契数列的过程中,当我们求出
无后效性是可以使用 DP 的前提条件,当后续的操作会影响到之前操作的值时,就无法通过重叠子问题来优化枚举的复杂度,也就无法使用 DP。一般求解 DP 问题都需要考虑 DP 的顺序,让问题没有后效性。
DP 一般有以下
记忆化搜索
在搜索时,如果遇到之前求解过的状态,就直接将它的 DP 值拿来用,而不用继续往下递归。
求解斐波那契数列的记忆化搜索代码:
int f[N];
int fib(int x){
if(x == 1 || x == 2)
return 1;
if(f[x])
return x;
else
return fib(x - 1) + fib(x - 2);
}
填表法
考虑当前状态是由哪几个状态转移而来。
求解斐波那契数列的填表法代码:
int f[N];
f[1] = f[2] = 1;
for(int i = 3; i <= n; i++)
f[i] = f[i - 1] + f[i - 2];
填表法也是最常见的 DP 写法。
刷表法
考虑当前状态会影响到后续哪几个状态的求解。
求解斐波那契数列的刷表法代码:
int f[N];
f[1] = 1;
for(int i = 1; i <= n; i++){
f[i + 1] += f[i];
f[i + 2] += f[i];
}
背包DP
一类非常经典的线性 DP 题,因此专门提出来讲。
一些定义:dp
)。
01背包
记
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int M = 109, T = 1009;
int w[M], v[M], dp[M][T], n, m;
int main(){
scanf("%d%d", &m, &n);
for(int i = 1; i <= n; i++)
scanf("%d%d", &w[i], &v[i]);
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++){
if(w[i] > j)
dp[i][j] = dp[i - 1][j];
else
dp[i][j] = max(dp[i - 1][j - w[i]] + v[i], dp[i - 1][j]);
}
printf("%d", dp[n][t]);
return 0;
}
滚动数组优化01背包
滚动数组可以优化背包的空间复杂度。
可以发现,
交替滚动
开两行数组,一行存计算过的旧的一行,一行存当前计算的一行。
完整代码:
int dp[2][N];
int now = 0, old = 1;
for(int i = 1; i <= n; i++){
swap(old, now);
for(int j = 0; j <= m; j++){
if(w[i] > j)
dp[now][j] = dp[old][j];
else
dp[now][j] = max(dp[old][j], dp[old][j - w[i]] + v[i]);
}
}
自我滚动
只开一行,一边计算,一边更新。这时候内层循环要倒着来枚举,下面来说明。
考虑当前状态由那些状态更新来:
发现
完整代码:
int dp[N];
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--)
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
bitset优化01背包
当我们在求解某类
首先,这种可行性的背包的转移方程就是
bitset <size> name
来定义,可以发现,
完整代码:
bitset <50000> b;
b.set(0, 1);
for(int i = 1; i <= n; i++)
b |= (b << w[i]);
完全背包
考虑现在不止一个物品,而是有无穷个物品,但背包有个总容量,因此每个物品最多放
还是记
其实,考虑
由于考虑到
当然,可行性的完全背包也可以用
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e7 + 5;
long long v[N], w[N], dp[N], n, m;
int main() {
scanf("%d%d", &n, &m);
memset(dp, 0, sizeof(dp));
for(int i = 1; i <= n; i++)
cin >> w[i] >> v[i];
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
if(j >= w[i])
dp[j] = max(dp[j], dp[j - w[i]] + v[i]);
printf("%d", dp[m]);
return 0;
}
由于完全背包中每个物品也不是能选任意多个,因此也可以套用接下来多重背包的优化方式。
多重背包
可以发现这和完全背包很像,但有可能物品取不到
注意此时的状态是由上一排转移过来,和
完整代码(会 TLE):
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 9;
int w[N], v[N], c[N], dp[N], n, m;
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d%d%d", &v[i], &w[i], &c[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--)
for(int k = 1; k <= c[i] && k * w[i] <= j; k++)
dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i]);
printf("%d", dp[m]);
return 0;
}
二进制拆分优化多重背包
考虑一个物品价值为
-
当取
个该物品时,重量为 ,价值为 ; -
当取两个该物品时,重量为
,价值为 ; -
当取
个该物品时,重量为 ,价值为 ; -
当取
个该物品时,重量为 ,价值为 ; -
当取
个该物品时,重量为 ,价值为 ; -
。
可以发现,对于同一种物品,可以把它二进制拆分成
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, m, dp[N];
int v[N], w[N], c[N];
int new_n;
int new_v[N], new_w[N], new_c[N];
int main(){
scanf("%d%d", &n, &m);
for(int i = 1; i <= n; i++)
scanf("%d%d%d", &v[i], &w[i], &c[i]);
int new_n = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= c[i]; j <<= 1){
c[i] -= j;
new_w[++new_n] = j * w[i];
new_v[new_n] = j * v[i];
}
if(c[i]){
new_w[++new_n] = c[i] * w[i];
new_v[new_n] = c[i] * v[i];
}
}
for(int i = 1; i <= new_n; i++)
for(int j = m; j >= new_w[i]; j--)
dp[j] = max(dp[j], dp[j - new_w[i]] + new_v[i]);
printf("%d", dp[m]);
return 0;
}
单调队列优化多重背包
混合背包
将
完整代码(使用了单调队列优化):
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 9;
int n, m, h1, h2, m1, m2;
int dp[N], q[N], num[N];
int w, v, c;
int main(){
scanf("%d:%d %d:%d %d", &h1, &m1, &h2, &m2, &n);
m = 60 * (h2 - h1) + m2 - m1;
for(int i = 1; i <= n; i++){
scanf("%d%d%d", &w, &v, &c);
if(c == 0)
c = 100000000;
if(c > m / w)
c = m / w;
for(int b = 0; b < w; b++){
int head = 1, tail = 1;
for(int y = 0; y <= (m - b) / w; y++){
int tmp = dp[b + y * w] - y * v;
while(head < tail && q[tail - 1] <= tmp)
tail--;
q[tail] = tmp;
num[tail++] = y;
while(head < tail && y - num[head] > c)
head++;
dp[b + y * w] = max(dp[b + y * w], q[head] + y * v);
}
}
}
printf("%d", dp[m]);
return 0;
}
二维费用背包
在
完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 209;
int dp[N][N], w[N], t[N], n, m, T;
int main(){
scanf("%d%d%d", &n, &m, &T);
for(int i = 1; i <= n; i++)
scanf("%d%d", &w[i], &t[i]);
for(int i = 1; i <= n; i++)
for(int j = m; j >= w[i]; j--)
for(int k = T; k >= t[i]; k--)
dp[j][k] = max(dp[j][k], dp[j - w[i]][k - t[i]] + 1);
printf("%d", dp[m][T]);
return 0;
}
分组背包
树形背包 (有依赖的背包)
线性DP
状压DP
普通状压DP
轮廓线DP
数位DP
树形DP
普通树形DP
换根DP
参考资料
- 算法竞赛 罗勇军、郭卫斌
本文来自博客园,作者:JPGOJCZX,转载请注明原文链接:https://www.cnblogs.com/JPGOJCZX/p/18422813
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!