新征程5.4 动态规划(一)

动态规划

\(by\) Cr_deviance

DP入门

B3635 B3637
这是两道DP的入门题,可以先从这两题入手,熟悉一下动态规划的基本思想,以及动态规划中状态及状态转移方程的设计思路,帮助各位初学者(julao)对动态规划有一个初步的了解。不讲,逃(

记忆化搜索

记忆化搜索(Memoization Search):是一种通过存储已经遍历过的状态信息,从而避免对同一状态重复遍历的搜索算法。
记忆化搜索是动态规划的一种实现方式。在记忆化搜索中,当算法需要计算某个子问题的结果时,它首先检查是否已经计算过该问题。如果已经计算过,则直接返回已经存储的结果;否则,计算该问题,并将结果存储下来以备将来使用。

举个例子,比如「斐波那契数列」的定义是:\(f(0) = 0, f(1) = 1, f(n) = f(n - 1) + f(n - 2)​\)。如果我们使用递归算法求解第 \(n​\) 个斐波那契数,则对应的递推过程如下:image
从图中可以看出:如果使用普通递归算法,想要计算 \(f(5)​\),需要先计算 \(f(3)​\)\(f(4)​\),而在计算 \(f(4)​\) 时还需要计算 \(f(3)​\)。这样 \(f(3)​\) 就进行了多次计算,同理 \(f(0)​\)\(f(1)​\)\(f(2)​\) 都进行了多次计算,从而导致了重复计算问题。

为了避免重复计算,在递归的同时,我们可以使用一个缓存(数组或哈希表)来保存已经求解过的 \(f(k)​\) 的结果。如上图所示,当递归调用用到 \(f(k)​\) 时,先查看一下之前是否已经计算过结果,如果已经计算过,则直接从缓存中取值返回,而不用再递推下去,这样就避免了重复计算问题。

使用「记忆化搜索」方法解决斐波那契数列的代码如下:

class Solution:
    def fib(self, n: int) -> int:
        # 使用数组保存已经求解过的 f(k) 的结果
        memo = [0 for _ in range(n + 1)]
        return self.my_fib(n, memo)

    def my_fib(self, n: int, memo: List[int]) -> int:
        if n == 0:
            return 0
        if n == 1:
            return 1
        
        # 已经计算过结果
        if memo[n] != 0:
            return memo[n]
        
        # 没有计算过结果
        memo[n] = self.my_fib(n - 1, memo) + self.my_fib(n - 2, memo)
        return memo[n]

我们在使用记忆化搜索解决问题的时候,其基本步骤如下:

写出问题的动态规划「状态」和「状态转移方程」。

定义一个缓存(数组或哈希表),用于保存子问题的解。

定义一个递归函数,用于解决问题。在递归函数中,首先检查缓存中是否已经存在需要计算的结果,如果存在则直接返回结果,否则进行计算,并将结果存储到缓存中,再返回结果。

在主函数中,调用递归函数并返回结果
P1352 没有上司的舞会
P1434 [SHOI2002] 滑雪
两个比较典的例题。

背包DP

背包问题的定义

一个可承载重量为 \(W​\) 的背包和 \(N​\) 件物品,每件物品有一个重量 \(w​\) 和一个价值 \(v​\)。现在让你用这个背包装物品,要求装入的物品总重量不能超过背包可承载重量 \(W​\),同时使装入物品的总价值最大。

背包问题的分类

image

01背包

P2871 [USACO07DEC] Charm Bracelet S

01背包的板子题,大家应该都做过。

板子不应该是采药吗?

典型例题

题目描述:
\(N\) 件物品和一个容量是 \(V\) 的背包。每件物品只能使用一次。

\(i​\) 件物品的体积是 \(v_i​\),价值是 \(w_i​\)

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式:
第一行两个整数,\(N\), \(V\), 用空格隔开,分别表示物品数量和背包容积。

接下来有 \(N\) 行,每行两个整数$ v_i$, \(w_i\),用空格隔开,分别表示第 \(i\) 件物品的体积和价值。

输出格式:
输出一个整数,表示最大价值。

数据范围:

0 < \(N , V\) ≤ 1000
0 < $v_i , w_i $≤ 1000

实现思路image

这里使用二维的状态表示 $f [ i ] [ j ] $,用来表示 只从前 \(i\) 件物品中选, 总体积 $\leq j $。

这样在状态计算中, 就可以把集合划分为$ f[ i − 1 ] [ j ] $(即只从前 $ i - 1 $ 件物品中选且不选择第 $ i $ 件物品), $ f[i - 1] [j - v[i]] + w[i] $( $v [ i ] $ 表示第 $ i $ 件物品的体积, \(w [i]\) 表示第 \(i\) 件物品的权重)(即只从前 $ i - 1 $ 件物品中选且选了第 $ i $ 件物品)。

通过以上思路可以得到状态转移方程:
$ \large f[i][j] = max(f [i - 1] [j],f[i - 1] [j - v[i]] + w[i]) ​$

P1855 榨取kkksc03

完全背包

完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。
所以完全背包主要在状态的计算方面有所不同,由于完全背包问题下所有可以选用的物品数量是无限的。因此状态转移方程为:$ f [i] [j]=max(f[i−1] [j],f [i−1] [j−v[i]]+w[i],...,f[i−1] [j−k∗v[i]]+k∗w[i]) ​\( 由于\) f [ i ] [ j − v [ i ] ] = m a x ( f [ i − 1 ] [ j − v [ i ] ] , f [ i − 1 ] [ j − 2 ∗ v [ i ] ] + w [ i ] , . . . , f [ i − 1 ] [ j − k ∗ v [ i ] ] + k ∗ w [ i ] ) ​$

为什么最后一项不是 \(f [ i − 1 ] [ j − ( k + 1 ) ∗ v [ i ] ] + ( k + 1 ) ∗ w [ i ] ​\)

这是因为 $ k ​$ 的值只与背包最大容量相关

由上面两个公式我们可以推导出:

$ f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i ] [ j − v [ i ] ] ) ​$

