牛客题解 | 剪绳子

题目## 题目

题目链接

题目的主要信息:
  • 把一根长度为 n n n的绳子分成 m m m段,每段长度都是整数
  • 求每段长度乘积的最大值
举一反三:

学习完本题的思路你可以解决如下题目:

JZ83. 剪绳子(进阶版)

JZ71. 跳台阶扩展问题

JZ42. 连续子数组的最大和

方法一:动态规划(推荐使用)

知识点:动态规划

动态规划算法的基本思想是:将待求解的问题分解成若干个相互联系的子问题,先求解子问题,然后从这些子问题的解得到原问题的解;对于重复出现的子问题,只在第一次遇到的时候对它进行求解,并把答案保存起来,让以后再次遇到时直接引用答案,不必重新求解。动态规划算法将问题的解决方案视为一系列决策的结果。

思路:

一旦分出一段长度为1的小段,只会减少总长度,还不能增加乘积,因此长度为2的绳子不分比分开的乘积大,长度为3的绳子不分比分开的乘积大,长度为4的绳子分成2*2比较大。前面的我们都可以通过这样递推得到,后面的呢?

同样递推!如果我有一个长度为 n n n的绳子,我们要怎么确定其分出最大的乘积,我们可以尝试其中一段不可分的为 j j j,那么如果另一段 n − j n-j nj最大乘积已知,我们可以遍历所有 j j j找到这个最大乘积。因此用 d p [ i ] dp[i] dp[i]表示长度为i的绳子可以被剪出来的最大乘积,那么后续遍历每个 j j j的时候,我们取最大 d p [ i ] = m a x ( d p [ i ] , j ∗ d p [ i − j ] ) dp[i] = max(dp[i], j * dp[i - j]) dp[i]=max(dp[i],jdp[ij])就好了。

//可以被分成两份
for(int j = 1; j < i; j++)
    //取最大值
    dp[i] = max(dp[i], j * dp[i - j]);

具体做法:

  • step 1:检查当number不超过3的时候直接计算。
  • step 2:用dp数组表示长度为i的绳子可以被剪出来的最大乘积,初始化前面4个容易推断的。
  • step 3:遍历每个长度,对于每个长度的最大乘积,可以遍历从1到 i i i的每个固定一段,按照上述公式求的最大值。
  • step 4:最后数组最后一位就是答案。

图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Java实现代码:

import java.util.*;
public class Solution {
    public int cutRope(int target) {
        //不超过3直接计算
        if(target <= 3) 
            return target- 1;
        //dp[i]表示长度为i的绳子可以被剪出来的最大乘积
        int[] dp = new int[target + 1];
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        dp[4] = 4;
        //遍历后续每一个长度
        for(int i = 5; i <= target; i++)
            //可以被分成两份
            for(int j = 1; j < i; j++)
                //取最大值
                dp[i] = Math.max(dp[i], j * dp[i - j]);
        return dp[target];
    }
}

C++实现代码:

class Solution {
public:
    int cutRope(int number) {
        //不超过3直接计算
        if(number <= 3) 
            return number - 1;
        //dp[i]表示长度为i的绳子可以被剪出来的最大乘积
        vector<int> dp(number + 1, 0);
        dp[1] = 1;
        dp[2] = 2;
        dp[3] = 3;
        dp[4] = 4;
        //遍历后续每一个长度
        for(int i = 5; i <= number; i++)
            //可以被分成两份
            for(int j = 1; j < i; j++)
                //取最大值
                dp[i] = max(dp[i], j * dp[i - j]);
        return dp[number];
    }
};

Python实现代码:

class Solution:
    def cutRope(self , number: int) -> int:
        #不超过3直接计算
        if number <= 3: 
            return number - 1
        #dp[i]表示长度为i的绳子可以被剪出来的最大乘积
        dp = [0 for i in range(number + 1)]
        dp[1] = 1
        dp[2] = 2
        dp[3] = 3
        dp[4] = 4
        #遍历后续每一个长度
        for i in range(5, number + 1):
            #可以被分成各种长度的两份
            for j in range(1, i):
                #取最大值
                dp[i] = max(dp[i], j * dp[i - j])
        return dp[number]

复杂度分析:

  • 时间复杂度: O ( n 2 ) O(n^2) O(n2),两层遍历
  • 空间复杂度: O ( n ) O(n) O(n),辅助数组dp的空间
方法二:贪心(扩展思路)

知识点:贪心

贪心思想属于动态规划思想中的一种,其基本原理是找出整体当中给的每个局部子结构的最优解,并且最终将所有的这些局部最优解结合起来形成整体上的一个最优解。

思路:

根据均值不等式,有: n 1 + n 2 + . . . + n m m ≥ n 1 n 2 . . . n m m \frac{n_1+n_2+...+n_m}{m}\geq \sqrt[m]{n_1n_2...n_m} mn1+n2+...+nmmn1n2...nm ,等号当且仅当 n 1 = n 2 = n 3 = . . . n m n_1=n_2=n_3=...n_m n1=n2=n3=...nm时成立,因为加法部分和是固定的绳子总长度,因此要使乘积最大,应该以相等的长度等分成多段。

如果将绳子按照每段长度为 x x x等分成 m m m段,则 n = m x n=mx n=mx,乘积为 x m x^m xm,因为有 x m = x n x = ( x 1 x ) n x^m=x^{\frac{n}{x}}=(x^{\frac{1}{x}})^n xm=xxn=(xx1)n因此当 x 1 x x^{\frac{1}{x}} xx1取最大值时取最大值。

