<福州集训之旅Day7> | 动态规划类型及其优化方式 |


<更新提示>

<第一次更新>继续刷博客


<正文>

DP(动态规划)

前言
动态规划是一种区别于分治和贪心的一种算法,是求解决策过程最优化的数学方法。它与贪心,分治都有所不同,它和贪心的区别在于它无后效性,它和分治的区别在于他的子问题可以重叠,而且经常有很多重叠。
目录

· 序列DP
· 背包问题
· 区间DP
· 状压DP

序列DP

顾名思义,就是在一个给定的序列上进行dp。序列dp大多是线性dp。状态是可以是一维的,也可以是多维的。
在前我们现讲如何DP:
我们使用一个状态来表示一种情况,然后再在这个状态上向下一个状态进行推导,由此解决一些分治和贪心都难以解决的问题。
如何表示状态的意义?
一般来说, 有两种编号的状态
① 状态[i ] 表示前i 个元素决策组成的一个状态。
② 状态[i ] 表示用到了第i 个元素,和其他在1 到i − 1 间的元素, 决策组成有的一个状态。
我们在实例中说明。
<例一>
最长上升子序列(LIS)
什么是最长上升子序列? 就是给你一个序列,请你在其中求出一段不断严格上升的部分,它不一定要连续。
解析:我们先选f[i]作为状态,表示第i 个作为序列的最后一个点的时候的最长序列。这样,我们很容易得出O(n2)的算法,即一层循环顺序扫描,另一层循环枚举合法的可以接下去的最优值。显然这是一种正确的解法。
我们尝试优化算法。
更改f[i]的意义:f [i] 表示长度为i 的上升子序列的最小末位元素。(不存在设其为INF)更改后,处理出f[i],发现f[i]必然是单调递增的,对于单调递增的序列,我们显然就想到了二分查找的算法。利用二分查找,将该问题的求解复杂度优化到O(nlogn)。
参考代码:

#include<bits/stdc++.h>  
using namespace std;  
#define INF 0x3f3f3f  
int f[10010];//f[i]表示长度为i+1的子序列末尾元素最小值;   
int a[10010];  
int main()  
{  
    int n;  
    while(scanf("%d",&n)!=EOF)  
    {  
        for(int i=0;i<n;i++)  
        {  
            scanf("%d",&a[i]);  
            f[i]=INF;
        }  
        for(int i=0;i<n;i++)  
        {  
            *lower_bound(f,f+n,a[i])=a[i];//找到>=a[i]的第一个元素,并用a[i]替换;   
        }  
        printf("%d\n",lower_bound(f,f+n,INF)-f);//找到第一个INF的地址减去首地址就是最大子序列的长度;   
    }  
    return 0;  
}  

<例二>
最长公共子串(LCS)
给定两个字符串A, B,长度分别为n, m (n, m ≤ 1000),求两个字符串最长的公共子串。
解析:我们继续设置状态:f [i][j] 表示匹配到字符串A 的第i 位,字符串B 的第j 位得到的最长公共子串。在这里,引导出状态转移方程的概念:我们通过一个可行的方程,将第k个状态推导到第k+1个状态。在本题中,我们假设以及得到了f [i][j]之前的值,推导f[i][j],那么有两种情况:
①A字符串的下一位和B字符串的下一位刚好相同,显然有f [i][j] =f[i-1][j-1]+1.
②A字符串的下一位和B字符串的下一位不相同,所有数字没有变化,取A字符串减一个位置的值和B字符串减一个位置的值中的最大值,即f [i][j] =max(f[i-1][j],f[i][j-1]).
讨论完了两种情况,这既是状态转移方程:

f[i][j]={f[i1][j1]+1(A[i]==B[j])max(f[i1][j],f[i][j1])(A[i]!=B[j])

拓展:如何求多个字符串的LCS?
加维,几个字符串就几维,f的多维数组表示的意义相似。

背包问题

· 01背包问题
· 完全背包问题
· 多重背包问题
· 混合三种背包

01背包问题

有N 件物品和一个容量为V 的背包。放入第i 件物品耗费的空间是ci,得到的价值是wi。求解将哪些物品装入背包可使价值总和最大。
解析:定义状态:f[i][j]表示前i件物品放入一个容量为j的背包可以获得的最大价值。
状态转移方程:两种情况,即是与否取第i件物品。
f[i][j]=max(f[i-1][j],f[i-1][j-c[i]]+w[i]).
核心部分代码如下

for(int i=1;i<=N;i++)
    for(int j=c[i];j<=V;j++)
        f[i][j]=max(f[i−1][j],f[i−1][j−c[i]]+w[i]);

如何优化空间复杂度(降维)?
由于两种情况都由[i-1]推导而来,我们考虑将第一维去掉,只留下f[j]表示容量为j的背包可以获得的最大价值。我们采用逆序求解,为了保证物品不被重复取到,01背包中每样物品只能取一次,如果顺序求解的话就有可能反复取到某样物品。
核心代码如下:

for(int i=1;i<=N;i++)
    for(int j=V;j>=c[i];j-−)
        f[j]=max(f[j],f[j−c[i]]+w[i]);
完全背包问题

有N 件物品和一个容量为V 的背包,每件物品都有无限件。放入第i 件物品耗费的空间是ci,得到的价值是wi。求解将哪些物品装入背包可使价值总和最大。
解析:完全背包问题刚好与01背包问题相反,每一个物品可以取无数遍,至于01背包的二维做法,时间复杂度是会太高的,所有我们效仿01背包降维做法,只是像刚才特意提到的一样,将逆序改为顺序,特意让每一个物品能取到无限次即可。除了循环顺序以外和01背包完全相同,就不附代码了。

多重背包问题

有N 件物品和一个容量为V 的背包,第i 件物品都有ni 件。放入第i 件物品耗费的空间是ci,得到的价值是wi。求解将哪些物品装入背包可使价值总和最大。
解析:解法与完全背包类似,对于第i中物品有m[i]+1种策略,只需要在枚举第i减物品的数量k即可。状态转移方程如下:
f [i][j] = max{f [i − 1][j − k ∗ ci] + k ∗ wi}
二进制优化:我们考虑用二进制中的2的整次方数来组合所有种数,把第i件物品转换为若干件物品,进而转换为01背包问题。这里就不细讲了。

混合三种背包

即同时有三种背包问题的情况。
解析:根据三种背包问题的定义,进行分类处理,如果会三种背包问题,使用if进行处理即可。

区间DP

对于区间中的DP,我们常用f[l][r]表示一段连续区间[l][r]上的答案。
常用特性:
1 序列:处理l,r之间分割点k进行操作。
2 环:将序列复制一份并延长,枚举断点。
3 点集回路:利用线段不相交的性质按顺序DP。

<例一>
乘法游戏
乘法游戏是在一行牌上进行的,牌的数量为n ,每张牌上有一个正整数ai。在每一个移动中,玩家拿出一张牌,得分是用它的数字乘以它左边和右边的数,所以不允许拿第1张和最后1张牌。最后一次移动后,这里只剩下两张牌,求最小得分和。
3 ≤ n ≤ 100

解析:定义状态:f[i][j]表示区间i,j上的最优解。即可得状态转移方程f[i][j]=min{f[i][k]+f[k][j]+a[i]*a[k]*a[j]}(j>k>i)这样就得到一种O(n3)的算法
核心代码如下:

for(int len=1;len<=n-1;len++)
{
    for(int i=1;i<=n-len;i++)
    {
        j=i+len;
        for(int k=i+1;k<=j-1;k++)
        {
            f[i][j]=max(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]);
        }
    }
}

状压DP

即把用二进制压位的方法把状态简化,用位运算加速dp。
数据特殊性: 给出的数据在某一个或几个维度上一般具有比较小的范围(可以枚举一类的状态),但是无法用该范围的数值刻画全部状态。
常见优化方式:
1 减少状态总数
· 跳过无用状态
· 变量相互制约
2 减少状态转移数
· 预处理/前缀和
· 部分和优化
· 四边形不等式
· 决策单调性
· 数据结构优化
3 内存优化
· 滚动数组
· 减少状态总数

<例一>
最大字段和
给定一个长度为N 的序列A1···N,请找出一段区间aL, aL+1, · · · , aR,
使得aL + aL+1 + · · · + aR 最大,即最大化。N ≤ 100000,你的答案要保证L ≤ R。
解析:考虑DP做法。固定有端点r,能快速求出max{sum[r]-sum[l-1]}就能解决问题。
所有我们固定r,任务就是找出min{sum[l-1]},f[r]表示[1,r]中的最大子段和,并且一定要取a[r],状态转移方程就是f[r]=max(f[r-1],0)+a[r]。

优化

<例一>
跳过无用状态:青蛙过河
x 轴上分布着M 个石子,在坐标为0 的点上有一只青蛙,每一次它向x 轴正方向跳跃的距离是S 到T 之间的任意正整数(包括S, T) 。当青蛙跳到或跳过坐标为L 的点时,就算青蛙已经过了河。你的任务是确定青蛙要想过河,最少需要踩到的石子数。
L ≤ 109, 1 ≤ S ≤ T ≤ 10, M ≤ 100
解析:当L巨大时,DP的无用状态就很多了,我们采取一个策略:当S!=T时,对于任意d>ST,从x总能跳到x+d,所以就把L缩短到ST,就跳过了无用状态,减少了数据范围。

<例二>
变量互相制约:乌龟棋
乌龟棋的棋盘是一行N 个格子,每个格子上有一个分数(非负整数) 。游戏要求玩家控制一个乌龟棋子,从起点第1 格出发走到终点第N格。
乌龟棋中有M 张卡片,卡片有四种花色,分别对应1, 2, 3, 4 四个数字。每次使用一张卡片,棋子就可以向前移动这张卡片所对应数字的格数。每张卡片在一次游戏中只能使用一次。
玩家在本次游戏中的得分,就是移动乌龟棋从第一格到最后一格的过程中,经过的所有的格子上的分值的和。
数据保证到达终点时刚好用光M 张爬行卡片。
N ≤ 350, M ≤ 120,每种卡片张数≤ 40。

解析:用f (i, a1, a2, a3, a4) 表示玩到第i 格,用了a1 张1, a2张2, a3张3, a4张4的最大得分。所以当前状态要从f (i − 1, a1 − 1, a2, a3, a4), f (i − 2, a1, a2 − 1, a3, a4), f (i −3, a1, a2, a3 − 1, a4), f (i − 4, a1, a2, a3, a4 − 1)转移。但是不难发现i = a1 + 2a2 + 3a3 + 4a4 + 1,所以直接把第一维扔掉就可以了。这就是互相制约的变量条件。


<后记>
初讲DP,蒟蒻能力有限,尽量以后在详细的做一篇博客。


<废话>

猜你喜欢

转载自blog.csdn.net/prasnip_/article/details/79391705
今日推荐