P1616 疯狂的采药

多重背包

image
P1776 宝物筛选

分组背包

P1757 通天之分组背包
image

区间DP

区间DP是一种动态规划算法,用于解决一些涉及区间的问题。区间DP通常需要定义一个二维的状态数组来表示区间的状态,其中状态转移方程往往与区间的划分有关。

具体来说,区间DP的状态通常表示为\(dp[i][j]\),表示区间\([i,j]\)的最优解或者其他需要求解的值。状态转移方程通常可以通过枚举区间内的一个点k来实现,即:
$dp[i][j] = min(dp[i][k] + dp[k+1][j] + cost[i][j]) $

或者

\(dp[i][j] = max(dp[i][k] * dp[k+1][j])\)

其中\(cost[i][j]​\)表示区间\([i,j]​\)的某种代价,可以是区间长度、区间权值和等。

通过这种方式,我们可以在\(O(n^3)\)或者\(O(n^4)​\)的时间复杂度内解决一些区间相关的问题,比如最长回文子序列、最长公共子序列、字符串编辑距离等问题。

P1775 石子合并(弱化版)

P1880 [NOI1995] 石子合并

需要考虑不在环上,而在一条链上的情况。

\(f(i,j)​\) 表示将区间 \([i,j]​\) 内的所有石子合并到一起的最大得分。

写出 状态转移方程:
\(f(i,j)=\max\{f(i,k)+f(k+1,j)+\sum_{t=i}^{j} a_t \}~(i\le k<j)​\)

\(sum_i​\) 表示 \(a​\) 数组的前缀和,状态转移方程变形为 \(f(i,j)=\max\{f(i,k)+f(k+1,j)+sum_j-sum_{i-1} \}​\)

状压DP

通过将状态压缩为整数来达到优化转移的目的 julao

“状压 DP 是最直接的 DP ” —— dottle

状态压缩DP介绍

状态压缩DP其实是一种暴力的算法,因为它需要遍历每个状态,而每个状态是多个事件的集合,也就是以集合为状态,一个集合就是一个状态。集合问题一般是指数复杂度的NP问题,所以状态压缩DP的复杂度仍然是指数的,只能用于小规模问题的求解。

为了方便地同时表示一个状态的多个事件,状态一般用二进制数来表示。一个数就能表示一个状态,通常一个状态数据就是一个一串0和1组成的二进制数,每一位二进制数只有两种状态,比如说硬币的正反两面,10枚硬币的结果就可以用10位二进制数完全表示出来,每一个10位二进制数就表示了其中一种结果。

使用二进制数表示状态不仅缩小了数据存储空间,还能利用二进制数的位运算很方便地进行状态转移。

下面列举了一些常见的二进制位的变换操作。

P1896 [SCOI2005] 互不侵犯

The End.

posted @ 2024-05-04 21:24  deviancez  阅读(21)  评论(0)    收藏  举报