LeetCode 剑指 Offer 14- II 剪绳子 II
题目
解题
题目 | 解题 |
---|---|
剑指 Offer 14- I. 剪绳子 、343. 整数拆分 | LeetCode343 整数拆分 & 剑指Offer 14- I 剪绳子 |
剑指 Offer 14- II. 剪绳子 II | LeetCode 剑指 Offer 14- II 剪绳子 II |
这道题 n 的最大取值不再是 58 而是 1000,结果也要去模,因而不能再用动态规划的方法来求解:
- 计算结果太大,需要取模。但动态规划的当前状态计算要基于之前的状态 => 状态转移方程: d p [ i ] = max j = 1 i − 1 ( m a x ( j × ( i − j ) , j × d p [ i − j ] ) ) dp[i]=\textstyle\max_{j=1}^{i - 1}(max(j×(i−j),j×dp[i−j])) dp[i]=maxj=1i−1(max(j×(i−j),j×dp[i−j]))。算法需要枚举多个状态选最大的结果,而取模后的数值大小不能反映真实的大小,所以很多测试用例得不到正确答案(部分语言用支持更大数字的数据类型也许能通过,比如 JS 的BigInt);
- 未优化动态规划的时间复杂度为 O ( n 2 ) O(n^2) O(n2),优化后的是 O ( n ) O(n) O(n),相比较而言,数学 / 贪心算法能够得到更低的时间复杂度。
动态规划 + BigInt 解答如下,因为 Math.max 函数不能处理 BigInt 类型,所以得自己用 reduce写一个 max 函数用来比较大小,总而言之不建议使用动态规划,但也是个解法,有比没有强:
// javascript
var cuttingRope = function(n) {
const dp = new Array(n + 1).fill(0n);
for (let i = 2; i <= n; ++i) {
for (let j = 1; j < i; ++j) {
dp[i] = max(dp[i], BigInt(j) * BigInt(i - j), BigInt(j) * dp[i - j]);
}
}
return dp[n] % BigInt(1e9 + 7);
};
const max = (...args) => args.reduce((prev, curr) => prev > curr ? prev : curr);
解题思路参考:面试题14- II. 剪绳子 II(数学推导 / 贪心思想 + 快速幂求余,清晰图解)
解题一:数学/贪心算法 + 循环求余
// javascript
var cuttingRope = function(n) {
const MOD = 1e9 + 7;
if (n <= 3) return n - 1;
const quotient = Math.floor(n / 3);
const remainder = n % 3;
let res = 1;
// 计算 3^{quotient - 1} % MOD
for (let i = 0; i < quotient - 1; ++i) {
res = (res * 3) % MOD;
}
if (remainder === 0) return (3 * res) % MOD; // 3^{quotient} % MOD 要拆一个 3 出来
else if (remainder === 1) return (4 * res) % MOD; // [(3^{quotient - 1}) * 4] % MOD
else return (6 * res) % MOD; // [(3^{quotient}) * 2] % MOD 要拆一个 3 出来
};
解题二:数学/贪心算法 + 快速幂求余
本质是使用快速幂的思想求 x 的正整数次方:LeetCode50 Pow(x, n) & 剑指Offer 16 数值的整数次方,只是在更新 x 和 res 时多了取余的操作。
a | res | x | x * x | |
---|---|---|---|---|
初始值 | 69 | 1 | 3 | 9 |
第1次循环结束 | 34 | 3 | 9 | 81 |
第2 次循环结束 | 17 | 3 | 81 | 6561 |
第3次循环结束 | 8 | 243 | 6561 | 43046721 |
第4次循环结束 | 4 | 243 | 43046721 | 1,853,020,188,851,841(16位) |
第5次循环结束 | 2 | 243 | 175880701 | 30,934,020,984,251,401(17位:下一次循环会开始丧失精度) |
第6次循环结束 | 1 | 243 | 767713261 | 589,383,651,115,254,121(18位) |
第7次循环结束 | 0 | 554321121 | 989568599 | … |
因为每次循环 x = (x * x) % (1e9 + 7)
,x 虽然取余(小于 1e9 + 7),但是 x * x
的最大值可能值:
(
1
e
9
+
6
)
2
(1e9 + 6)^2
(1e9+6)2 是 19 位数,JS 中能精准表示的最大整数是 Math.pow(2, 53),十进制即 9,007,199,254,740,992(16位数),所以在计算 x * x
时有可能会失去精度,随后取余也必然不准确,因而要 使用 BigInt 类型。循环求余没有问题是因为 res * 3
的最大可能值
(
1
e
9
+
6
)
∗
3
(1e9 + 6) * 3
(1e9+6)∗3 必然不会超过 Math.pow(2, 53)。
参考:讲一讲JS 能表示的最大数值及JS Number类型数字位数及IEEE754标准
// javascript
var cuttingRope = function(n) {
const MOD = BigInt(1e9 + 7);
if (n <= 3) return n - 1;
const quotient = Math.floor(n / 3);
const remainder = n % 3;
// 计算 3^{quotient - 1} % MOD
let res = 1n;
let a = quotient - 1, x = 3n;
while (a > 0) {
if ((a & 1) === 1) {
res = (res * x) % MOD;
}
x = (x * x) % MOD;
a >>>= 1;
}
if (remainder === 0) return (3n * res) % MOD; // 3^{quotient} % MOD 要拆一个 3 出来
else if (remainder === 1) return (4n * res) % MOD; // [(3^{quotient - 1}) * 4] % MOD
else return (6n * res) % MOD; // [(3^{quotient}) * 2] % MOD 要拆一个 3 出来
};