【PAT】5. 动态规划

【PAT】5. 动态规划

动态规划的递归和递推写法

  • 如果一个问题可以被分解为若干个子问题,且这些子问题会重复出现,那么就称这个问题拥有重叠子问题(Overlapping Subproblems)
  • 如果一个问题的最优解可以由其子问题的最优解有效的构造出来,那么称这个问题拥有最优子结构(Optimal Substructure)
  • 一个问题必须拥有重叠子问题和最优子结构,才能使用动态规划去解决。
  • 状态的无后效性是指:当前状态记录了历史信息,一旦当前状态确定,就不会再改变,且未来的决策只能在已有的一个或若干个状态的基础上进行,历史信息只能通过已有的状态去影响未来的决策
  • 必须设计一个无后效性的状态以及相应的状态转移方程,这也是动态规划的核心

最大连续子序列和

给定一个数组序列 A 1 , A 2 , . . . , A n A_1,A_2,...,A_n A1,A2,...,An,求 i , j ( 1 ≤ i ≤ j ≤ n ) i,j(1\leq i\leq j\leq n) i,j(1ijn),使得 A i + . . . + A j A_i+...+A_j Ai+...+Aj最大,输出这个最大和。

  1. 暴力解法( O ( n 3 O(n^3 O(n3)):枚举左端点和右端点(即枚举i,j)需要 O ( n 2 ) O(n^2) O(n2)的复杂度,而计算 A i + . . . + A j A_i+...+A_j Ai+...+Aj需要 O ( n ) O(n) O(n)复杂度
  2. 记录前缀和( O ( n 2 ) O(n^2) O(n2)):预处理 S [ i ] = A 1 , A 2 , . . . , A i S[i]=A_1,A_2,...,A_i S[i]=A1,A2,...,Ai,这样 A i + . . . + A j = S [ j ] − S [ i − 1 ] A_i+...+A_j=S[j]-S[i-1] Ai+...+Aj=S[j]S[i1]
  3. 动态规划(O(n)),其实左端点的枚举是没有必要的。
    • 令状态dp[i]表示以A[i]作为末尾的连续序列的最大和
    • 状态转移方程dp[i] = max{A[i],dp[i-1]+A[i]},边界dp[0]=A[0];

最长不下降子序列(LIS)

最长不下降子序列(Longest Increasing Sequence, LIS):在一个数字序列中,找到一个最长的子序列(可以不连续),使得这个子序列是不下降(非递减)的。

令dp[i]表示以A[i]结尾的最长不下降子序列长度,这样对A[i]来说就会有两种可能:

  1. 如果存在A[i]之前的元素A[j](j < i),使得A[j]<=A[i]dp[j] + 1 > dp[i](即把A[i]跟在以A[j]结尾的LIS后面,形成一条更长的不下降子序列)
  2. 如果A[i]之前的元素都比A[i]大,那么A[i]就只好自己形成一条LIS,但是长度为1.

最后以A[i]结尾的LIS长度就是步骤1,2中能形成的最大高度。

  • 状态转移方程dp[i]=max{1,dp[j]+1} (j=1,2,...,i-1 && A[j]<A[i])
  • 边界dp[i]=1(i~[1,n])

最长公共子序列(LCS) 2

最长公共子序列(Longest Common Subsequence, LCS):给定两个字符串(或数字序列)A和B,求一个字符串,使得这个字符串是A和B的最长公共部分(子序列可以不连续)

令dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度(下标从1开始),根据A[i]和B[j]的情况,分为两种决策:

  1. A[i]==B[j],则字符串S与字符串B的LCS增加了一位,有dp[i][j] = d[i-1][j-1] + 1
  2. A[i]!=B[j],则字符串A的i号位和字符串B的j号位之前的LCS无法延长,因此dp[i][j]将会继承dp[i-1][j]与dp[i][j-1]中的较大值,即有dp[i][j]=max{dp[i-1,j],dp[i][j-1]}
  3. 边界:dp[i][0] = dp[0][j] = 0

最长回文子串 3

给出一个字符串S,求S的最长回文子串的长度

令dp[i][j]表示S[i]至S[j]所表示的子串是否是回文子串,是则为1,不是为0。根据S[i]是否等于S[j],可以把转移情况分为两类:

  1. S[i]==S[j],那么只要S[i+1]至S[j-1]是回文子串,S[i]至S[j]就是回文子串,令dp[i][j] = dp[i+1][j-1]
  2. 若S[i]!=S[j],那么一定不是回文子串,令d[i][j]=0
  3. 边界:dp[i][i]=1,dp[i][i+1]=(S[i]=S[i+1])?1:0

注意:如果按照i和j从小到大的顺序来枚举子串的两个端点,然后更新dp[i][j],会无法保证dp[i+1][j-1]已经被计算过,从而无法得到正确的dp[i][j]。

注意到边界表示的是长度为1和2的子串,且每次转移时都对子串的长度减了1,不妨考虑按子串的长度和子串的初始位置进行枚举,即可以先枚举子串长度L(L是可以取到整个字符串的长度S.len()的),再枚举左端点i,这样右端点i+L-1也可以直接得到

  • 也可以通过二分+字符串hash,复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
  • 最优秀的是Manacher算法,复杂度为 O ( n ) O(n) O(n)

数塔DP

将一些数字排成数塔的形状,第n层有n个数字。现在从第一层走到第n层,每次只能走向下一层连接的两个数字中的一个,问:最后将路径上所有数字相加后得到的和最大是多少。

令dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和。状态转移方程为dp[i][j]=max(dp[i+1][j],dp[i+1][j+1]) + f[i][j];由于数塔的最后一层的dp值总是等于元素本身,即边界为dp[n][j]=f[n][j](1<=n<=j)

DAG最长路

给定一个有向无环图,怎么样求解整个图的所有路径中权值之和最大的那条。

  • 令dp[i]表示从i号顶点出发能获得的最长路径长度,所有dp[i]的最大值就是整个DAG的最长路径长度
  • 求解dp数组:如果从第i号顶点出发能直接到达顶点 j 1 , j 2 , . . . j k j_1,j_2,...j_k j1,j2,...jk,而 d p [ j 1 ] , d p [ j 2 ] , . . . , d p [ j k ] dp[j_1],dp[j_2],...,dp[j_k] dp[j1],dp[j2],...,dp[jk]均已知,那么有 d p [ i ] = m a x { d p [ j ] + l e n g t h [ i → j ] ∣ ( i , j ) ∈ E } dp[i]=max\left \{ dp[j]+length[i\rightarrow j] |\left ( i,j \right )\in E \right \} dp[i]=max{dp[j]+length[ij](i,j)E},因此我们需要逆拓扑序列的顺序来求解dp数组,或者使用递归的方法,不求出逆递归数组也能计算dp数组
  • 由于由出度为0的顶点出发的最长路径长度为0,因此边界为这些顶点的dp值为0,具体实现中对整个dp数组初始化为0。递归求解出度不是0的顶点,递归过程中遇到已经计算过的顶点则直接返回对应的dp值。
  • 求解具体最长路径(类比Dijkstra):开一个int型choice数组记录最长路径上顶点的后继结点。如果最终可能有多条最长路径,将choice数组改为vector类型的数组即可
  • 如果DAG中有多条路径,选取字典序最小的那条:只需要让遍历i的邻接点的顺序从小到达即可(下面代码自动实现了这个功能)
  • 如果令dp[i]表示以i号顶点结尾能获得的最长路径长度,只要把求解公式变为 d p [ i ] = m a x { d p [ j ] + l e n g t h [ j → i ] ∣ ( j , i ) ∈ E } dp[i]=max\left \{ dp[j]+length[j\rightarrow i] |\left ( j,i \right )\in E \right \} dp[i]=max{dp[j]+length[ji](j,i)E}(相应的求解顺序变为拓扑序),同样可以得出结果,但不能直接得到字典序最小的方案。因为字典序的大小总是先根据序列中较前的部分来判断,因此序列中越靠前的顶点,其dp值应当越后计算。
    int DP(int i){  //i为源点
        if(dp[i] > 0){  
            return dp[i];   //dp[i]已计算得到
        }
        for(int j = 0; j < n; j++){//遍历i的所有边
            if(G[i][j] != INF){
                int temp = DP(j) + G[i][j]; //单独计算,防止if中调用DP函数两次
                if(temp > dp[i]){   //可以获得更长的路径
                    dp[i] = temp;   //覆盖dp[i]
                    choice[i] = j;  //i号顶点的后继结点是j
                }
            }
        }
        return dp[i];   //返回计算完毕的dp[i]
    }
    //调用printfPath前需要先得到最大的dp[i],然后将i作为路径起点传入
    void printPath(int i){
        printf("%d", i);
        while(choice[i] != -1){ //choice数组初始化为-1
            i = choice[i];
            pritnf("->%d", i);
        }
    }
    

固定终点,求DAG的最长路径长度

  • 令dp[i]表示从i号顶点出发到达终点T能获得的最长路径长度,状态转移方程和上面一样,但是边界有很大区别,设置边界为dp[T]=0,并且初始化dp数组为一个负的大数,来保证“无法到达终点”的含义的以表达(即-INF);然后设置一个vis数组表示顶点是否已经被计算,
    int DP(int i){
        if(vis[i]){
            return dp[i];
        }
        vis[i] = true;
        for(int j = 0; j < n; j++){
            if(G[i][j] != INF){
                dp[i] = max(dp[i], DP(j) + G[i][j]);
            }
        }
        return dp[i];
    }
    

背包问题

多阶段动态规划问题:问题可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关。01背包问题就是这样的例子

01背包问题

有n件物品,每件物品的单件重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都只有一件。

dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。考虑对第i件物品的选择策略,有两种策略:

  1. 不放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v]
  2. 放第i件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]的背包中所能获得的最大价值,即dp[i-1][v-w[i]]+c[i]

