动态规划(DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。在 OI 中,计数等非最优化问题的递推解法也常被不规范地称作 DP。
动态规划的实现一般分为三种,分别为:记忆化搜索、迭代和递归
DP的常见分类
1.记忆化搜索
记忆化搜索是用一个类似于备忘录的 DP 数组取记录过你计算过的每一个状态,从而达到每个状态只计算一次的目的。
看例题
Luogu P1434 [SHOI2002] 滑雪
题意
给定一个矩阵,你可以从任意点出发,可以向上下左右四个方向走,走过的数必须是严格递减的,求最长可以走的步数
最朴素的做法
对于每个位置 ( i , j ) (i, j) (i,j),我们都去计算他最长可以走的长度,只要他的上下左右有比他小的,我们就去递归计算他的上下左右的位置
code
int a[N][N];
bool check(int x, int y){
return x <= n && x >= 1 && y <= m && y >= 1;
}
int f(int i, int j){
int ans = 1;
for(int k = 0; k < 4; i++){
int x = i + dx[k];
int y = j + dy[k];
if(check(x, y) && a[x][y] < a[i][j]){
ans = max(ans, f(x, y) + 1);
}
}
return ans;
}
int main(){
int ans = 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
ans = max(ans, f(i, j));
}
}
printf("%d\n", ans);
}
但是,这样做法的复杂度极高,有没有什么好的办法取优化呢
我们打印出来我们计算的 f ( i , j ) f(i, j) f(i,j)
1 1
1 2
1 1
1 3
1 2
1 4
1 3
1 5
1 4
2 1
(只给出前 10 个)
我们发现,我们有些 f ( i , j ) f(i, j) f(i,j) 被计算了多次,这就会出现很多我们计算过了的还要再计算
如何避免这些计算呢,我们就给他们加一个备忘录,每次计算完一个值,我们都把他们储存下来
所以,我们令 d p [ i ] [ j ] = f ( i , j ) dp[i][j] = f(i, j) dp[i][j]=f(i,j),当 f ( i , j ) f(i, j) f(i,j) 被第一次计算时,我们将 d p [ i ] [ j ] ← f ( i , j ) dp[i][j] \gets f(i, j) dp[i][j]←f(i,j),当 f ( i , j ) f(i, j) f(i,j) 后面被计算时,我们只需要返回 d p [ i ] [ j ] dp[i][j] dp[i][j] 即可。因为每个 f ( i , j ) f(i, j) f(i,j) 只会被计算一次,所以复杂度是 O ( n m ) O(nm) O(nm)
code
int a[N][N];
int dp[N][N];
bool check(int x, int y){
return x <= n && x >= 1 && y <= m && y >= 1;
}
int f(int i, int j){
if(dp[i][j] != -1) return dp[i][j];
int ans = 1;
for(int k = 0; k < 4; i++){
int x = i + dx[k];
int y = j + dy[k];
if(check(x, y) && a[x][y] < a[i][j]){
ans = max(ans, f(x, y) + 1);
}
}
return dp[i][j] = ans;
}
int main(){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
dp[i][j] = -1;
}
}
int ans = 1;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= m; j++){
ans = max(ans, f(i, j));
}
}
printf("%d\n", ans);
}
2.背包DP
(1)0/1背包
0/1背包之所以叫0/1背包,是因为他每件物品只有两种情况,选和不选
我们设 d p [ i ] [ j ] dp[i][j] dp[i][j] 为,我们只考虑前 i i i 件物品,总重量不超过为 j j j 的情况下,可以获得的最大价值
分为两种情况
选第 i i i 件物品,此时 d p [ i ] [ j ] ← d p [ i − 1 ] [ j − c o s t [ i ] ] + v a l [ i ] ( c o s t [ i ] ≤ j ) dp[i][j] \gets dp[i - 1][j - cost[i]] + val[i](cost[i] \le j) dp[i][j]←dp[i−1][j−cost[i]]+val[i](cost[i]≤j)
不选第 i i i 件物品,此时 d p [ i ] [ j ] ← d p [ i − 1 ] [ j ] dp[i][j] \gets dp[i - 1][j] dp[i][j]←dp[i−1][j]
(2)分组背包
与0/1背包不同,多组背包的物品进行了分组,每组物品都会相互冲突
我们设 d p [ i ] [ j ] dp[i][j] dp[i][j] 为,我们只考虑前 i i i 组物品,总重量不超过为 j j j 的情况下,可以获得的最大价值
那也会有两种情况,选这一组的物品和不选这一组的物品
选第 i i i 组物品,我们需要取枚举选哪一件物品,即 d p [ i ] [ j ] ← max k = 1 l e n [ i ] ( d p [ i − 1 ] [ j − c o s t [ i ] [ k ] ] + v a l [ i ] [ k ] ) dp[i][j] \gets \max_{k = 1}^{len[i]}(dp[i - 1][j - cost[i][k]] + val[i][k]) dp[i][j]←maxk=1len[i](dp[i−1][j−cost[i][k]]+val[i][k])
其中, l e n [ i ] , c o s t [ i ] [ k ] , v a l [ i ] [ k ] len[i],cost[i][k],val[i][k] len[i],cost[i][k],val[i][k] 分别表示,第 i i i 组的物品数量、第 i i i 组的第 k k k 件物品的代价和收益
不选第 i i i 组物品,此时 d p [ i ] [ j ] ← d p [ i − 1 ] [ j ] dp[i][j] \gets dp[i - 1][j] dp[i][j]←dp[i−1][j]
(3)完全背包
完全背包支持我们拿多次同一种物品,而且不限制数量
我们设 d p [ i ] [ j ] dp[i][j] dp[i][j] 为,我们只考虑前 i i i 件物品,总重量不超过为 j j j 的情况下,可以获得的最大价值
那也会有两种情况
不选第 i i i 件物品,此时 d p [ i ] [ j ] ← d p [ i − 1 ] [ j ] dp[i][j] \gets dp[i - 1][j] dp[i][j]←dp[i−1][j]
选一次第 i i i 件物品,此时 d p [ i ] [ j ] ← d p [ i ] [ j − c o s t [ i ] ] + v a l [ i ] ( c o s t [ i ] ≤ j ) dp[i][j] \gets dp[i][j - cost[i]] + val[i](cost[i] \le j) dp[i][j]←dp[i][j−cost[i]]+val[i](cost[i]≤j)
因为我们拿了还可以再拿,所以不能用 i − 1 i - 1 i−1 进行 转移