ACM算法总结 动态规划(一)



简介

动态规划,dynamic programming,简称 dp,通过把原问题分解成更小的子问题来解决问题,适用于有重叠子问题最优子结构的问题。重叠子问题是指某一个子问题的答案可能被多个更大的问题使用到,而最优子结构是指当更大的问题满足最优解时该子问题也满足这个解。

所以,当我们使用动态规划解决某一个状态的最优化问题时,往往需要用到之前的某些状态的结果,而之前的这些状态是在之前已经计算出最优值的,这和递推有很大的相似之处。不严格情况下往往都统称为动态规划。

解决动态规划问题最重要的两个东西是状态转移方程边界条件




经典问题

背包问题

(0-1背包):有 n 种物品和一个容量为 V 的背包,其中第 i 种物品的体积为 v[i],价值为 w[i],每种物品只有一个,求能放入背包的最大价值和。

设 f[i][j] 表示前 i 种物品放入容量为 j 的背包的最大价值,对于某个容量 j ,对于第 i 种物品只有放或者不放两种选择,故状态转移方程为 f ( i , j ) = m a x { f ( i − 1 , j ) , f ( i − 1 , j − v i ) + w i } f(i,j)=max\{f(i-1,j),f(i-1,j-v_i)+w_i\} f(i,j)=max{f(i1,j),f(i1,jvi)+wi}

代码如下:

