剑指offer小解析——14_I_剪绳子

剑指offer小解析——14_I_剪绳子

1.题目描述

剑指 Offer 14- I. 剪绳子

难度中等112

给你一根长度为 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:

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

示例 2:

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

提示:

  • 2 <= n <= 58

2.动态规划

2.1 正常人的思维

//time: 38.74%
//memory: 77.31%
public static int cuttingRope(int n) {
   
    if (n < 2) {
   
        return 0;
    }
    if (n == 2) {
   
        return 1;
    }
    int[] res = new int[n + 1];
    res[1] = 0;
    res[2] = 1;
    int max = 0;
    for (int i = 3; i <= n; i++) {
   
        for (int j = 1; j <= i / 2; j++) {
   
            int temp = (Math.max(j, res[i]) * Math.max((i - j), res[i - j]));
            if (temp > max) {
   
                max = temp;
            }
        }
        res[i] = max;
    }
    return res[n];
}

​ 这里我们可以想一想曾经做过的斐波那契数列,对于本题来说,我们提供两个初始值,绳长为1(或者为0)时我们返回0,因为1已经是最小单位了,剪不了了;绳长为2时我们返回1,即剪成1米和1米的两段。

​ 如果绳长大于2呢?我们可以设置一个数组来存储每个长度对应的最优结果。我们设这个数组为res,res的下标代表绳子长度,即res[1]代表绳长为1米,res[2]代表绳长为2米……,那么我们初始化res[1] = 0, res[2] = 1。

​ 做完初始化工作后,我们就要开始利用已有的结果计算未知的结果。我们首先写一个循环,从3开始,到绳长为n时结束,这个循环代表了绳子的长度。比如i = 4,说明我们正在计算绳长为4米时候的最优解。

​ 然后我们进行剪绳,本来我们可以剪的长度是从1米到i - 1米,但举个例子来说,2 + 4 = 6,可4 + 2也等于6,这样就重复了,我们只需要剪到中间就行,不需要再往右了。

​ 如果绳长为3米,我们的最优剪法应该是剪成1米和2米,结果为2,我们想当然地使用res[j] * res[i - j] (j就是你剪断的长度,i -j就是剩下的长度)= res[2] * res[1] = ……欸,等等,res[2] = 1,res[1] = 0,这俩乘积是0啊!

​ 这就需要你仔细想一想,无论是res[2]还是res[1],都是他们作为题目输入的长度时,必须剪绳子获得的最优解,但是如果他们作为计算的中间量的时候,就比如我们输入3,剪断成2和1,你还必须剪那个2米的绳子吗?不需要,因为他是中间量,我们没必要非得剪断他,很明显,2米绳子的本身长度大于他作为输入时的最优解,所以我们选择使用Math.max方法来获得2米绳子的长度及其最优解之间的最大值即可。

​ 然后就简单了,我们定义一个max量,记录乘积的最大值。每次内循环结束这个max值归零或者不归零都行,反正他都是递增的,每次循环都一定会产生一个值把他覆盖掉,无所谓。

​ 但是这个解法,耗时,时间上只能达到38%,还不够好,问题出在哪里呢?

2.2 很多人都会写但没多少人证明过的一种方式

public int cuttingRope(int n) {
   
    if (n < 2) {
   
        return 0;
    }

    if (n == 2) {
   
        return 1;
    }

    if (n == 3) {
   
        return 2;
    }

    int[] res = new int[n + 1];
    res[1] = 0;
    res[2] = 2;
    res[3] = 3;
    int max = 0;
    for (int i = 4; i <= n; i++) {
   
        for (int j = 1; j <= i / 2; j++) {
   
            int temp = res[j] * res[i - j];
            if (temp > max) {
   
                max = temp;
            }
        }
        res[i] = max;
    }
    return res[n];
}

​ 我们先带着上面的疑问,看完上面的代码,觉得这个跟之前的思路也差不多,稍微看看好像能看懂,但是,又总觉得哪里不对劲?

​ 为什么是4,我的循环为什么要从4开始?不是从5或者6或者100?res[2]是啥?res数组不应该记录的最优解吗?如果res[2]代表的是2米长的绳子的最优解,不应该是1吗?咋res[1]、res[2]、res[3]的值都等于他们自身的长度了?

​ 这个问题的核心原因,也是2.1小节耗时过多的原因——判断一段绳子的最优解是否比它的本身长?

​ 换个说法,比如我有一个3米长的绳子,如果他作为题目本身,让你剪一个3米长的绳子,你知道最优解是1* 2 = 2;但是如果我的绳子长4米呢?当你把绳子剪成3米和1米的时候,你还会继续剪那个3米长的绳子吗?你不会,因为如果继续剪3米的那段,你就把3 * 1 = 3变成了1 * 2 * 1 = 2,反而减小了。所以如果剪断的过程中出现3米的子绳段,我们选择不剪,直接保留,因为他的最优解小于他本身的长度。这也是为什么我们在2.1小节,每次都要使用Math.max方法。

