用递归法将一个整数n转化为字符串_扒一扒这种题的外套(343. 整数拆分)

希望通过这篇题解让大家知道“题解区的水有多深”,让大家知道“什么才是好的题解”。

原题地址:https://leetcode-cn.com/problems/integer-break/

题目描述

给定一个正整数  n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。返回你可以获得的最大乘积。

示例 1:

输入: 2 输出: 1 解释: 2 = 1 + 1, 1 × 1 = 1。示例  2:

输入: 10 输出: 36 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。说明: 你可以假设  n  不小于 2 且不大于 58。

思路

我看了很多人的题解直接就是两句话,然后跟上代码:

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [1] * (n + 1)
        for i in range(3, n + 1):
            for j in range(1, i):
                dp[i] = max(j * dp[i - j], j * (i - j), dp[i])
        return dp[n]

这种题解说实话,只针对那些”自己会, 然后去题解区看看有没有新的更好的解法的人“。但是大多数看题解的人是那种自己没思路,不会做的人。那么这种题解就没什么用了。

我认为好的题解应该是新手友好的,并且能够将解题人思路完整展现的题解。比如看到这个题目,我首先想到了什么(对错没有关系),然后头脑中经过怎么样的筛选将算法筛选到具体某一个或某几个。我的最终算法是如何想到的,有没有一些先行知识。

当然我也承认自己有很多题解也是直接给的答案,这对很多人来说用处不大,甚至有可能有反作用,给他们一种”我已经会了“的假象。实际上他们根本不懂解题人本身原本的想法, 也许是写题解的人觉得”这很自然“,也可能”只是为了秀技“。

Ok,下面来讲下我是如何解这道题的

抽象

首先看到这道题,自然而然地先对问题进行抽象,这种抽象能力是必须的。LeetCode 实际上有很多这种穿着华丽外表的题,当你把这个衣服扒开的时候,会发现都是差不多的,甚至两个是一样的,这样的例子实际上有很多。就本题来说,就有一个剑指 Offer 的原题《剪绳子》和其本质一样,只是换了描述方式。类似的有力扣 137 和 645 等等,大家可以自己去归纳总结。

137 和 645 我贴个之前写的题解 https://leetcode-cn.com/problems/single-number/solution/zhi-chu-xian-yi-ci-de-shu-xi-lie-wei-yun-suan-by-3/

「培养自己抽象问题的能力,不管是在算法上还是工程上。」 务必记住这句话!

数学是一门非常抽象的学科,同时也很方便我们抽象问题。为了显得我的题解比较高级,引入一些你们看不懂的数学符号也是很有必要的(开玩笑,没有什么高级数学符号啦)。

实际上这道题可以用纯数学角度来解,但是我相信大多数人并不想看。即使你看了,大多人的感受也是“好 nb,然而并没有什么用”。

这道题抽象一下就是:

令:309d1a6cdb5352c1be6b6947c3b4f0df.png(图 1)

求:1e7c9114ae57eec85f5422fcebbc03ed.png

(图 2)

第一直觉

经过上面的抽象,我的第一直觉这可能是一个数学题,我回想了下数学知识,然后用数学法 AC 了。数学就是这么简单平凡且枯燥。

然而如果没有数学的加持的情况下,我继续思考怎么做。我想是否可以枚举所有的情况(如图 1),然后对其求最大值(如图 2)。

问题转化为如何枚举所有的情况。经过了几秒钟的思考,我发现这是一个很明显的递归问题。具体思考过程如下:

  • 我们将原问题抽象为 f(n)
  • 那么 f(n) 等价于 max(1 * fn(n - 1), 2 * f(n - 2), ..., (n - 1) * f(1))。

用数学公式表示就是:

51b981cc2e8350588f2f1d9a29c72d56.png(图 3)

截止目前,是一点点数学 + 一点点递归,我们继续往下看。现在问题是不是就很简单啦?直接翻译图三为代码即可,我们来看下这个时候的代码:

class Solution:
    def integerBreak(self, n: int) -> int:
        if n == 2: return 1
        res = 0
        for i in range(1, n):
            res = max(res, max(i * self.integerBreak(n - i),i * (n - i)))
        return res

毫无疑问,超时了。原因很简单,就是算法中包含了太多的重复计算。如果经常看我的题解的话,这句话应该不陌生。我随便截一个我之前讲过这个知识点的图。

00fed472f6668538067a46b5ebff228f.png(图 4)

