ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

动态规划问题(一)

2021-09-21 14:02:10  阅读:219  来源: 互联网

标签:字符 匹配 int 问题 5C% 20% 字符串 动态 规划


一、递推问题

递推问题的典型代表就是斐波那契数列问题,即已知斐波那契数列前几项(下标从0开始)为0,1,1,2,3,5,8……,求第n项。
通过观察前几项,发现每一项都是前两项的和,因此递推式为f(n) = \begin{cases} 0, & n = 0 \\ 1, & n = 1 \\ f(n - 1) + f(n - 2), & n \geqslant 2 \end{cases} 
C++部分代码如下:

fib[0] = 0;
fib[1] = 1;
for(int i = 2; i <= n; i ++)
    fib[i] = fib[i - 1] + fib[i - 2];

例题:Leetcode509 斐波那契数

二、记忆化搜索

在DP问题中,记忆化搜索往往指的是在递归中记录计算过的状态,并在后续的计算中跳过已经计算过的状态,从而大大减少递归的计算次数。

我们以上一部分的斐波那契数列问题为例,递归每一层f(n) = f(n - 1) + f(n - 2)\; \; (n \geqslant 2),C++递归部分代码如下:

int f(int n)
{
    if(n == 0) return 0;
    if(n == 1) return 1;
    return f(n - 1) + f(n - 2);
}

可以发现求解过程是一棵二叉树,高度为n,每个节点都计算一次,可以发现n越大,重复计算的次数越多,显然浪费了很多的时间。因此我们考虑如何将每个值只计算一次,这就是记忆化搜索的思想。我们可以将f[i]的值存入哈希数组h[i]中,这样就实现了记忆化搜索,每次需要的值只需要访问h[i],如果有值就直接使用即可,访问的时间复杂度为O(1)。

C++记忆化搜索部分代码如下:

int h[N];

void init(){
    memset(h, -1, sizeof h);   // 初始化哈希数组
}

int f(int n)
{
    if(h[n] != -1) return h[n];
    if(n == 0) return 0;
    if(n == 1) return 1;
    h[n] = f(n - 1) + f(n - 2);
    return h[n];
}

三、线性DP

1、最小花费

最小花费问题的典型代表就是爬楼梯问题

数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。

每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。

请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。

我们令走到第 i 个阶梯的最小体力花费为 f(i),而第 i 个阶梯只可能由第 i - 1个阶梯或第 i - 2 个阶梯到达:由第i - 1个阶梯到达,有:f(i) = f(i - 1) + cost(i - 1)

                  由第i - 2个阶梯到达,有:f(i) = f(i - 2) + cost(i - 2)

又由于起点可以在第 0 个阶梯或者第 1 个阶梯,因此,状态转移方程为f(i) = \begin{cases} 0, & i = 0, 1 \\ min[f(i - 1) + cost(i - 1), f(i - 2) + cost(i -2)], & i \geqslant 2 \end{cases}

C++部分代码如下:

f[0] = f[1] = 0;
for(int i = 2; i <= n; i ++)
    f[i] = min(f[i - 1] + cost[i - 1], f[i - 2] + cost[i - 2]);

例题:Leetcode746 使用最小花费爬楼梯

2、最大子段和

最大子段和问题本身就是一个典型问题:

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

由于是连续子数组,我们令以 i 号元素结尾的最大和为f(i),那么nums(i)一定包含在f(i)中。再分析nums(i - 1)nums(i - 2)……nums(k)是否包含于f(i)中,可以发现:

f(i - 1) \leqslant 0时,nums(i - 1)nums(i - 2)……nums(k)如果包含在f(i)中,那么f(i) \leqslant nums(i),此时f(i)不是最大连续子数组,所以这种情况下f(i) = nums(i)

f(i - 1) > 0时, nums(i - 1)nums(i - 2)……nums(k) 包含进f(i)中可以使f(i) > nums(i),因此f(i) = f(i - 1) + nums(i)

综上分析,可以得出状态转移方程为

 f(i) = \begin{cases} nums(0), & i = 0 \\ nums(i), & f(i - 1) \leqslant 0 \\ nums(i) + f(i - 1), & f(i - 1) > 0 \end{cases}

C++部分代码如下:

f[0] = nums[0];
int maxn = nums[0];
for(int i = 1; i < n; i ++){
    if(f[i - 1] <= 0) f[i] = nums[i];
    else f[i] = f[i - 1] + nums[i];
    maxn = max(maxn, f[i]);
}

例题:Leetcode53 最大子序和

3、最长单调子序列

最长递增子序列问题为例:

给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。

子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