​ 但是,我们真的每次都需要Math.max吗?比如4,我们知道他的最优解是剪成2 、 2的两段,结果是4,等于绳长;5可以分成2和3,结果是6,比5还大;6可以分成2和4,结果是8;7可以分成3和4,结果是12……wait a minute,好像,从4开始,他们的最优解都大于或者等于绳子的长度。那么我们是否可以这样,如果输入的n是0到3,我们直接返回最优解;但是如果n大于3,那么我们设作为子段的0、1、2、3的**“最优解”**分别都是他们自身的长度。然后每次循环计算的时候我们就可以直接使用int temp = res[j] * res[i - j];了,这样岂不妙哉?

​ But,你这么做的前提是啥?前提是知道上面的猜想从4开始,他们的最优解都大于或者等于绳子的长度是正确的,接下来就是证明。

(警告🔥,以下涉及数学知识,而且我高数已经快忘完了,写的跟喝醉了似的,你看不懂不要怀疑自己,绝对是我写的不够清楚明白)

​ 首先我们假设一个数字n( n ∈ Z + n \in Z_{}^{+} nZ+),并且设他的一个加数为x,我们要找到一个最小值,从这个值开始,所有的数一定存在一组加数,使得这两个加数的成绩大于等于这个数本身。用符号描述就是
∀ m ∈ Z + , 如 果 m ≥ n ( n ∈ Z + ) , 则 ∃ x < m , 使 得 ( m − x ) × x ≥ m 。 求 n 。 \forall m \in Z_{}^{+}, 如果m \geq n(n \in Z_{}^+),则\exists x < m,使得(m - x)\times x \geq m。求n。 mZ+,mn(nZ+),x<m,使(mx)×xmn
​ 那么我们需要利用 ( n − x ) × x ≥ n (n - x)\times x \geq n (nx)×xn来求得n的最小值(很明显x一定是大于1的,也就是x≥2,因为如果x=1,结果只能是n-1,不可能等于或者大于n的)

​ 通过化简,我们获得了 x 2 x − 1 ≤ n ( x ≥ 2 ) \frac{x^{2}}{x - 1} \leq n (x \geq 2) x1x2n(x2) ,我们现在只需要求得 x 2 x − 1 \frac{x^{2}}{x - 1} x1x2 的最小值。

​ 对 x 2 x − 1 \frac{x^{2}}{x - 1} x1x2 进行变形,获得 ( x − 1 ) 2 + 2 x − 1 x − 1 \frac{(x - 1)^2 + 2x - 1}{x - 1} x1(x1)2+2x1 ,即 x − 1 + 2 x − 1 x − 1 x - 1 + \frac{2x - 1}{x - 1} x1+x12x1

​ 继续变形化简,获得 x + 1 + 1 x − 1 x + 1 + \frac{1}{x - 1} x+1+x11

​ 求导获得 1 − 1 ( x − 1 ) 2 = x ( x − 2 ) ( x − 1 ) 2 ( x ≥ 2 ) 1 - \frac{1}{(x - 1)^2} = \frac{x(x-2)}{(x-1)^2}(x\geq 2) 1(x1)21=(x1)2x(x2)(x2)

​ 很明显,在x≥2的区间范围内, x 2 x − 1 \frac{x^{2}}{x - 1} x1x2 的导数始终大于等于0,即它始终是增函数,最小值即为x=2时。我们带入得到4,也就是说,从4开始,n一定存在两个加数的乘积大于等于n。那么放到我们的题目就是从,每个子段,只要他的长度大于4,他的最优解一定大于他自身的长度。

​ 上面如果我讲的还不够清楚,没关系,你只需要知道我们的思路应该是这样

  • 如果绳子的长度为0、1、2、3,你就直接返回对应的最优解即可,即0、0、1、2;
  • 如果绳子的长度大于等于4,我们定义res数组,注意,res的下标就是绳子的长度。那么现在res[1],res[2],res[3]代表的是长度为1、2、3的**子段(注意,他们是作为子段,就是过程中被剪出来的绳子)**能提供的最大长度,我们分别设为他们各自的长度(通过上面的证明我们已经知道,只有大于4的线段才会出现加数乘积大于本身的情况,所以1、2、3这些段如果是作为中间量出现的话,就没必要继续剪下去了)。

别的我就不多说了,只要理解了上面的,这个代码本身没有难度,10分钟就能完整地背下来。

3.贪婪算法

在说这个算法之前,我们可以看看如果我们输入n = 10,利用上面的动态规划算***怎么运行

运行到10,可以分成2和8,8又可以分成2和6,6又可以分成3和3,最后的结果也就是2 * 2 * 3 * 3 = 36

我们发现了,在剪绳子宇宙的尽头是铁岭……不对,

我们发现,宇宙的尽头是2和3,你无论是什么数我最后都能给你分成2和3,而且当n ≥5的时候,你证明一下,

3(n - 3) ≥ 2(n - 2)