y = x 1 x y=x^{\frac{1}{x}} y=xx1,即求这个函数的极值即可直到绳子等分成长度为多少可以使乘积最大。根据取对数、求导、求极值等一系列数学操作,得驻点为 x 0 = e x_0=e x0=e,即极大值需要将绳子分成每段e,但是绳子长度只能是整数,靠近e的只有2
和3,二者分别代入公式,发现当 x = 3 x=3 x=3是,乘积达到最大。

因此后续,使用贪心思想,不断将绳子分成每段长度为3即可,不足3的可以考虑,如果最后剩余的是2,直接乘上,如果最后剩余的是1,则取出一个3组成4分成长度为2的两段,因为 2 ∗ 2 > 1 ∗ 3 2*2>1*3 22>13

具体做法:

  • step 1:按照上述思路,不超过3的直接计算
  • step 2:超过3的不断累乘3,然后number不断减去3,直到最后不超过4。
  • step 3:最后乘上剩余的数字。

Java实现代码:

public class Solution {
    public int cutRope(int target) {
        //不超过3直接计算
        if(target <= 3) 
            return target - 1;
        int res = 1;
        while(target > 4){
            //连续乘3
            res *= 3; 
            target -= 3; 
        }
        return res * target;   
    }
}

C++实现代码:

class Solution {
public:
    int cutRope(int number) {
        //不超过3直接计算
        if(number <= 3) 
            return number - 1;
        int res = 1;
        while(number > 4){
            //连续乘3
            res *= 3; 
            number -= 3; 
        }
        return res * number;
    }
};

Python实现代码:

class Solution:
    def cutRope(self , number: int) -> int:
        #不超过3直接计算
        if number <= 3: 
            return number - 1
        res = 1
        while number > 4:
            #连续乘3
            res *= 3 
            number -= 3 
        return res * number

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),其中 n n n为绳子的长度,最坏需要计算 n / 3 n/3 n/3
  • 空间复杂度: O ( 1 ) O(1) O(1),常数级变量,无额外辅助空间

题目链接

题目主要信息:
  • 给定一个链表的头节点,判断这个链表是否有环
  • 环形链表如下所示:
    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
举一反三:

学习完本题的思路你可以解决如下题目:

BM4.合并有序链表

BM5.合并k个已排序的链表

BM7.链表中环的入口节点

BM8.链表中倒数最后k个节点

BM9.删除链表的倒数第n个节点

BM10.两个链表的第一个公共节点

BM13.判断一个链表是否为回文结构

BM14.链表的奇偶重排

方法:双指针(推荐使用)

知识点:双指针

双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。

思路:

我们都知道链表不像二叉树,每个节点只有一个val值和一个next指针,也就是说一个节点只能有一个指针指向下一个节点,不能有两个指针,那这时我们就可以说一个性质:环形链表的环一定在末尾,末尾没有NULL了。为什么这样说呢?仔细看上图,在环2,0,-4中,没有任何一个节点可以指针指出环,它们只能在环内不断循环,因此环后面不可能还有一条尾巴。如果是普通线形链表末尾一定有NULL,那我们可以根据链表中是否有NULL判断是不是有环。

但是,环形链表遍历过程中会不断循环,线形链表遍历到NULL结束了,但是环形链表何时能结束呢?我们可以用双指针技巧,同向访问的双指针,速度是快慢的,只要有环,二者就会在环内不断循环,且因为有速度差异,二者一定会相遇。

具体做法:

  • step 1:设置快慢两个指针,初始都指向链表头。
  • step 2:遍历链表,快指针每次走两步,慢指针每次走一步。
  • step 3:如果快指针到了链表末尾,说明没有环,因为它每次走两步,所以要验证连续两步是否为NULL。
  • step 4:如果链表有环,那快慢双指针会在环内循环,因为快指针每次走两步,因此快指针会在环内追到慢指针,二者相遇就代表有环。

图示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

Java实现代码:

public class Solution {
    public boolean hasCycle(ListNode head) {
        //先判断链表为空的情况
        if(head == null) 
            return false;
        //快慢双指针
        ListNode fast = head; 
        ListNode slow = head;
        //如果没环快指针会先到链表尾
        while(fast != null && fast.next != null){ 
            //快指针移动两步
            fast = fast.next.next; 
            //慢指针移动一步
            slow = slow.next; 
            //相遇则有环
            if(fast == slow) 
                return true;
        }
        //到末尾则没有环
        return false; 
    }
}

C++实现代码:

class Solution {
public:
    bool hasCycle(ListNode *head) {
        //先判断链表为空的情况
        if(head == NULL) 
            return false;
        //快慢双指针
        ListNode* fast = head; 
        ListNode* slow = head;
        //如果没环快指针会先到链表尾
        while(fast != NULL && fast->next != NULL){ 
            //快指针移动两步
            fast = fast->next->next; 
            //慢指针移动一步
            slow = slow->next;
            //相遇则有环 
            if(fast == slow) 
                return true;
        }
        //到末尾则没有环
        return false; 
    }
};

Python代码实现:

class Solution:
    def hasCycle(self , head: ListNode) -> bool:
        #先判断链表为空的情况
        if head == None: 
            return False
        #快慢双指针
        fast = head 
        slow = head
        #如果没环快指针会先到链表尾
        while fast != None and fast.next != None: 
            #快指针移动两步
            fast = fast.next.next 
            #慢指针移动一步
            slow = slow.next 
            #相遇则有环
            if fast == slow: 
                return True
        #到末尾则没有环
        return False 

复杂度分析:

  • 时间复杂度: O ( n ) O(n) O(n),最坏情况下遍历链表 n n n个节点
  • 空间复杂度: O ( 1 ) O(1) O(1),仅使用了两个指针,没有额外辅助空间
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值