343. 整数拆分 (integer-break)

3,926 阅读4分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情

343. 整数拆分 题目描述:给定一个 正整数 nn,将其拆分为 kk正整数 的和(k2 k \ge 2),并使这些整数的乘积最大化。求这个最大乘积。(2n582 \le n \le 58

示例1示例2
输入: 22
输出: 11
解释: 2=1+1,1×1=12 = 1 + 1, 1 × 1 = 1
输入: 1010
输出: 3636
解释: 10=3+3+4,3× 3× 4=3610 = 3 + 3 + 4, 3 × 3 × 4 = 36

中规中矩的动态规划

题意很符合动态规划题目的必要条件,求最值可枚举无后效性,但是否存在最优子结构?我们试猜想,记 TnT_n 为整数 nn 拆分 kk 个正整数后,这些正整数的最大乘积。如果知道 Tn1T_{n-1} 的值,能否得到 TnT_n 的值?貌似并不能从 Tn1T_{n-1} 转移到 TnT_n

那换个思路,对于正整数 nn,当 n2n \ge 2 时,可以拆分成至少两个正整数的和。若 mmnn 拆分出的第一个正整数,则剩下的部分是 nmn - m,这部分有两种情况

  1. 不能拆分,那么乘积就是 Tn=m×(nm)T_n = m \times (n - m)
  2. 继续拆分,那么乘积就是 Tn=m×TnmT_n = m \times T_{n-m}

1、确定 dp 状态数组

dp[i]dp[i] 表示正整数 ii 拆分成至少两个正整数后,,这些正整数的最大乘积。

2、确定 dp 状态转移方程

设正整数 ii 拆分出来的第一个正整数为 jj(其中,1j<i1 \le j <i),则有以下两种情况:

  • 正整数 ii 拆分成 jjiji-j,而且 iji-j 不能再继续拆分成多个正整数,此时最大的乘积为 j×(ij)j \times (i-j)

  • 正整数 ii 拆分成 jjiji-j,而且 iji-j 可以再继续拆分成多个正整数,此时最大的乘积为 j×dp[ij]j \times dp[i-j]

那么状态转移方程为,dp[i]=max(dp[i],j×(ij),j×dp[ij])dp[i] = max(dp[i], j \times (i-j), j \times dp[i -j])

3、确定 dp 初始状态

i=0i = 0i=ni = ndp[i]=0dp[i] = 0

  • 0011 都不能正常拆分,保证 dpdp 数组连续,故 dp[0]=dp[1]=0dp[0]=dp[1]=0

  • 22 开始都可以正常拆分,而且 22 只能拆分成 1+11+1,故 dp[2]=1dp[2] = 1

4、确定遍历顺序

  • 第一层循环从 i=3i =3i=ni = n

  • 第二层循环从 j=1j=1j=i1j = i -1

5、确定最终返回值

重温 dpdp 数组定义:正整数 nn,将其拆分为 kk 个 正整数的和(k2 k \ge 2),并使这些整数的乘积最大化,即为 dp[n]dp[n]

6、代码示例

/**
 * 时间复杂度 O(n^2)
 * 空间复杂度 O(n)
 */
function integerBreak(n: number): number {
    const dp = new Array(n + 1).fill(0);
    dp[2] = 1;

    for(let i = 3; i <= n; i++) {
        for(let j = 1; j < i; j++) {
            dp[i] = Math.max(dp[i], j * (i - j), j * dp[i - j]);
        }
    }

    return dp[n];
};

思维升级-不等式

求证最大值不等式

在高中阶段我们接触过这样的不等式: (n1+n2+...+nn)an1n2...nna{\frac {(n_1+n_2+...+n_n)} {a}} \ge \sqrt[a]{n_1n_2...n_n} ,当且仅当 n1=n2=...=nnn_1 = n_2 = ... = n_n 时,乘积最大值。

设将数字 nn 以因子 xx 等分为 aa 个,即 n=axn=ax,则乘积为 xa=xnx=(x1x)nx^a = x^{\frac{n}{x}} = (x^{\frac{1}{x}})^nnn 是常数,所以当 x1xx^{\frac{1}{x}} 取最大值时,乘积取最大值。

y=x1xy=x^{\frac{1}{x}},公式两边取对数,得到 lny=1xlnxlny ={\frac {1} {x}} lnx,对 xx 求导,得到 1yy=1x2lnx+1x2{\frac {1} {y}} y^{'} = -{\frac {1} {x^2}} lnx + {\frac {1} {x^2}},整理得到

y=(1lnx)×(x1x2)y^{'} = (1 - lnx) \times ({x^{{\frac {1} {x}}-2}})

y=0y^{'} = 0x1x2{x^{{\frac {1} {x}}-2}} 恒大于 00,所以只能当 1lnx1 - lnx00 时,yy^{'} 才能为 00,得到极值点为 x0=ex_0 = e。当 x<ex < e 时,y>0y^{'} > 0 单调递增,当 x>ex > e 时,y<0y^{'} < 0 单调递减,所以 x0=ex_0 = e 是极大值点。由于 xx 要取正整数,所以 x=23x=2 、3 符合题意,代入方程得到 y(3)>y(2)y_{(3)}>y_{(2)},所以再拆分整数时,尽可能多拆一些 33,才能使得最终乘积最大。

n3n \le 3 时,n=2n=2 返回 1×1=11 \times 1 = 1n=3n=3 返回 1×2=21 \times 2 = 2

n>3n>3 时,n=3a+bn=3a+b,并分为以下三种情况:

  • b=0b=0 时,直接返回 3a3^a

  • b=1b=1 时,要将一个 33 单独拿出来,和这个 11 组成 4=2+24 = 2+2,所以返回值为 3a1×43^{a-1} \times 4

  • b=2b=2 时,直接返回 3a×23^a \times 2

代码示例

/**
 * 时间复杂度 O(1),看js引擎如何实现幂运算
 * 空间复杂度 O(1)
 */
function integerBreak(n: number): number {
    if (n <= 3) {
        return n - 1; // n=2返回1,n=3返回2
    }

    const a = ~~(n / 3);
    const b = n % 3;

    if (b === 0 ) {
        return 3 ** a;
    }

    if (b === 1) {
        return (3 ** (a - 1)) * 4;
    }

    return (3 ** a) * 2;
};

回到动态规划

方才我们已经证明了,当x=23x=2 、3时才会让乘积更大,所以我们在拆分整数时应该有尽可能多的 22 或者 33 的参与,因此我们改造一下“中规中矩动态规划”算法中的 dpdp 状态转移方程,降低算法的时间复杂度。

在第二层循环,完全没有必要从 j=1j=1 一直遍历到 j=i1j=i-1,我们只需要判断 2×(i2)2 \times (i - 2)2×dp[i2]2 \times dp[i - 2]3×(i3)3 \times (i - 3)3×dp[i3]3 \times dp[i - 3],即

dp[i]=max(2×(i2),2×dp[i2],3×(i3),3×dp[i3])dp[i] = max(2 \times (i - 2),2 \times dp[i - 2],3 \times (i - 3),3 \times dp[i - 3])

代码示例

/**
 * 时间复杂度 O(n)
 * 空间复杂度 O(n)
 */
function integerBreak(n: number): number {
    const dp = new Array(n + 1).fill(0);
    dp[2] = 1;
    dp[3] = 2;

    for(let i = 4; i <= n; i++) {
        dp[i] = Math.max(2 * (i - 2), 2 * dp[i - 2], 3 * (i - 3), 3 * dp[i - 3]);
    }

    return dp[n];
};

参考

# 重识动态规划