原文链接:https://github.com/azl397985856/leetcode/blob/master/thinkings/dynamic-programming.md

大家可以尝试自己画图理解一下。

看到这里,有没有种殊途同归的感觉呢?

考虑优化

如上,我们可以考虑使用记忆化递归的方式来解决。只是用一个 hashtable 存储计算过的值即可。

class Solution:
    @lru_cache()
    def integerBreak(self, n: int) -> int:
        if n == 2: return 1
        res = 0
        for i in range(1, n):
            res = max(res, max(i * self.integerBreak(n - i),i * (n - i)))
        return res

为了简单起见(偷懒起见),我直接用了 lru_cache 注解, 上面的代码是可以 AC 的。

动态规划

看到这里的同学应该发现了,这个套路是不是很熟悉?下一步就是将其改造成动态规划了。

如图 4,我们的思考方式是从顶向下,这符合人们思考问题的方式。将其改造成如下图的自底向上方式就是动态规划。

0d36ef02424049dbc8b2535b2f43b0b3.png(图 5)

现在再来看下文章开头的代码:

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [1] * (n + 1)
        for i in range(3, n + 1):
            for j in range(1, i):
                dp[i] = max(j * dp[i - j], j * (i - j), dp[i])
        return dp[n]

dp table 存储的是图 3 中 f(n)的值。一个自然的想法是令 dp[i] 等价于 f(i)。而由于上面分析了原问题等价于 f(n),那么很自然的原问题也等价于 dp[n]。

而 dp[i]等价于 f(i),那么上面针对 f(i) 写的递归公式对 dp[i] 也是适用的,我们拿来试试。

// 关键语句
res = max(res, max(i * self.integerBreak(n - i),i * (n - i)))

翻译过来就是:

dp[i] = max(dp[i], max(i * dp(n - i),i * (n - i)))

而这里的 n 是什么呢?我们说了dp是自底向下的思考方式,那么在达到 n 之前是看不到整体的n 的。因此这里的 n 实际上是 1,2,3,4... n。

自然地,我们用一层循环来生成上面一系列的 n 值。接着我们还要生成一系列的 i 值,注意到 n - i 是要大于 0 的,因此 i 只需要循环到 n - 1 即可。

思考到这里,我相信上面的代码真的是不难得出了。

关键点

  • 数学抽象
  • 递归分析
  • 记忆化递归
  • 动态规划

代码

class Solution:
    def integerBreak(self, n: int) -> int:
        dp = [1] * (n + 1)
        for i in range(3, n + 1):
            for j in range(1, i):
                dp[i] = max(j * dp[i - j], j * (i - j), dp[i])
        return dp[n]

总结

培养自己的解题思维很重要, 不要直接看别人的答案。而是要将别人的东西变成自己的, 而要做到这一点,你就要知道“他们是怎么想到的”,“想到这点是不是有什么前置知识”,“类似题目有哪些”。

最优解通常不是一下子就想到了,这需要你在不那么优的解上摔了很多次跟头之后才能记住的。因此在你没有掌握之前,不要直接去看最优解。在你掌握了之后,我不仅鼓励你去写最优解,还鼓励去一题多解,从多个解决思考问题。到了那个时候, 萌新也会惊讶地呼喊“哇塞, 这题还可以这么解啊?”。你也会低调地发出“害,解题就是这么简单平凡且枯燥。”的声音。

扩展

正如我开头所说,这种套路实在是太常见了。希望大家能够识别这种问题的本质,彻底掌握这种套路。另外我对这个套路也在我的新书《LeetCode 题解》中做了介绍,本书目前刚完成草稿的编写,如果你想要第一时间获取到我们的题解新书,那么请发送邮件到 azl397985856@gmail.com,标题著明“书籍《LeetCode 题解》预定”字样。。

推荐阅读

1、哈希表哪家强?几大编程语言吵起来了!

2、别再暴力匹配字符串了,高效的KMP才是真的香!

3、揭开「拓扑排序」的神秘面纱

4、IP 基础知识“全家桶”,45 张图一套带走

5、或许是一本可以彻底改变你刷 LeetCode 效率的题解书

6、迎接Vue3.0 | 在Vue2与Vue3中构建相同的组件

5c1b1d15f087714d181b4248f71c1154.png

8c846beb8437a0d4f709d99fba3ddaff.png

如果觉得文章不错,帮忙点个在看呗

  • 0
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页
评论

打赏作者

weixin_39846378

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值