对于序列a_{i}(1 \leqslant i \leqslant n),我们令f(i)表示以第 i 个数为结尾的最长递增子序列的长度,在这个序列中,a_{i}的前一个数一定是a_{j}(1 \leqslant j < i)中的一个,可以得出f(i) = f(j) + 1,其中a_{j} < a_{i}

因此,状态转移方程为

f(i) = max(f(j)) + 1 \; \; \; (a_j < a_i, \; \; 1 \leqslant j \leqslant i - 1)

这里max(f(j))f(i)的一个最优子结构,自然会想到,有一种特殊情况就是f(i)不存在最优子结构,此时f(i) = 1

C++部分代码如下:

int length = 0;
for(int i = 0; i < n; i ++){
    f[i] = 1;
    for(int j = 0; j < i; j ++)
        if(nums[j] < nums[i]) f[i] = max(f[i], f[j] + 1);
}
for(int i = 0; i < n; i ++)
    length = max(length, f[i]);

例题:Leetcode300 最长递增子序列

三、二维DP

1、最长公共子序列

字符串最长公共子序列问题为例:

给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。

一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。

例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

对于两个字符串text1的第 i 项text1(i)和text2的第 j 项text2(j),记dp[i][j]为text1以第 i 项结尾和text2以第 j 项结尾的公共子序列长度,分为两种情况:

第一种情况是text1(i) = text2(j),可以看作text1以第 i - 1 项结尾和text2以第 j - 1 项结尾的公共子序列长度加上1,即dp[i][j] = dp[i - 1][j - 1] + 1

第二种情况是text1(i) \neq text2(j),又可以分成两类,一类是看作text1以第 i 项结尾、text2以第 j - 1 项结尾;另一类是看作text1以第 i 项结尾、text2以第 j - 1 项结尾,再取两类中的最大值即可,故dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])

再考虑边界情况:当i = 0j = 0时,很明显dp[0][j] = 0, dp[i][0] = 0

综上分析,可以得出状态转移方程为

dp[i][j] = \begin{cases} 0, & i = 0 \; \; or \; \; j = 0 \\ dp[i - 1][j - 1] + 1, & i, j > 0,\; \; text1[i] = text2[j] \\ max(dp[i][j - 1], dp[i - 1][j]), & i, j > 0,\; \; text1[i] \neq text2[j] \end{cases}

C++部分代码如下:

for(int i = 1; i <= n1; i ++)   // 这里n1表示text1字符串的长度,n2表示text2字符串的长度
    for(int j = 1; j <= n2; j ++){
        if(text1[i - 1] == text2[j - 1])
            dp[i][j] = dp[i - 1][j - 1] + 1;
        else
            dp[i][j] = max(dp[i][j - 1], dp[i - 1][j]);
    }

例题:Leetcode1143 最长公共子序列

2、最小编辑距离

编辑距离计算的是将源字符串修改成目标字符串需要操作的次数,例题如下:

给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。

你可以对一个单词进行如下三种操作:

  • 插入一个字符
  • 删除一个字符
  • 替换一个字符

设源字符串word1的每个字符为a_{i}(1 \leqslant i \leqslant n_{1})n_1表示字符串word1的长度),目标字符串word2的每个字符为b_{j}(1 \leqslant j \leqslant n_{2})n_2表示字符串word2的长度),我们令dp[i][j]表示源字符串变成目标字符串的最小操作数,对于三种不同的操作,就有三种不同的情况:

第一种,假设a_1,a_2,......,a_i变成b_1,b_2,......,b_{j-1}最小操作数为dp[i][j - 1],只需再插入一个字符即可变成目标字符串,因此dp[i][j] = dp[i][j - 1] + 1

第二种,假设a_1,a_2,......,a_{i -1}变成b_1,b_2,......,b_{j}最小操作数为dp[i - 1][j],只需删除a_i即可变成目标字符串,因此dp[i][j] = dp[i - 1][j] + 1

第三种,假设a_1,a_2,......,a_{i - 1}变成b_1,b_2,......,b_{j - 1}最小操作数为dp[i - 1][j - 1],这里又有两种情况,如果a_i = b_j,那么不需要操作就已经变成目标字符串了,即dp[i][j] = dp[i - 1][j - 1];如果a_i \neq b_j,那么只需将a_i替换为b_j即可变成目标字符串,即dp[i][j] = dp[i - 1][j - 1] + 1

再考虑边界情况:如果i = 0j = 0,即两个字符串中至少有一个为空字符串:

        如果源字符串是空字符串,目标字符串不是空字符串,那么每次操作都是插入目标字符串中的一个字符,因此最小操作数dp[0][j] = j

        如果源字符串不是空字符串,目标字符串是空字符串,那么每次操作都是删除源字符串中的一个字符,因此最小操作数dp[i][0] = i

        如果源字符串和目标字符串都是空字符串,那么不需要进行操作,因此最小操作数dp[i][j] = 0