因此状态转移方程为 d p [ i ] [ v ] = m a x { d p [ i − 1 ] [ v ] , d p [ i − 1 ] [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v]=max\left \{ dp[i-1][v] , dp[i-1][v-w[i]]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[i][v]=max{dp[i1][v],dp[i1][vw[i]]+c[i]}(1in,w[i]vV)

边界为 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0\left (0\le v\le V \right ) dp[0][v]=0(0vV)(即前0件物品放入任何容量为v的背包中都只能获得价值0)。

注意到dp[i][v]只与之前的状态dp[i-1][]有关,所以可以枚举i从1到n,v从0到V,通过边界来递推出整个dp数组。由于dp[i][v]表示的是恰好为v的情况,所以要枚举 d p [ n ] [ v ] ( 0 ≤ v ≤ V ) dp[n][v]\left(0\le v\le V \right ) dp[n][v](0vV),取其最大值才是最后的结果。时间复杂度为 O ( n V ) O(nV) O(nV)

for(int i = 1; i <= n; i++){    //前i件物品
    for(int v = w[i]; v <= V; v++){ //容量至少为w[i],最大为V
        dp[i][v]=max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
    }
}

优化空间复杂度:注意到状态转移方程中计算dp[i][v]时总是只需要dp[i-1][v]左侧部分的数据,且当计算dp[i+1][]部分时,dp[i-1]的数据又完全又不到了(只需要用到dp[i][]),因此不妨直接开一个一维数组dp[v](即把第一维省去),枚举方向改变为i从1到n,v从V到0(逆序!),状态转移方程改变为 d p [ v ] = m a x { d p [ v ] , d p [ v − w [ i ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v]=max\left \{ dp[v] , dp[v-w[i]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[v]=max{dp[v],dp[vw[i]+c[i]}(1in,w[i]vV)。注意v的枚举顺序是从右往左,这样的技巧称为滚动数组,优化的空间复杂度为 O ( V ) O(V) O(V)

for(int i = 1; i <= n; i++){    
    for(int v = V; v >= w[i]; v--){ 
        dp[v]=max(dp[v],dp[v-w[i]+c[i]);
    }
}

特别说明:如果是二维数组存放,v的枚举是顺序还是逆序都无所谓;如果使用一维数组存放,则v的枚举必须是逆序。

完全背包问题

有n件物品,每件物品的单件重量为w[i],价值为c[i]。现有一个容量为V的背包,问如何选取物品放入背包,使得背包内物品的总价值最大。其中每种物品都有无穷件。

和01背包一样,令dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值。考虑对第i件物品的选择策略,有两种策略:

  1. 不放第i件物品,那么dp[i][v] = dp[i-1][v],这一步和01背包一样
  2. 放第i件物品,并不是转移到dp[i-1][v-w[i]]这个状态,而是转移到dp[i][v-w[i]],因为放了第i件物品后还可以继续放第i件物品,直到第二维的v-w[i]无法保持大于等于0为止。

状态转移方程为 d p [ i ] [ v ] = m a x { d p [ i − 1 ] [ v ] , d p [ i ] [ v − w [ i ] ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[i][v]=max\left \{ dp[i-1][v] , dp[i][v-w[i]]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[i][v]=max{dp[i1][v],dp[i][vw[i]]+c[i]}(1in,w[i]vV)

边界为 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0vV)

一维形式的状态转移方程为 d p [ v ] = m a x { d p [ v ] , d p [ v − w [ i ] + c [ i ] } ( 1 ≤ i ≤ n , w [ i ] ≤ v ≤ V ) dp[v]=max\left \{ dp[v] , dp[v-w[i]+c[i] \right \}\left (1\le i\le n,w[i]\le v \le V \right ) dp[v]=max{dp[v],dp[vw[i]+c[i]}(1in,w[i]vV)

边界为 d p [ 0 ] [ v ] = 0 ( 0 ≤ v ≤ V ) dp[0][v]=0(0\le v \le V) dp[0][v]=0(0vV)

写成一维形式后和01背包完全相同,唯一区别在于这里v的枚举顺序是正向枚举,而01背包的一维形式中v必须是逆向枚举。

for(int i = 1; i <= n; i++){    
    for(int v = w[i]; v <= V; v++){ 
        dp[v]=max(dp[v],dp[v-w[i]+c[i]);
    }
}

总结

(1)最大连续子序列和

令状态dp[i]表示以A[i]作为末尾的连续序列的最大和

(2)最长不下降子序列(LIS)

dp[i]表示以A[i]结尾的最长不下降子序列长度

(3)最长公共子序列(LCS)

dp[i][j]表示字符串A的i号位和字符串B的j号位之前的LCS长度

(4)最长回文子串

dp[i][j]表示S[i]S[j]所表示的子串是否是回文子串

(5)数塔DP

dp[i][j]表示从第i行第j个数字出发的到达最底层的所有路径中能得到的最大和

(6)DAG最长路
dp[i]表示从i号顶点出发能获得的最长路径长度

(7)01背包

dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值

(8)完全背包

dp[i][v]表示前i件物品恰好装入容量为v的背包中所能获得的最大价值

  • 1~4:当题目与序列或者字符串(记为A)有关时,可以考虑把状态设计成下面两种形式,然后根据端点特点去考虑状态转移方程。其中XXX均为原问题的表述

    1. dp[i]表示以A[i]结尾(或开头)的XXX。
    2. dp[i][j]表示A[i]A[j]区间的XXX。
  • 5~8:它们的状态设计都包含了某种“方向”的意思,那么分析题目中的状态需要几维来表示,然后对其中的每一维采取下面的某一个表述

    1. 恰好为i
    2. 前i

    在每一维的含义设置完毕后,dp数组的含义可以设置成“令dp数组表示恰好为i(或前i)、恰好为j(或前j)…的XXX”,接下来通过端点的特点去考虑状态转移方程。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值