leecode/problems/343.integer-break.md
2020-05-22 18:17:19 +08:00

192 lines
8.5 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 题目地址(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。
## 思路
希望通过这篇题解让大家知道“题解区的水有多深”,让大家知道“什么才是好的题解”。
我看了很多人的题解直接就是两句话,然后跟上代码:
```python
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 的原题[《剪绳子》](https://leetcode-cn.com/problems/jian-sheng-zi-lcof/)和其本质一样,只是换了描述方式。类似的有力扣 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然而并没有什么用”。
这道题抽象一下就是:
令:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1geu3kolxoyj305o03cwef.jpg)
(图 1
求:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1geu3jy6mxkj305o0360sp.jpg)
(图 2
## 第一直觉
经过上面的抽象,我的第一直觉这可能是一个数学题,我回想了下数学知识,然后用数学法 AC 了。 数学就是这么简单平凡且枯燥。
然而如果没有数学的加持的情况下,我继续思考怎么做。我想是否可以枚举所有的情况(如图 1然后对其求最大值如图 2
问题转化为如何枚举所有的情况。经过了几秒钟的思考,我发现这是一个很明显的递归问题。 具体思考过程如下:
- 我们将原问题抽象为 f(n)
- 那么 f(n) 等价于 max(1 \* fn(n - 1), 2 \* f(n - 2), ..., (n - 1) \* f(1))。
用数学公式表示就是:
![](https://tva1.sinaimg.cn/large/007S8ZIlly1geu3swzc9ej30co03yaa4.jpg)
(图 3
截止目前,是一点点数学 + 一点点递归,我们继续往下看。现在问题是不是就很简单啦?直接翻译图三为代码即可,我们来看下这个时候的代码:
```python
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
```
毫无疑问,超时了。原因很简单,就是算法中包含了太多的重复计算。如果经常看我的题解的话,这句话应该不陌生。我随便截一个我之前讲过这个知识点的图。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1geu3zfz89jj313p0u0wnj.jpg)
(图 4)
> 原文链接https://github.com/azl397985856/leetcode/blob/master/thinkings/dynamic-programming.md
大家可以尝试自己画图理解一下。
> 看到这里,有没有种殊途同归的感觉呢?
## 考虑优化
如上,我们可以考虑使用记忆化递归的方式来解决。只是用一个 hashtable 存储计算过的值即可。
```python
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我们的思考方式是从顶向下这符合人们思考问题的方式。将其改造成如下图的自底向上方式就是动态规划。
![](https://tva1.sinaimg.cn/large/007S8ZIlly1geu4a1grbvj31eq0r0wj8.jpg)
(图 5)
现在再来看下文章开头的代码:
```python
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 即可。
思考到这里,我相信上面的代码真的是`不难得出`了。
## 关键点
- 数学抽象
- 递归分析
- 记忆化递归
- 动态规划
## 代码
```python
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 题解》预定”字样。。