综上分析,可以得出状态转移方程为

dp[i][j] = \begin{cases} i + j, & i = 0 \; \; or \; \; j = 0 \\ min(dp[i][j - 1] + 1, dp[i - 1][j] + 1, \begin{cases} dp[i - 1][j - 1], & a_i = b_j \\ dp[i - 1][j - 1] + 1, & a_i \neq b_j \end{cases}), & i, j \neq 0 \end{cases}

C++部分代码如下:

// n1表示字符串word1的长度,n2表示字符串word2的长度
if(n1 * n2 == 0) return n1 + n2;   // 至少一个字符串为空串的情况

// 初始化边界状态
for(int i = 0; i <= n1; i ++) dp[i][0] = i;
for(int j = 0; j <= n2; j ++) dp[0][j] = j;

// 计算所有DP值
for(int i = 1; i <= n1; i ++){
    for(int j = 1; j <= n2; j ++){
        int in = dp[i - 1][j] + 1;
        int de = dp[i][j - 1] + 1;
        int re = dp[i - 1][j - 1];
        if(word1[i - 1] != word2[j - 1])   // 判断要替换的两个字符是否相同,不同则次数+1
            re += 1;
        dp[i][j] = min(in, min(de, re));
    }
}

例题:Leetcode72 编辑距离

3、串匹配

串匹配的经典题目是带通配符的匹配问题

给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。

'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。

说明:

s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。

我们令字符串s每个字符为s_i(1 \leqslant i \leqslant n1)n_1表示字符串s的长度),字符模式p每个字符为p_j(1 \leqslant j \leqslant n2)n_2表示字符模式p的长度),令bool型数组dp[i][j]表示字符串s到s_i和字符模式p到p_j是否匹配,匹配为true,不匹配为false。根据字符的类型分成三种情况:

第一种,如果p_j为小写字母,那么只有当s_ip_j相同时,二者才能匹配成功,因此dp[i][j] = true当且仅当s_i = p_jdp[i - 1][j - 1] = true

第二种,如果p_j'?',那么无论s_i是什么,都能匹配成功,因此dp[i][j] = true当且仅当dp[i - 1][j - 1] = true

第三种,如果p_j'*',由于'* '有两种用法, 所以分两类讨论:

        如果用来匹配空字符串,那么p_js_i的匹配由p_{j - 1}s_{i}的匹配转移而来,因此dp[i][j] = true        当且仅当dp[i][j - 1] = true

        如果用来匹配任意字符串,那么p_js_i的匹配由p_is_{i - 1}的匹配转移而来,因此                ​​​​​​​        dp[i][j] = true当且仅当dp[i - 1][j] = true

再考虑边界情况:

        如果字符串s是空字符串,字符模式p不是空字符串,由于只有'*'可以匹配空字符串,因此p的前j个字符必须均为'*'才能匹配成功;

        如果字符串s不是空字符串,字符模式p是空字符串,由于空字符串无法匹配空字符串,因此这种情况是无法匹配成功的;

        如果字符串s和字符模式p均为空字符串,那么一定匹配成功。

综上分析,可以得到状态转移方程为

dp[i][j] = \begin{cases} dp[i - 1][j - 1], & p_j = s_i \; \; or \; \; p_j = '?' \\ dp[i - 1][j] \vee dp[i][j - 1], & p_j = '*' \\ true, & i, j = 0 \\ false, & i \neq 0, \; \; j = 0 \\ \begin{cases} true, & p_k \equiv '*'(1 \leqslant k \leqslant j) \\ false, & p_k \not\equiv '*'(1 \leqslant k \leqslant j) \end{cases}, & i = 0, \; \; j \neq 0 \end{cases}

C++部分代码如下:

dp[0][0] = true;
for(int i = 1; i <= n2; i ++){
    if(p[i - 1] == '*') dp[0][i] = true;
    else break;
}
for(int i = 1; i <= n1; i ++){
    for(int j = 1; j <= n2; j ++){
        if(p[j - 1] == '*') dp[i][j] = dp[i - 1][j] | dp[i][j - 1];
        else if(p[j - 1] == '?' || p[j - 1] == s[i - 1]) dp[i][j] = dp[i - 1][j - 1];
    }
}

例题:Leetcode44 通配符匹配

标签:字符,匹配,int,问题,5C%,20%,字符串,动态,规划
来源: https://blog.csdn.net/qq_58207591/article/details/120389512

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有