剪绳子

scorlw 发布于

剪绳子

关键词:位运算

题目描述

给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]*k[1]*…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。

示例 1:

1
2
3
输入: 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1

示例2:

1
2
3
输入: 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36

思路

思路一:

暴力递归(超出时间限制)

我们往往会在头脑中形成一种很直观的暴力解法,就是列举出所有的情况,找到乘积最大的那个解。

设 F(n)为长度为 n的绳子可以得到的最大乘积,对于每一个 F(n)),可以得到如下分解:

1

从上图看出我们可以把求解 F(n)的问题分解成求解 F(n−1) 的问题,以此类推,直到求解到 F(2)时,F(2)=1,递推回去,问题就得到了解决。这用到的就是分治的思想。

分治思想的解决方法往往是递归,注意到我们每次将一段绳子剪成两段时,剩下的部分可以继续剪,也可以不剪, 因此我们得到了递归函数

F(n)=max(i×(n−i),i×F(n−i)),i=1,2,…,n−2。

复杂度分析

时间复杂度:O(N^2),对于每一个 i调用一次递归,递归的时间复杂度为 O(N),故时间复杂度为 O(N^2)。

空间复杂度:O(N^2)。

思路二:

记忆化技术(自顶向下)

上述暴力解法会超时,但是很多进阶解法往往是暴力解法的优化。注意到上述代码中超时的原因主要是因为重复计算了 F(n),为了避免重复计算可以使用 记忆化(memoization)技术。

记忆化技术的代码中经常需要建立函数 memoize 辅助实现。我们使用数组 f 来保存长度为 i时的最大长度 f[i],最后返回 f[n]即可。

复杂度分析

时间复杂度:O(N^2),原因同上,时间复杂度仍然为 O(N^2),只是采用记忆化减少部分计算时间

空间复杂度:O(N),使用了数组f。

思路三:

动态规划(自底向上):(本地成功,力扣失败,报错超了int 的范围)

同样地,我们也可以使用动态规划,从已知值 F(2)逐步迭代到目标值 F(n),它是一种自底向上的方法。

算法

建立一维动态数组 dp:

​ 边界条件:dp[1] = dp[2] = 1,表示长度为 2 的绳子最大乘积为 1;

​ 状态转移方程:dp[i] = max(dp[i], j * max(i - j, dp[i - j])),可以这样理解:

2

复杂度分析

时间复杂度:O(N^2).

空间复杂度:O(N)。

思路四:

动态规划优化解法

算法

我们发现任何大于 3 的数都可以拆分为数字 1,2,3 的和,且它们对 3 的余数总是 0,1,2,因此我们可以仅用 dp[0],dp[1],dp[2] 表示所有大于 3 的值,这样空间复杂度可降到 O(1)。

3

这样重复使用 dp 数组,只须一趟遍历即可完成,可使时间复杂度降到 O(N)。

复杂度分析

时间复杂度:O(N).

空间复杂度:O(1)。

思路五:

数学方法(在面试时尽量按照常规思路去解)

贪心法则:尽可能分解出多的 3,3 的个数为 a,得到余数 b 可能为 0,1,2:

b = 0,返回 3^a;
b = 1,我们将末尾的 3+1 分解成 2×2,因此返回 3 < 4
b = 2,返回 3^a×2;

代码

思路一:

1
2
3
4
5
6
7
8
9
10
int cuttingRope(int n)
{
if(n == 2) return 1;
int res = -1;
for(int i = 1; i <= n; i++)
{
res = max(res, max(i * cuttingRope(n - i), i * (n - i)));
}
return res;
}

思路二:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Solution
{
public:
int f[59] = {0};//因为n的范围是2-58,数组的开始是0,所以数组的大小可以设为59。
//vector<int> f;//也可以使用vector
//vector<int> f(59,0);//error!报错:应输入类型说明符。因为在.h文件中不能调用vector的构造函数来进行赋值(但是在下面的具体函数实现中可以)

int memoize(int n)//记忆函数,将每次计算结果保存在数组f中
{
//if(n < 2 || n > 58) return 0;//如果上面的数组定义大小为57,下面代码使用f[n - 2]的话,这句需要加上
if (n == 2)
return 1;
if (f[n] != 0)
return f[n];
int res = -1;
for (int i = 1; i <= n; i++)
{
res = max(res, max(i * (n - i), i * memoize(n - i)));
}
f[n] = res;
return res;
}

int cuttingRope(int n)
{
if (n == 2)
return 1;
int res = -1;
for (int i = 1; i <= n; i++)
{
res = memoize(i);
}
return res;
}
};

思路三:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int cuttingRope(int n)
{
if(n < 2 || n > 58) return 0;
if(n == 2) return 1;
int dp[n + 1];//int dp[n + 1] = {0};error!因为变长度的数组不能在声明时初始化。
dp[0] = 1;
dp[1] = 1;
dp[2] = 1;//因为m > 0 所以 n = 2的话一定要剪成1、1.
for(int i = 3;i <= n; i++){
for(int j = 1; j <= i; j++){
dp[i] = max(dp[i], j * max(i - j, dp[i - j]));//dp[i - j]并不包括i - j(因为必须剪)
}
}
return dp[n];
}

思路四:

1
2
3
4
5
6
7
8
9
10
11
12
int cuttingRope(int n)
{
if(n < 2 || n > 58) return 0;
if(n == 2) return 1;
int dp[3] = {0, 1, 1};
for(int i = 3; i <= n; i++){
dp[i % 3] = max(max(max(dp[(i - 1) % 3], i - 1),
2 * max(dp[(i - 2) % 3], i - 2)),
3 * max(dp[(i - 3) % 3], i - 3));
}
return dp[n % 3];
}

思路五:

1
2
3
4
5
6
7
8
9
10
int cuttingRope(int n)
{
if(n < 2 || n > 58) return 0;
if(n < 4) return n - 1;
int a = n / 3;
int b = n % 3;
if(b == 0) return pow(3, a);//pow需要#include <cmath>
else if(b == 1) return pow(3, a - 1) * 4;
else if(b == 2) return pow(3, a) * 2;
}

知识点

  1. max函数
  2. vector的用法
  3. c++数组