动态规划两个例子总结

最近在刷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;
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值