这说明当n ≥5的时候,应该尽量剪出来3米的绳子。而当长度为4的时候,2 * 2 > 1 * 3,这时候我们就要收手,要剪出来两段2米的。

那么我们用n / 3 获得可以分成的3米的数量numberOf3,这时候余数可能为1或者2,如果余数是1,我们就让numberOf3-1,拿出一个3米的子段跟1米合并,成为1个4米的子段,这样效果更好。

如果余数是2,那就让它2着,不用管

然后我们通过(n - numberOf3 * 3) / 2来获得2米子段是数量numberOf2,最后利用Math.pow(3, numberOf3)*Math.pow(2, numberOf2)来获得结果。

public int cuttingRope(int n) {
   
    if (n < 2) {
   
        return 0;
    }
    if (n == 2) {
   
        return 1;
    }

    if (n == 3) {
   
        return 2;
    }

    int numberOf3 = n / 3;
    if (n - 3 * numberOf3 == 1) {
   
        numberOf3--;
    }
    int numberOf2 = (n - numberOf3 * 3) / 2;
    return (int)(Math.pow(3, numberOf3) * Math.pow(2, numberOf2));
}

4.最后叨逼两句

​ 说实话,什么动态规划,贪婪,我还真不知道怎么就动态了,怎么就贪婪了,之前做题的时候看到一个老哥的话,不要死记模板,见到一个题,知道大致的方向,见招拆招即可,没必要非得分那么细,这个是干嘛的,那个得用哪个套路,反正我不会(主要还是刷的少,估计以后刷多了也就很自然地写出来了)。

全部评论

相关推荐

04-17 23:48
西北大学 Java
陈好好wy:加油加油 字节和心脏谁先跳动
字节跳动开奖383人在聊
点赞 评论 收藏
分享
xdm&nbsp;早上喝奶茶差点喷出来。事情是这样的,我们班有个哥们儿,简称&nbsp;L,去年秋招拿了字节sp,专业方向是后端。我们当时都震惊:这哥们儿平时课上从来不发言,期末小组作业基本是划水的那种,刷题平台&nbsp;commit记录我点进去看过,绿格子稀稀拉拉。但他面试一路绿灯。一面二面三面&nbsp;hr&nbsp;面,全过,给的还是sp。当时班级群里恭喜他的、问他经验的、约饭的,热闹了一周。他说自己"运气好,准备充分"。我们都信了,直到三月初他入职。入职第二周开始,班里另一个进字节的同学W(在隔壁组的)开始跟我他的不对劲。一开始是写代码慢,后来写不出来,再后来是组里&nbsp;mentor&nbsp;让他fix&nbsp;一个简单&nbsp;bug&nbsp;都搞了一下午没动静。最离谱的是上周。W&nbsp;说他们大部门搞了个新人分享会,让新人讲一下自己负责模块的设计思路。L&nbsp;上去讲了&nbsp;20分钟,全程念稿子,问答环节别人随便问一个"那你这里为什么用&nbsp;Redis&nbsp;不用&nbsp;Memcached",他直接卡&nbsp;30秒说"这个我回去再确认一下"。会后他&nbsp;mentor&nbsp;直接找&nbsp;leader&nbsp;谈,leader&nbsp;找&nbsp;hr&nbsp;谈,hr调出了他面试录像,全程对比口型和回答节奏,发现他二三面有大量时长在偷偷看屏幕外(推测开了双机位&nbsp;AI&nbsp;答题)。(这段是&nbsp;W后来转述给我的,他自己也是听他组里同事八卦来的)昨天下班前,W&nbsp;告诉我L&nbsp;被辞退了,让他自己走,不走就走仲裁但会发函到学校。L&nbsp;现在已经回学校了,朋友圈仅三天可见。我说真的,我不是个心眼小的人,但是我看到这个消息的时候真的有种"嗯,挺好"的感觉。去年秋招我投字节后端,简历挂。我准备了八个月,背&nbsp;八股&nbsp;+&nbsp;刷&nbsp;500&nbsp;题&nbsp;+项目改了三版,连面试机会都没拿到。班里这哥们儿凭着一个外挂上岸,最后还是被甩出来了。不是说作弊就一定会被发现,但是当面试拿到的&nbsp;offer远远超出真实能力的时候,迟早会有这一天。试用期三个月不是给你过家家的,是真的要写代码、要在会议上回答问题、要扛需求的。我现在反而有点同情他。同情他相信"上岸就是终点"。发出来不是为了嘲笑谁,就是想说给那些正在被身边作弊上岸的同学搞得很&nbsp;emo&nbsp;的&nbsp;uu&nbsp;们听——别急,回旋镖很长,但它一定会回来。你继续刷你的题,写你的项目,背你的八股。该是你的迟早是你的,不是你的早晚还得还回去。xdm&nbsp;共勉。
牛客12588360...:我不想评论面试方式,作弊是绝对不对的,但是你八股加刷题也不过是个做题小子,他穿帮纯粹是他菜,你也没有高明到哪里去
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务