sword_for_offer/docs/剑指 Offer 14- II. 剪绳子 II.md
此题与 剑指 Offer 14- I. 剪绳子 主体等价,唯一不同在于本题目涉及 大数越界情况下的求余问题 。建议先做上一道题,在此基础上再研究此题目的大数求余方法。
$$ n = n_1 + n_2 + ... + n_a $$
$$ \max(n_1 \times n_2 \times ... \times n_a) $$
以下数学推导总体分为两步:① 当所有绳段长度相等时,乘积最大。② 最优的绳段长度为 $3$ 。
$$ \frac{n_1 + n_2 + ... + n_a}{a} \geq \sqrt[a]{n_1 n_2 ... n_a} $$
推论一: 将绳子 以相等的长度等分为多段 ,得到的乘积最大。
$$ x^a = x^{\frac{n}{x}} = (x^{\frac{1}{x}})^n $$
$$ \begin{aligned} \ln y & = \frac{1}{x} \ln x & \text{取对数} \ \frac{1}{y} \dot {y} & = \frac{1}{x^2} - \frac{1}{x^2} \ln x & \text{对 $x$ 求导} \ & = \frac{1 - \ln x}{x^2} \ \dot {y} & = \frac{1 - \ln x}{x^2} x^{\frac{1}{x}} & \text{整理得} \end{aligned} $$
$$ \dot {y} \begin{cases}
0 & , x \in [- \infty, e) \ < 0 & , x \in (e, \infty] \ \end{cases} $$
$$ y(3) = 3^{1/3} \approx 1.44 \ y(2) = 2^{1/2} \approx 1.41 $$
$$ [y(3)]^6 = (3^{1/3})^6 = 9 \ [y(2)]^6 = (2^{1/2})^6 = 8 $$
推论二: 尽可能将绳子以长度 $3$ 等分为多段时,乘积最大。
{:width=600}
大数越界: 当 $a$ 增大时,最后返回的 $3^a$ 大小以指数级别增长,可能超出 int32 甚至 int64 的取值范围,导致返回值错误。
大数求余问题: 在仅使用 int32 类型存储的前提下,正确计算 $x^a$ 对 $p$ 求余(即 $x^a \odot p$ )的值。
解决方案: 循环求余 、 快速幂求余 ,其中后者的时间复杂度更低,两种方法均基于以下求余运算规则推出:
$$ (xy) \odot p = [(x \odot p)(y \odot p)] \odot p $$
$$ x^a \odot p = [(x ^{a-1} \odot p)(x \odot p)] \odot p=[(x ^{a-1} \odot p)x] \odot p $$
rem 都在 int32 取值范围中。封装方法代码如下所示。# 求 (x^a) % p —— 循环求余法
def remainder(x, a, p):
rem = 1
for _ in range(a):
rem = (rem * x) % p
return rem
$$ x^a \odot p = (x^2)^{a/2} \odot p = (x^2 \odot p)^{a / 2} \odot p $$
$$ {x^a \odot p = } \begin{cases} (x^2 \odot p)^{a // 2} \odot p & \text{, $a$ 为偶数} \ {[(x \odot p)(x ^{a-1} \odot p)] \odot p = [x(x^2 \odot p)^{a//2}] \odot p} & \text{, $a$ 为奇数} \ \end{cases} $$
# 求 (x^a) % p —— 快速幂求余
def remainder(x, a, p):
rem = 1
while a > 0:
if a % 2: rem = (rem * x) % p
x = x ** 2 % p
a //= 2
return rem
| $n$ | $rem \times (x^a \odot p)$ | $rem_n=rem_{n-1} \times x_{n-1} \odot p$ | $x_n=x_{n-1}^2 \odot p$ | $a_n=a_{n-1}//2$ |
|---|---|---|---|---|
| $1$ | $1 \times (3^{19} \odot p)$ | $1$ | $3$ | $19$ |
| $2$ | $3 \times (9^{9} \odot p)$ | $3=1\times3\odot p$ | $9=3^2 \odot p$ | $9=19//2$ |
| $3$ | $27 \times (81^{4} \odot p)$ | $27 = 3 \times 9 \odot p$ | $81=9^2\odot p$ | $4=9//2$ |
| $4$ | $27 \times (6561^{2} \odot p)$ | $27$ | $6561=81^2 \odot p$ | $2=4//2$ |
| $5$ | $27 \times (43046721^{1} \odot p)$ | $27$ | $43046721=6561^2 \odot p$ | $1=2//2$ |
| $6$ | $162261460 \times (175880701^{0} \odot p)$ | $162261460=27 \times 43046721 \odot p$ | $175880701=43046721^2 \odot p$ | $0=1//2$ |
以下为 二分求余法 的复杂度。
a, b, p, x, rem 使用常数大小额外空间。Python 代码: 由于语言特性,理论上 Python 中的变量取值范围由系统内存大小决定(无限大),因此在 Python 中其实不用考虑大数越界问题。
Java/C++ 代码: 根据二分法计算原理,至少要保证变量 x 和 rem 可以正确存储 $1000000007^2$ ,而 $2^{64} > 1000000007^2 > 2^{32}$ ,因此我们选取 long 类型。
class Solution:
def cuttingRope(self, n: int) -> int:
if n <= 3: return n - 1
a, b, p, x, rem = n // 3 - 1, n % 3, 1000000007, 3 , 1
while a > 0:
if a % 2: rem = (rem * x) % p
x = x ** 2 % p
a //= 2
if b == 0: return (rem * 3) % p # = 3^(a+1) % p
if b == 1: return (rem * 4) % p # = 3^a * 4 % p
return (rem * 6) % p # = 3^(a+1) * 2 % p
class Solution {
public int cuttingRope(int n) {
if(n <= 3) return n - 1;
int b = n % 3, p = 1000000007;
long rem = 1, x = 3;
for(int a = n / 3 - 1; a > 0; a /= 2) {
if(a % 2 == 1) rem = (rem * x) % p;
x = (x * x) % p;
}
if(b == 0) return (int)(rem * 3 % p);
if(b == 1) return (int)(rem * 4 % p);
return (int)(rem * 6 % p);
}
}
class Solution {
public:
int cuttingRope(int n) {
if(n <= 3) return n - 1;
int b = n % 3, p = 1000000007;
long rem = 1, x = 3;
for(int a = n / 3 - 1; a > 0; a /= 2) {
if(a % 2 == 1) rem = (rem * x) % p;
x = (x * x) % p;
}
if(b == 0) return (int)(rem * 3 % p);
if(b == 1) return (int)(rem * 4 % p);
return (int)(rem * 6 % p);
}
};
# 由于语言特性,Python 可以不考虑大数越界问题
class Solution:
def cuttingRope(self, n: int) -> int:
if n <= 3: return n - 1
a, b, p = n // 3, n % 3, 1000000007
if b == 0: return 3 ** a % p
if b == 1: return 3 ** (a - 1) * 4 % p
return 3 ** a * 2 % p