n=read(),V=read();
REP(i,1,n) v[i]=read(),w[i]=read();
REP(i,1,n) REP_(j,V,v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[V];

注意上面代码进行了空间优化,第二维之所以要反着枚举,是因为每种物品只能选一次,反着枚举避免了多次选择。

(完全背包):有 n 种物品和一个容量为 V 的背包,其中第 i 种物品的体积为 v[i],价值为 w[i],并且每种物品都可以无限选取,求能放入背包的最大价值和。

对应于 0-1背包,只需将第二维正向枚举,就变成了可以无限选取。

代码如下:

n=read(),V=read();
REP(i,1,n) v[i]=read(),w[i]=read();
REP(i,1,n) REP(j,v[i],V) f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[V];

(多重背包):有 n 种物品和一个容量为 V 的背包,其中第 i 种物品的体积为 v[i],价值为 w[i],有 s[i] 件,求能放入背包的最大价值和。

我们知道数字在计算机中都是用二进制进行存储,所以只要合理选取若干个二的幂,就可以组成任意数字。所以对于每个 s[i],将其分解成 1,2,4,... 以及最后的余数(比如 9 分解成 1,2,4,2),那么可以发现这些分解后的数字可以且只能组成任意 0-s[i] 的数。所以把能选 s[i] 个的物品分解为若干个只能选一个的物品,多重背包问题就变成了 0-1背包问题。

代码如下:

n=read(),V=read();
REP(i,1,n) v[i]=read(),w[i]=read(),s[i]=read();
int tot=n;
REP(i,1,n) if(s[i]>1)
{
    s[i]--;
    for(int j=2;j<=s[i];s[i]-=j,j<<=1) w[++tot]=w[i]*j,v[tot]=v[i]*j;
    if(s[i]) w[++tot]=w[i]*s[i],v[tot]=v[i]*s[i];
}
REP(i,1,tot) REP_(j,V,v[i]) f[j]=max(f[j],f[j-v[i]]+w[i]);
cout<<f[V];

(混合背包):有 n 种物品和一个容量为 V 的背包,其中第 i 种物品的体积为 v[i],价值为 w[i],且有些物品只有一件,有些有无限多件,有些有 s[i] 件,求能放入背包的最大价值和。

将有 s[i] 件的物品二进制分解,原问题就变成了 0-1背包和完全背包的混合。枚举第 i 件物品时,如果只能选取一件,那么第二维体积反向枚举;如果可以选取无穷多件,那么第二维正向枚举。

(二维费用背包):有 n 种物品和一个容量为 V ,最大承受重量为 M 的背包,其中第 i 种物品的体积为 v[i],重量为 m[i],价值为 w[i],每种物品只有一个,求能放入背包的最大价值和。

背包的体积和最大承受重量可以看成两种费用,设 f[i][j][k] 为前 i 中物品放入容量为 j,最大承受重量为 k 的背包中的最大价值,那么状态转移方程为: f ( i , j , k ) = m a x { f ( i , j , k ) , f ( i , j − v i , k − m i ) + w i } f(i,j,k)=max\{f(i,j,k),f(i,j-v_i,k-m_i)+w_i\} f(i,j,k)=max{f(i,j,k),f(i,jvi,kmi)+wi} 。和 0-1背包差不多,只用对后两维倒序枚举即可。

(分组背包):在 0-1背包的基础上将物品分了若干组,每一组最多只能选择一件物品。

设 f[i][j] 表示前 i 组物品放入容量为 j 的背包中的最大价值,状态转移方程为 组 f ( i , j ) = m a x { f ( i − 1 , j ) , f ( i − 1 , j − v k ) + w k } ,   k ∈ 组 i 组f(i,j)=max\{f(i-1,j),f(i-1,j-v_k)+w_k\}, \ k\in 组_i f(i,j)=max{f(i1,j),f(i1,jvk)+wk}, ki


矩阵链乘

有 n 个矩阵相乘,适当地添加括号,使得乘法计算次数最少( A m n ∗ B n p A_{mn}*B_{np} AmnBnp的乘法计算次数为 m n p mnp mnp 次)。

设数组 x x x,使得第 i 个矩阵的大小为 x [ i − 1 ] × x [ i ] x[i-1]\times x[i] x[i1]×x[i] ,设 f[i][j] 表示矩阵序列 A i A_i Ai A i + 1 A_{i+1} Ai+1 … \dots A j A_j Aj 的最少计算次数,状态转移方程为: f ( i , j ) = m i n { f ( i , k ) + f ( k + 1 , j ) + x i − 1 x k x j } ,   i ∈ [ i , j ) f(i,j)=min\{f(i,k)+f(k+1,j)+x_{i-1}x_kx_j\}, \ i\in [i,j) f(i,j)=min{f(i,k)+f(k+1,j)+xi1xkxj}, i[i,j)

注意要先枚举区间长度,这样才能保证处理当前状态时,子问题已经都得到了最优解。


其它

还有很多经典的 dp 问题,比如钢条切割、流水调度、跳台阶等等。处理这类问题最重要的是合理地利用数组表示各个状态,然后找到状态转移方程。




最长上升子序列

Longest Increasing Subsequence,简称LIS:给定一段序列,求最长的上升子序列的长度。

设 f[i] 为:上升子序列长度为 i 时的最后一个数的最小值。首先可以发现,f 具有单调性,因为当上升子序列长度越长时,其最后一个数一定越大。循环枚举原序列 a,对于 a[i],我们把它放到 f 中观察,如果 f[j]<a[i],说明对于长度为 j 的上升子序列,可以把 a[i] 放在最后使得它成为以 a[i] 结尾,长度为 j+1 的上升子序列,这时有两种情况:如果 f[j+1]<=a[i],说明把 a[i] 放到长度为 j 的上升子序列最后并不是最优的;如果 f[j+1]>a[i],则说明是最优的(要特别注意等号的位置)。

所以一开始把 f 数组全部初始化为 inf,对于每个 a[i],在 f 中找出第一个大于等于 a[i] 的位置,赋值为 a[i]。最后的最长上升子序列长度就是 f 中不为 inf 的个数。

最长上升子序列代码如下:

n=read();
REP(i,1,n) a[i]=read();
fill(f,f+n+5,inf);
REP(i,1,n) *lower_bound(f,f+n,a[i])=a[i];
cout<<lower_bound(f,f+n,inf)-f;

如果要求的是最长不下降子序列,那么我们对于每个 a[i],在 f 中要找的就是第一个大于 a[i] 的位置赋值为 a[i],因为这样包括了之前等于 a[i] 这种情况,就表明可以在末尾添加相同的数。

最长不下降子序列代码如下:

n=read();
REP(i,1,n) a[i]=read();
fill(f,f+n+5,inf);
REP(i,1,n) *upper_bound(f,f+n,a[i])=a[i];
cout<<lower_bound(f,f+n,inf)-f;




区间dp

区间dp其实就是针对区间的dp,比如矩阵链乘就是区间dp问题。一般的思路是枚举区间长度,然后枚举起点,然后计算当前区间的最优值。这样可以保证处理大区间时,小区间(子问题)已经计算出了最优解。




树形dp

就是在树形结构上的dp。当处理某个结点的最优解问题时,我们使用其儿子的最优值作为已知条件,体现了最优子结构的特点。

一个例子 没有上司的舞会 :对于一棵树,如果选择了某个结点,那么它的所有儿子都不能选择,每个结点都有一个权值,求最大的权值和。

设 f[i][j](j=0,1)表示结点 i 选择情况为 j(1选择,0不选择)时的最大权值和,状态转移方程为:

f ( i , 0 ) = ∑ u m a x { f ( u , 0 ) , f ( u , 1 ) } , f ( i , 1 ) = ∑ u f ( u , 0 ) + R i ,     u 为 i 的 儿 子 f(i,0)=\sum_{u}{max\{f(u,0),f(u,1)\}}, \\ f(i,1)=\sum_u f(u,0)+R_i,\ \ \ u为i的儿子 f(i,0)=umax{f(u,0),f(u,1)},f(i,1)=uf(u,0)+Ri,   ui

一般用 dfs 从根结点开始遍历树形结构,递归地处理每个结点的最优值,叶子结点作为边界条件。




状压dp

状态压缩,就是把某一状态和一个二进制数结合在一起,用一个数去表示这个状态。

用一个例子( 「SCOI2005」互不侵犯)去解释状态压缩和状态压缩dp:

在N×N的棋盘里面放K个国王,使他们互不攻击,共有多少种摆放方案。国王能攻击到它上下左右,以及左上左下右上右下八个方向上附近的各一个格子,共8个格子。

我们对于一行 N 个格子状态压缩,放国王则表示为 1,不放国王则表示为 0,那么这一行的状态就可以表示为一个二进制数。从上往下扫描,当处理某一行时,只用考虑上一行的状态是什么,以此来决定这一行的某一状态是否可行。所以可以首先处理出所有状态中哪些状态合法,然后处理出哪两种状态相邻是合法的,然后设 f ( i , j , k ) f(i,j,k) f(i,j,k) 表示第 i 行为状态 j ,总共放了 k 个国王的放置方法总数,状态转移方程为:
f ( i , j , k ) = ∑ v a l i d ( u , j ) f ( i − 1 , u , k − s ( j ) ) f(i,j,k)=\sum_{valid(u,j)}f(i-1,u,k-s(j)) f(i,j,k)=valid(u,j)f(i1,u,ks(j))
其中 valid(u, j) 表示状态 u 和状态 j 相邻合法,s(j) 表示状态 j 所放置的国王数目。

由于状态数目和元素个数是按指数级增长的,所以一般状压dp的元素数目不会很多(最多十几个);另外用二进制数表示状态,意味着要对位运算熟练掌握。

有时候状态转移要考虑某一个状态的子集(即当前选取状态的子集,比如对于状态10011,其子集有10001, 00011等等),对于状态 x,枚举其子集的方法为:

for(int i=x&(x-1);i;i=(i-1)&x)

其中对于某一个子集 i,其补集为 i^n

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值