最近在刷LeetCode上的算法题,遇到几个感觉比较好的动态规划题目,再次总结一下:
1:第一题是关于字符串匹配问题。感觉题目本身就是正则表达式一部分算法的实现。题目内容如下:
给定一个字符串 (s
) 和一个字符模式 (p
)。实现支持 '.'
和 '*'
的正则表达式匹配。
'.' 匹配任意单个字符。
'*' 匹配零个或多个前面的元素。
匹配应该覆盖整个字符串 (s
) ,而不是部分字符串。
说明:
s
可能为空,且只包含从a-z
的小写字母。p
可能为空,且只包含从a-z
的小写字母,以及字符.
和*
。
示例 1:
输入:
s = "aa"
p = "a"
输出: false
解释: "a" 无法匹配 "aa" 整个字符串。
示例 2:
输入:
s = "aa"
p = "a*"
输出: true
解释: '*' 代表可匹配零个或多个前面的元素, 即可以匹配 'a' 。因此, 重复 'a' 一次, 字符串可变为 "aa"。
示例 3:
输入:
s = "ab"
p = ".*"
输出: true
解释: ".*" 表示可匹配零个或多个('*')任意字符('.')。
示例 4:
输入:
s = "aab"
p = "c*a*b"
输出: true
解释: 'c' 可以不被重复, 'a' 可以被重复一次。因此可以匹配字符串 "aab"。
示例 5:
输入:
s = "mississippi"
p = "mis*is*p*."
输出: false
既然是动态规划题目首先就是想到状态方程,那么这个题目的状态方程该怎么写。假设 长度为i-1的字符串s是否匹配长度为j-1的正则表达式p用bool变量f[i][j]表示,true表示匹配,false表示不匹配。
假如p[j-1]不为“*”,那么状态方程应该是 f[i][j] = i&&f[i-1][j-1]&&(s[i-1] == p[j-1]||p[j-1]=='.'),这里解释一下这个状态方程,如果s是要有长度的,即包含字符,不包含字符,肯定是匹配的,f[i-1][j-1]是前一个状态方程的状态,后面的两个等号,表示当前状态方程成立的必要条件。
假如p[j-1]为“*”,这种情况可能比较复杂一点,因为“*”表示匹配前面字符的0个或任意个,也就说“*”号前的字符可以是0个,也可以是任意多个,所以我们就不能依据方f[i-1][j-1]来判断了。不多说,先把状态方程给出f[i][j]==f[i][j-2]||( i&&f[i-1][j]&&s[i-1]==p[j-2]&&p[j-2]=='*' ),这个比较难理解的是为何使用f[i][j-2]来判断。我前面提过 “*”号前一个字符,表示在匹配字符串中可以出现0或任意次,也就这个字符可有可无,所以这就需要f[i][j-2]来判断了,这也是j-2的目的。后面难理解的地方是f[i-1][j]这个状态,为何要依据这个来判断,还是因为“*”号前字符可以出现0或者任意次,个人觉得其实这个状态和f[i-1][j-2]是相同的。其他部分还是很好理解的。
下面是相应的代码实现:这个是用二维数组来写的,也可以用一维数组来优化,这里的优化还待以后慢慢理解。
bool isMatch(string s, string p) {
int m = s.size(), n = p.size();
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
dp[0][0] = true;
for (int i = 0; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
dp[i][j] = dp[i][j - 2] || (i && dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
}
else {
dp[i][j] = i && dp[i - 1][j - 1] && (s[i - 1] == p[j - 1] || p[j - 1] == '.');
}
}
}
return dp[m][n];
}
第二题。是关于鸡蛋掉落的问题
鸡蛋掉落的问题,是我第一次遇到,当然我学动态规划也没有多长时间,之间一直看的是背包问题,有名就是《背包九讲》,里面讲的还是挺通俗易懂的,刚接触动态规划的码友可以去看看。对于鸡蛋掉落的问题,网上说也是大厂面试经常遇到的问题,说是经典的动态规划问题。题目内容如下:
你将获得 K
个鸡蛋,并可以使用一栋从 1
到 N
共有 N
层楼的建筑。
每个蛋的功能都是一样的,如果一个蛋碎了,你就不能再把它掉下去。
你知道存在楼层 F
,满足 0 <= F <= N
任何从高于 F
的楼层落下的鸡蛋都会碎,从 F
楼层或比它低的楼层落下的鸡蛋都不会破。
每次移动,你可以取一个鸡蛋(如果你有完整的鸡蛋)并把它从任一楼层 X
扔下(满足 1 <= X <= N
)。
你的目标是确切地知道 F
的值是多少。
无论 F
的初始值如何,你确定 F
的值的最小移动次数是多少?
示例 1:
输入:K = 1, N = 2
输出:2
解释:
鸡蛋从 1 楼掉落。如果它碎了,我们肯定知道 F = 0 。
否则,鸡蛋从 2 楼掉落。如果它碎了,我们肯定知道 F = 1 。
如果它没碎,那么我们肯定知道 F = 2 。
因此,在最坏的情况下我们需要移动 2 次以确定 F 是多少。
示例 2:
输入:K = 2, N = 6
输出:3
示例 3:
输入:K = 3, N = 14
输出:4
第一遍看完这个题目,我完全处于懵逼状态,这个题目在讲什么,怎么看不懂。这里也看出本码狗的语言和理解功底了O(∩_∩)O;后来又看了几面,才看到里面的意思:大概就是给你几个鸡蛋,让你在最坏的情况下,最少需要移动多少次能够测出鸡蛋摔坏的零界点。ps:还是前人伟大,这个题目都能想得出来,常识啊,这个还要几层楼码,一米足够了^_^。言归正传,这个题目怎么看出来是动态规划的问题,感觉很多人都会有这个疑问,其实我也有这个疑问,为何要使用动态规划,因为这个里面涉及到最优问题。在目前我们所遇到的最优问题,一般都是动态规划,当然最短路径,那个就是这个动态规划问题,是另外两个算法的问题。既然想到是动态规划问题,那么就需要列状态方程以及状态方程转移问题。价格k个鸡蛋测试N层楼,最坏情况下最少次数为f[k][n],即状态方程可以这样写f[k][n] = min(max(f[k][n-x]+1, f[k-1][x-1]+1)) 1<=x<=m, f[k][n-x]表示鸡蛋在x楼层落下未摔坏,所以鸡蛋数还是K个,剩下的就是检测x+1 - n楼层了,这里用n-x来时表示,f[k-1][x-1]+1表示在X楼层摔坏了,下面就是检测X-1楼层了,去这两个最大值的目的是可以确定第一次在X层扔下,无论坏还是不坏,在那么多次数下都能检测出来,对于m个最要的数据,当然要去选最小的哪一个了,最优值吗。
这个思路的代码如下:
int superEggDrop(int K, int N )
{
if K < 1 || N < 1
{
return 0
}
//备忘录,存储K个鸡蛋,N层楼条件下的最优化尝试次数
//cache = [K + 1][N + 1]int{}
int* cache = new int[k+1];
//把备忘录每个元素初始化成最大的尝试次数
for (int i = 0; i <= K; i++)
{
cache[i] = new int[N+1]
for int j = 1; j <= N; j++)
{
cache[i][j] = j
}
}
for (int n = 2; n <= K; n++)
{
for (int m = 1; m <= N; m++)
{
//假设楼层数可以是1---N,
int min = cache[n][m]
for (int k = 1; k < m; k++)
{
//M层,N鸡蛋,F(N,K)= Min(Max( F(N-X,K)+ 1, F(X-1,K-1) + 1)),1<=X<=N
//(动态规划)
//鸡蛋碎了
int max = cache[n-1][k-1] + 1
if cache[n][m-k]+1 > max
{
max = cache[n][m-k] + 1 //鸡蛋没碎
}
if max < min
{
min = max
}
}
cache[n][m] = min
}
}
return cache[K][N]
}
这里用到了三重循环,这个时间复杂度太高了,但是也是最好理解的,
下面说一下,网上说的另外一种自底而上的方法
下面的是别人讲的方式 https://www.jianshu.com/p/50103a152617
假设移动x次, k个鸡蛋, 最优解的最坏条件下可以检测n层楼, 层数n = 黑箱子函数f(x, k)
假设从n0 + 1层丢下鸡蛋,
1, 鸡蛋破了
剩下x - 1次机会和k - 1个鸡蛋, 可以检测n0层楼
2, 鸡蛋没破
剩下x - 1次机会和k个鸡蛋, 可以检测n1层楼
那么 临界值层数F在[1, n0 + n1 + 1]中的任何一个值, 都都能被检测出来
归纳的状态转移方程式为 : f(x, k) = f(x - 1, k - 1) + f(x - 1, k) + 1, 即x次移动的函数值可以由x - 1的结果推导, 这个思路很抽象, 需要花时间去理解, 具体看代码, 对照着代码理解
可以简化为黑箱子函数的返回值只跟鸡蛋个数k有关系 :
本次fun(k) = 上次fun(k - 1) + 上次fun(k) +1
int superEggDrop(int K, int N)
{
int moves = 0;
vector<int> vecDp(K + 1, 0);
// dp[i] = n 表示, i 个鸡蛋,利用 moves 次移动,最多可以检测 n 层楼
while( vecDp[K] < N)
{
for (int i = K; i > 0; i--)
{
//逆序从K---1,dp[i] = dp[i]+dp[i-1] + 1 相当于上次移动后的结果,dp[]函数要理解成抽象出来的一个黑箱子函数,跟上一次移动时鸡蛋的结果有关系
vecDp[i] += vecDp[i - 1] + 1;
// 以上计算式,是从以下转移方程简化而来
// dp[moves][k] = 1 + dp[moves-1][k-1] + dp[moves-1][k]
// 假设 dp[moves-1][k-1] = n0, dp[moves-1][k] = n1
// 首先检测,从第 n0+1 楼丢下鸡蛋会不会破。
// 如果鸡蛋破了,F 一定是在 [1:n0] 楼中,
// 利用剩下的 moves-1 次机会和 k-1 个鸡蛋,可以把 F 找出来。
// 如果鸡蛋没破,假如 F 在 [n0+2:n0+n1+1] 楼中
// 利用剩下的 moves-1 次机会和 k 个鸡蛋把,也可以把 F 找出来。
// 所以,当有 moves 个放置机会和 k 个鸡蛋的时候
// F 在 [1, n0+n1+1] 中的任何一楼,都能够被检测出来。
}
moves++;
}
return moves;
}