动态规划
写在前面:
如果您没有接触过DP,想要学习一下的话,建议您往下看
如果说您学过,只是想过来复习一下,找一些遗忘知识点,那么建议您跳转到Iris_鸢桃吻月的&动态规划(DP)&(满满的干货)
当然,只是个建议,怎么选择在于您
进入正题
动态规划
动态规划(DP)不是某种算法,而是一种思想
核心在于:把大问题转化成小问题,利用小问题的解推断出大问题的解
举个栗子:
【例1】//递推
走楼梯
问题描述:今有\(n\)级台阶,初始时站在0级,每次可以向上走1级或2级。问总方案数是多少?
暴力模拟:指数级复杂度 可怕
考虑更优的算法。如果以f[x]表示“从0级走到\(x\)级的方案数”,假设f[1],f[2]...f[n-1]全部已知,如何利用这些信息推出f[n]
走到f[n],要么是从\(n-1\)级走上来的,要么是从\(n-2\)级走上来的。
依据加法原理
这就是这个问题的递推式
【例2】//DP
硬币问题
问题描述:今天给你手上有无限的面值为1,5,11元的硬币。给定\(n\),问:至少用多少枚硬币,可以恰好凑出\(n\)元?
例
n=15时,答案是3,构造方法是5+5+5
n=12时,答案是2,构造方法是11+1
用f[x]记录“凑出\(x\)元所需要的硬币数”。那么答案歉然就是f[n],如何求出\(f\)数组呢?f[x]等于什么?
提示:
注意思考“f[x]从哪里来”
考虑一个具体的例子:凑出15元
为了凑出15元,我们最开始的时候,可以使用哪枚硬币?
-
假设用了1元硬币,那么接下来要凑出14元。共1+4=5枚
-
假设用了5元硬币,那么接下来要凑出10元。共1+2=3枚
-
假设用了11元硬币,那么接下来要凑出4元。共1+4=5枚
这三种方案,当然是选代价最低的,所以我们在这一次决策中,选择了5元硬币
现在再看来,f[x]是“凑出\(x\)需要的硬币数”,它等于什么?
可供选择的决策方案如下:
-
先用一个1元硬币,代价
1+f[x-1] -
先用一个5元硬币,代价
1+f[x-5] -
先用一个11元硬币,代价
1+f[x-11]
再上述方案里面,选择代价最低的就行!
所以有
\(f[x]=min\begin{cases}1+f[x-1]\\1+f[x-5]\\1+f[x-11]\end{cases}\)
相当于
\(f[x]=1+min\begin{cases}f[x-1]\\f[x-5]\\f[x-11]\end{cases}\)
在这里说一个问题
当x=10时f[x-11]=f[-1],这是不合法的
所以我们就要在得到结果之后判断是否合法
如果不合法
-
不进行计算
-
将这个不合法的值设置一个值,具体见下
INF一般取为0x3f
即INF=0x3f
当题目要求取min的时候,取INF
当题目要求取max的时候,取-INF
不在乎的话直接设置成0
而且硬币问题有min这个比较函数,这也就是DP与递推最明显的一个特征
走楼梯就是一个非常纯的递推,而硬币问题就是一个DP问题
初步总结:状态
我们用 “大事化小,小事化了” 的思想,解决了上楼梯问题和硬币问题。
大事能转化成小事,是因为大事和小事都有一样的形式:
-
上楼梯问题
大问题:爬上n级有多少种方案
小问题:爬上n-1级有多少种方案、爬上n-2级有多少种方案
它们都是 “爬上xx级有多少种方案” 这一类问题
-
硬币问题
大问题:凑出n元钱的最少硬币数
小问题:凑出n-1,n-5,n-11元的最少硬币数
它们都是 “凑出xx元钱所需要最少硬币数” 这一类问题
可见,只有大问题和小问题拥有相同的形式,才能考虑大事化小。
如果满足这个要求,那么我们遇到的每个问题,都可以很纯洁地表达。
我们可以把可能遇到的每种 “局面” 成为状态
例
硬币问题中,要表达“我们需要凑出n元钱”这个局面,可以设计状态;“f[x]表示凑出x元用的最少硬币数”
上楼梯问题中,设计状态:“f[x]表示走上x级的方案数”
设计完状态之后,只要能 利用小状态的解求出大状态的解 ,就可以动手把题目做出来!
【例3】
LIS问题
问题描述:数组的“最长上升子序列”是指:最长的那一个单调上升的子序列。(或者说你可以理解为:去取出数列中某些数,可以不连续,保持原有顺序)
例如:数组a:[1,3,4,2,7,6,8,5]的最长上升子序列是1,3,4,7,8
如何求数组的最长上升子序列的长度?
分析
想用大事化小来完成这道题,必须先设计状态。
如何设计状态,来完整地描述当前遇到的局面?
设计状态
以f[x]表示“以a[x]结尾的上升子序列,最长有多长”
那么答案就是f[1],f[2]...f[n]里面的最大值。
问题来了,如何求出\(f\)数组?
提示:思考f[x]从哪里来
f[x]表达的是“以a[x]结尾的上升子序列长度”。
这个最长的子序列,一定是把a[x]接在某个上升子序列尾部形成的
例
数组a:[1,3,4,7,6,8,5]
考虑f[8],它的来源有:
自己的一个元素作为一个序列。长度为1
接在a[1]后面。长度为f[1]+1=2
接在a[2]后面。长度为f[2]+1=3
接在a[3]后面。长度为f[3]+1=4
接在a[5]后面。长度为f[5]+1=3
此时,稍有常识的人都会看出,要得到f[x],只需要看a[x]能接在那些书的后面。
也就是
其中p<x,a[p]<a[x]的含义是:枚举在\(x\)前面的,a[p]又比a[x]小的那些\(p\),因为a[x]可以接到这些数的后面,形成一个更长的上升子序列
初步总结:转移(核心)
在前面的三个立体种,我们都是先设计好状态,然后给出了一套用小状态推出的大状态的方法
从一个状态的解,得知另一个状态的解,我们称之为 “状态转移”。
这个转移的式子成为 “状态转移方程”
硬币问题中,状态转移方程是:
\(f[x]=1+min\begin{cases}f[x-1]\\f[x-5]\\f[x-11]\end{cases}\)
LIS问题中,状态转移方程是:
小结:状态和转移
总结刚刚学习的内容。
如果我们想用大事化小的思想解决一个问题,我们需要:
-
设计状态。把面临的每一个问题,用状态表达出来
-
设计转移。写出状态转移方程,从而利用小问题的解推出大问题的解
前三个问题中,我们设计转移的时候,考虑的都是“这个局面是从哪里过来的”
这是一种常见的思路:当前状态未知。需要用已经解决的状态,来推出当前的状态的解
DP还有另一种设计转移的思路:当前状态的解已知。需要利用这个解,去更新它能走到的状态。
这两种思路,一种是考虑“我从哪里来”,一种是考虑“我到哪里去”。
这两种手段都是能解决问题的!
上楼梯问题再讨论
如何用“我到哪里去”的转移手段,结局上楼的问题?
那如何用“我到哪里去”的转移手段,解决硬币问题?
其中f[x]初始值为INF
原值 更新值
f[x+1]=min(f[x+1],f[x]+1)
f[x+5]=min(f[x+5],f[x]+1)
f[x+11]=min(f[x+11],f[x]+1)
那又如何用“我到哪里去”的转移手段,解决LIS问题?
其中\(p>x,a[p]>a[x]\)
f[p]的初始值为-INF
小结:设计转移
设计转移有两种方法:
-
pull型(我从哪里来):对于一个没有求出解的状态,利用能走到它的状态,来得出它的解。
-
push型(我到哪里去):对于一个已经求好了的状态,拿去更新它能走到的状态
(前方高能……)
DP三连
综上所述,如果你想用DP解决一个问题,要干的事情可以总结为DP三连
-
我是谁?(如何设计状态)
-
设计转移(二选一)
-
我从哪里来?(pull型转移)
-
我到哪里去?(push型转移)
-
开始和最后怎么办,初始未确定的值,初值是什么
-
如何处理特判
————————————斐波那契数列————————————
众所周知,斐波那契数列是
假设严格按照定义,写一个递归的代码,复杂度是什么情况?
不用说,肯定会炸
朴素代码
int fib(int n)
{
if(n==1||n==2) return 1;
return fib(n-2)+fib(n-1);
}
我们遇到的最大的麻烦,是很多fib值被重新计算了
假设现在在计算fib(7),明明fib(5)只需要计算一次就可以;
但是fib(7)要调用fib(6)和fib(5),fib(6)要调用fib(5),所以fib(5)莫名其妙被调用了两次
如何避免这种情况???
答案就是 记 忆 化
调用fun(x)时:
-
如果
fun(x)没有被计算过,则计算fun(x),并存储到mem[x] -
如果
fun(x)被计算过,则直接返回mem[x]
记忆化搜索的复杂度是多少呢??
等于直接递推
好好用
小结:记忆化搜索
-
按顺序递推和记忆化搜索,是DP的两种高效实现方式
-
记忆化搜索一般配套“我从哪里来”的转移方式
记忆化搜索的优势
-
如果转移顺序不太好确定,则记忆化搜索可以帮你省一堆事
-
有时候,记忆化搜索更节省时间。空间。因为不可能达到的状态是不会被搜索到的
小结
决策类的DP,设计状态时,需要满足两个原则:
-
最有子结构
大问题的最优解,一定是从小问题的最优解推出来的
例如硬币问题,凑出\(15\)元的最有解是有凑出\(4\)、\(10\)、\(14\)元的最优解(而不是最坏解)而来
-
无后效性
现在的决策,只和过去的结果有关,而和过去的决策无关
例如硬币问题(
它的出镜率好高哦)要在n=15时做出决策有关,只需要知道
f(4),f(10),f(14)的值,而并不关心这些值是怎么来的
—————————————\(01\)背包问题—————————————
DP三连
-
我是谁?
设计状态:
dp[k][m]表示“只考虑钱k个物品,用m的容量能装下的最大价值” -
我从哪里来?
dp[k][m]的来历:要么取了第k个物品,要么没有取。因此\(dp[k][m]=max\begin{cases}dp[k-1][m]\\dp[k-1][m-v[k]]+w[k]\end{cases}\)
第一种情况表示没取,第二种情况表示取了
\(Code\)
for(int k=1;k<=n;k++)
for(int m=0;m<=V;m++)
{
dp[k][m]=dp[k-1][m];//没取
if(m-v[k]>=0)
dp[k][m]=max(dp[k][m],dp[k-1][m-v[k]]+w[k]);//取了
}
- \(01\)背包的空间优化
回顾\(01\)背包的状态转移方程
\(dp[k][m]=max\begin{cases}dp[k-1][m]\\dp[k-1][m-v[k]]+w[k]\end{cases}\)
我们注意到,dp[k][]只与dp[k-1][]有关,从而dp[][]数组的第\(1\)行、第\(2\)行……第\(k-2\)行全部都被浪费了。
占据了空间,但以后不会被访问到。
下面是优化过后的代码
for(int k=1;k<=n;k++)
for(int m=V;m>=0;m--)//注意!!!
if(m-v[k]>=0)
dp[m]=max(dp[m],dp[m-v[k]]+w[k]);
在第二层的循环中,我们是按照倒着的顺序进行访问的
因为只与上一层有关,所以我们只调用上一层的值
因为m-v[k]>=0
所以m>=v[k]
所以dp[m-v[k]]一定在dp[m]之前
为了防止前面的已经被覆盖,所以我们采用倒着访问的顺序
\(Over\)
跪求资瓷QAQ

浙公网安备 33010602011771号