动态规划问题(一)
作者:互联网
一、递推问题
递推问题的典型代表就是斐波那契数列问题,即已知斐波那契数列前几项(下标从0开始)为0,1,1,2,3,5,8……,求第n项。
通过观察前几项,发现每一项都是前两项的和,因此递推式为
C++部分代码如下:
fib[0] = 0;
fib[1] = 1;
for(int i = 2; i <= n; i ++)
fib[i] = fib[i - 1] + fib[i - 2];
二、记忆化搜索
在DP问题中,记忆化搜索往往指的是在递归中记录计算过的状态,并在后续的计算中跳过已经计算过的状态,从而大大减少递归的计算次数。
我们以上一部分的斐波那契数列问题为例,递归每一层,C++递归部分代码如下:
int f(int n)
{
if(n == 0) return 0;
if(n == 1) return 1;
return f(n - 1) + f(n - 2);
}
可以发现求解过程是一棵二叉树,高度为n,每个节点都计算一次,可以发现n越大,重复计算的次数越多,显然浪费了很多的时间。因此我们考虑如何将每个值只计算一次,这就是记忆化搜索的思想。我们可以将的值存入哈希数组中,这样就实现了记忆化搜索,每次需要的值只需要访问,如果有值就直接使用即可,访问的时间复杂度为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 个阶梯的最小体力花费为 ,而第 i 个阶梯只可能由第 i - 1个阶梯或第 i - 2 个阶梯到达:由第i - 1个阶梯到达,有:;
由第i - 2个阶梯到达,有:;
又由于起点可以在第 0 个阶梯或者第 1 个阶梯,因此,状态转移方程为
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]);
2、最大子段和
最大子段和问题本身就是一个典型问题:
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
由于是连续子数组,我们令以 i 号元素结尾的最大和为,那么一定包含在中。再分析、……是否包含于中,可以发现:
当时,、……如果包含在中,那么,此时不是最大连续子数组,所以这种情况下;
当时, 、…… 包含进中可以使,因此。
综上分析,可以得出状态转移方程为
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]);
}
3、最长单调子序列
以最长递增子序列问题为例:
给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。
子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。
对于序列,我们令表示以第 i 个数为结尾的最长递增子序列的长度,在这个序列中,的前一个数一定是中的一个,可以得出,其中。
因此,状态转移方程为
这里是的一个最优子结构,自然会想到,有一种特殊情况就是不存在最优子结构,此时。
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]);
三、二维DP
1、最长公共子序列
以字符串最长公共子序列问题为例:
给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。
两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。
对于两个字符串text1的第 i 项和text2的第 j 项,记为text1以第 i 项结尾和text2以第 j 项结尾的公共子序列长度,分为两种情况:
第一种情况是,可以看作text1以第 i - 1 项结尾和text2以第 j - 1 项结尾的公共子序列长度加上1,即;
第二种情况是,又可以分成两类,一类是看作text1以第 i 项结尾、text2以第 j - 1 项结尾;另一类是看作text1以第 i 项结尾、text2以第 j - 1 项结尾,再取两类中的最大值即可,故。
再考虑边界情况:当或时,很明显。
综上分析,可以得出状态转移方程为
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]);
}
2、最小编辑距离
编辑距离计算的是将源字符串修改成目标字符串需要操作的次数,例题如下:
给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。
你可以对一个单词进行如下三种操作:
- 插入一个字符
- 删除一个字符
- 替换一个字符
设源字符串word1的每个字符为(表示字符串word1的长度),目标字符串word2的每个字符为(表示字符串word2的长度),我们令表示源字符串变成目标字符串的最小操作数,对于三种不同的操作,就有三种不同的情况:
第一种,假设变成最小操作数为,只需再插入一个字符即可变成目标字符串,因此;
第二种,假设变成最小操作数为,只需删除即可变成目标字符串,因此;
第三种,假设变成最小操作数为,这里又有两种情况,如果,那么不需要操作就已经变成目标字符串了,即;如果,那么只需将替换为即可变成目标字符串,即。
再考虑边界情况:如果或,即两个字符串中至少有一个为空字符串:
如果源字符串是空字符串,目标字符串不是空字符串,那么每次操作都是插入目标字符串中的一个字符,因此最小操作数;
如果源字符串不是空字符串,目标字符串是空字符串,那么每次操作都是删除源字符串中的一个字符,因此最小操作数;
如果源字符串和目标字符串都是空字符串,那么不需要进行操作,因此最小操作数。
综上分析,可以得出状态转移方程为
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));
}
}
3、串匹配
串匹配的经典题目是带通配符的匹配问题:
给定一个字符串 (s) 和一个字符模式 (p) ,实现一个支持 '?' 和 '*' 的通配符匹配。
'?' 可以匹配任何单个字符。
'*' 可以匹配任意字符串(包括空字符串)。
两个字符串完全匹配才算匹配成功。说明:
s 可能为空,且只包含从 a-z 的小写字母。
p 可能为空,且只包含从 a-z 的小写字母,以及字符 ? 和 *。
我们令字符串s每个字符为(表示字符串s的长度),字符模式p每个字符为(表示字符模式p的长度),令bool型数组表示字符串s到和字符模式p到是否匹配,匹配为true,不匹配为false。根据字符的类型分成三种情况:
第一种,如果为小写字母,那么只有当与相同时,二者才能匹配成功,因此当且仅当且;
第二种,如果为,那么无论是什么,都能匹配成功,因此当且仅当。
第三种,如果为,由于有两种用法, 所以分两类讨论:
如果用来匹配空字符串,那么与的匹配由与的匹配转移而来,因此 当且仅当;
如果用来匹配任意字符串,那么与的匹配由与的匹配转移而来,因此 当且仅当。
再考虑边界情况:
如果字符串s是空字符串,字符模式p不是空字符串,由于只有可以匹配空字符串,因此p的前j个字符必须均为才能匹配成功;
如果字符串s不是空字符串,字符模式p是空字符串,由于空字符串无法匹配空字符串,因此这种情况是无法匹配成功的;
如果字符串s和字符模式p均为空字符串,那么一定匹配成功。
综上分析,可以得到状态转移方程为
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];
}
}
标签:字符,匹配,int,问题,5C%,20%,字符串,动态,规划 来源: https://blog.csdn.net/qq_58207591/article/details/120389512