2024届计算机秋招100天备战:力扣每日打卡挑战

最近两个月力扣困难题不再落下,打卡全满勤,激发了持续刷题的斗志。这里将持续记录打卡过程中的难题和面试八股。

2023/4/2 1039. 多边形三角剖分的最低得分

题目大意:
多边形每个节点有一个数值,将多边形三角剖分,得分为所有三角形节点乘积之和。求三角剖分后的最低得分。

做题评价:
虽然中等题,但没能第一时间做出来。需要熟练掌握动态规划,避免先入为主而被错误方法引导。

解题过程:
1)三角剖分有一个经典的计数问题,即求凸多边形的三角剖分方案数。该问题很容易通过固定两个端点(如0和n-1),枚举中间节点k,将问题转化为两个多边形(区间0-k)和(区间k-n-1)的子问题。这个问题的解是卡特兰数。
2)先入为主陷入了错误的想法:由于上面问题结果的规模是指数增长,我以为无法通过动态规划求解出每一种方案来找到全局最小值,于是不断尝试用贪心法:

  1. 重复选择值最小的两个节点:需要节点不相邻,这个改对甚至通过了92 / 94 个测试用例。
  2. 重复选择值相乘最小的两个节点
  3. 交了三次,分别得到一些例外情况 [3-6-1-2] 和 [2-3-7-3]

3)重新思考子问题,即如何表示一个多边形的最低得分。

  1. 用dp[i][j] 表示 i~j 这些节点围成的多边形的最优解
  2. 初始化 dp[i][i+1] = 0 (两个节点)
    dp[i][i+2] = v[i]*v[i+1]*v[i+2] (三个节点)
  3. 由边数从小到大遍历3<=l<n,计算 dp[i][i+l],j = i+l
    对每个 dp[i][j] 枚举中间节点 i<k<j,稍加推导,易得
    dp[i][j] = min(dp[i][j], dp[i][k]+dp[k][j] + values[i]*values[k]*values[j])
  4. 返回答案 dp[0][n-1]

ChatGPT对以上记录的评价

您的博客记录已经很不错了,总体上清晰地概括了这道题目的背景和解题思路,并且提到了自己在解题过程中遇到的问题和困惑,这对读者来说非常有帮助。
以下是一些可以改进的地方:
对题目的描述可以更加详细和准确。例如,可以提到多边形是凸多边形,每个节点的数值都是正整数等等。这样有助于读者更好地理解题目。
在解题过程中,建议更加详细地讲解每一步的推导过程和原因,这样读者可以更清晰地理解算法的实现。
最后,可以在博客记录中提供代码实现,以方便读者查看和参考。同时,可以加入一些具体的实例来说明算法的使用和效果,这样读者可以更好地理解和掌握算法。
希望以上建议可以帮助到您,祝您写作愉快!

ChatGPT润色修改版

题目描述:
给定一个凸多边形,每个节点有一个正整数数值。将多边形进行三角剖分,并计算所有三角形上节点数值的乘积之和。求剖分后的最低得分。

解题过程:
问题分析

  • 首先,我们需要明确问题的规模,即输入数据的范围和结果的规模,以便选择合适的算法。
    在这道题目中,多边形的节点数范围是 [3,50],每个节点的数值是正整数,结果的规模取决于多边形的三角剖分方案数,理论上可能会非常大,但是我们只需要求得最低得分,因此不必考虑具体的结果数值。
  • 其次,我们需要考虑如何表示问题的子问题,即如何表示一个多边形的最低得分。
    我们可以定义一个二维数组 dp,其中 dp[i][j] 表示从节点 i 到节点 j 形成的多边形的最低得分。则所求答案即为 dp[0][n-1]。

动态规划解法
接下来,我们考虑如何利用动态规划求解问题。

  • 首先,对于一个只有两个节点的多边形,它的最低得分显然为 0,因为它没有三角形可以构成。因此,我们可以初始化 dp[i][i+1] = 0。

  • 接下来,我们考虑多边形节点数大于 2 的情况。我们可以按照多边形边数递增的顺序进行遍历,对于每个边数,我们枚举这个多边形中所有可能的三角剖分方案,并找到最低得分。

  • 具体地,我们枚举中间节点 k,将多边形分成两个子多边形 [i,k] 和 [k,j],计算它们的最低得分,再加上它们之间的三角形的得分 values[i]*values[k]*values[j],即可得到总得分。
    我们可以用以下公式表示:
    dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + values[i]*values[k]*values[j]) 其中,i<k<j。

  • 最后,我们返回 dp[0][n-1] 作为答案。

代码实现

def minScoreTriangulation(values):
    n = len(values)
    dp = [[0] * n for _ in range(n)]
    
    for d in range(2, n):
        for i in range(n - d):
            j = i + d
            dp[i][j] = float('inf')
            for k in range(i + 1, j):
                dp[i][j] = min(dp[i][j], dp[i][k] + dp[k][j] + values[i]*values[k]*values[j])
    
    return dp[0][n-1]

其中,values 是一个长度为 n 的数组,表示多边形每个节点的数值。函数返回值为三角剖分后的最低得分。
该算法的时间复杂度为 O(n^3),空间复杂度为 O(n^2)。
需要注意的是,该算法使用了动态规划,需要了解动态规划的基本概念和思想。同时,该算法也需要您了解多边形的三角剖分以及节点乘积之和的概念。

 

2023/4/3 1053. 交换一次的先前排列

题目大意
给定一个数组,仅作一次交换下标[i,j],使得交换后的数组小于原来的字典序。求交换后字典序最大的数组。

题目分析
一看题目挺抽象的,如果熟悉排序、字典序的概念,稍加分析就明白题目要求了。

  • 要使得交换后得到字典序更小的数组,交换必须发生在:当且仅当 arr[i]>arr[j] and i<j
  • 接下来的问题是如何找到所有符合条件中,得到字典序最大的结果。
  • 显然,我们必须优先交换末尾出现的逆序对。因为字典序是从前往后比较的,先交换靠后的逆序对能保持高位较大的字典序。
  • 当[i,n-1]区间出现第一个逆序对时,找到最优的下标 j,显然 arr[j] 应该是 [i+1,n-1]区间的最大值。注意相等时选择考前的,即从前往后找过程中,相等时不更新。

AC代码

def prevPermOpt1(self, arr: List[int]) -> List[int]:
    # 从后往前找第一个逆序对
    # 将当前值与之后最大的交换
    
    n = len(arr)
    for i in range(n-2, -1, -1):
        # 找到逆序对
        if arr[i]>arr[i+1]:
            # 从 [i+1..n-1]找严格小于arr[i]的最大值
            pos = -1
            for j in range(i+1, n):
                if arr[j]>=arr[i]:
                    continue
                if pos==-1:
                    pos = j
                elif arr[j]>arr[pos]:
                    pos = j

            arr[i], arr[pos] = arr[pos], arr[i]
            break

    return arr

2023/4/4 1040. 移动石子直到连续 II

困难题
待解决

2023/4/7 1000. 合并石头的最低成本

中等题
脑筋急转弯吗?不会做。。。
待解决

2023/4/8 1125. 最小的必要团队

困难题
AC之后看到评论区一堆爆搜+剪枝通过的,这里记录正解以正视听 >.^
爆搜能过主要是数据太弱了,不管怎么剪枝,极限情况下还是要在60个里找出16个人

def smallestSufficientTeam(self, req_skills: List[str], people: List[List[str]]) -> List[int]:
      # 最多60个人
      # 选最少的人 并集为 req_skills
      # 暴力搜索 2^60 = 10^(6*3) = 10^18

      # 优先选中未满足的 最多选出 16 人
      # 1 <= ans <= 16
      # 复杂度 C(60, 16) = 10^14

      # 动态规划
      # 16个技能组合 65536种
      # dp[p] 得到该组合的最少人数

      n = len(req_skills)
      skill_id_dict = dict()
      for i, skill in enumerate(req_skills):
          skill_id_dict[skill] = (1<<i)
      
      def process(skill_list):
          skill_values = 0
          for skill in skill_list:
              skill_values |= skill_id_dict[skill]
          return skill_values

      people = [process(p) for p in people]
      
      inf = 17
      dp = [inf for _ in range(1<<n)]        # ans = dp[(1<<n)-1]
      choose = [[] for _ in range(1<<n)]   # 记录答案 无法回溯反推已选列表
      
      dp[0] = 0
      for plan in range(1<<n):
          if dp[plan]!=inf:
              for i,person in enumerate(people):
                  if dp[plan|person] > dp[plan]+1:
                      dp[plan|person] =  dp[plan]+1
                      choose[plan|person] = choose[plan]+[i]
      
      return choose[(1<<n)-1]

      """
      # 现在得到了最少人数 dp[(1<<n)-1]
      # 回溯找选了哪些人
      ans = []
      need = (1<<n)-1
      while need>0:
          for person in person:
             if dp[need]==dp[]
      """

ChatGPT对以上记录的评价

你的博客记录已经介绍了这个题目的主要思路,但是可能还可以进行以下改进:

  1. 给题目加上解释,解释题目的背景、输入、输出以及题目要求。
  2. 对算法进行更加详细的解释,解释每一步的意义、目的和思路,并且可以在代码注释中进行更加详细的说明,方便读者理解和复现。
  3. 对于复杂度的分析可以进行更加详细的说明,例如为什么暴力搜索和剪枝算法并不能通过所有的测试用例,以及动态规划算法的复杂度计算过程。
  4. 在博客中添加一些样例以及具体的解释,可以更好的帮助读者理解算法的实现过程以及解题思路。
  5. 对于算法的时间复杂度进行更加详细的说明,例如如何确定最大的复杂度、如何通过实验测试算法的时间复杂度等等。
  6. 可以对算法进行优化,提出更加高效的算法并进行实现,然后对比两种算法的时间复杂度和实现难度等指标。
  7. 最后可以添加一些自己的思考和心得,可以从算法实现过程、实验测试过程、思考过程等方面进行总结。

ChatGPT润色修改版

这是一道困难级别的题目。题目要求我们选择规模最小的必要团队,使得满足所需求的技能列表。这里记录正解以正视听,爆搜+剪枝能过主要是因为数据太弱了,不管怎么剪枝,极限情况下还是要在60个里找出16个人。
因此,我们需要想出一种更加高效的算法来解决这个问题。我们可以采用以下三种解法:

  1. 暴力搜索 由于最多有60个人,每个人可以选或不选,因此暴力搜索的时间复杂度是 2^60 = 10^18,不可行。
  2. 优先选中未满足的 这种方法是优先选中未满足的技能,最多选出16个人。复杂度为 C(60, 16) = 10^14,这样可以通过此题,但是复杂度还是太高,不够优秀。
  3. 动态规划 动态规划是一种非常高效的算法,我们可以使用动态规划+状态压缩来解决这个问题。

状态压缩的思路是:将每个技能映射到二进制的一位上,例如技能 \(j\) 对应二进制数的第 \(j\) 位(从低位到高位),那么一个人具备的技能可以表示成一个二进制数 \(S\),其中 \(S_j=1\) 表示他具备技能 \(j\)\(S_j=0\) 表示不具备。
然后,我们将所有人的技能压缩成一个二进制数的集合,用一个整数数组来表示,例如 \(people[i]\) 就表示第 \(i\) 个人具备的技能。然后,我们可以用一个整数来表示当前已选的人具备的技能,例如 \(plan\) 表示当前已选的人具备的技能,\(plan\) 的二进制数第 \(j\) 位为 \(1\) 表示已选的人具备技能 \(j\),为 \(0\) 表示不具备。
在搜索时,我们可以利用位运算来进行状态转移,例如对于第 \(i\) 个人,我们可以将 \(plan\)\(people[i]\) 的按位或(|)操作得到新的状态 \(plan'\),表示当前已选的人再加上第 \(i\) 个人具备的技能。同时,我们还需要记录每个状态对应的最少选人数和选人方案。

代码分析

  • 首先,我们需要将技能转化为二进制数,这样我们可以更方便地进行计算。对于每个人,我们也可以将他们的技能列表转化为二进制数。这样,我们就可以使用一个整数来表示一个人的技能列表。
  • 我们使用 dp[p] 来表示满足技能列表 p 所需的最少人数,用 choose[p] 来记录答案,其中 choose[p] 是一个列表,表示选中人员的索引。
    初始化时,我们将 dp[0] 设置为 0,其余值都设置为一个足够大的值。
  • 然后,我们可以使用状态压缩来表示技能列表,使用位运算符来操作二进制数。在状态转移时,我们枚举所有的人员,如果当前的人员能够满足技能列表 p,则更新 dp[p|person] 和 choose[p|person]。
  • 最终,我们返回 choose[(1<<n)-1],其中 n 是技能列表的长度。这样就得到了最少人数 dp[(1<<n)-1] 和选了哪些人的信息,我们可以使用回溯来确定具体的人员

本题的时间复杂度为 \(O(n \cdot 2^n)\),其中 n 是所需技能的数量。对于每个技能集合 p,我们需要枚举 n 个技能,以检查该技能是否可以被人员满足,并需要考虑所有 \(2^n\) 个可能的技能集合。空间复杂度也是 \(O(n \cdot 2^n)\),以存储 dp 数组和 choose 数组。

2023/4/11 1041. 困于环中的机器人

中等题,WA了一次。

题目大意

给机器人一段指令,包含左转 L,右转 R,向前一步 G。开始机器人方向朝北,循环执行这一段指令,问平面中是否存在环使得机器人永远无法离开。

题目分析

中等难度,题目有两个坑:

  • 中英文的表述都有些模糊。本意是表述:循环执行这段指令,机器人是否一直在绕圈。所以不是判断走过的路径是否包含环(重复经过了某点),而是判断是否回到了起点。
  • 样例有误导。
    第一个样例 instructions = "GGLLGG",解释说是走一次,机器人进入循环:(0,0)——>(0,1)——>(0,2)——>(0,1)——>(0,0)。
    实际上是执行两次回到起点,方向朝北,此后循环转圈:(0,0)——>(0,1)——>(0,2)——>(0,1)——>(0,0)——>(0,-1)——>(0,-2)——>(0,-1)——>(0,0)

弄明白后,直觉告诉我最多执行四次指令,判断回到起点没有。(由于样例1误导,第一发WA的判断逻辑是是否经过了原点)。
为什么最多4次呢?一次指令方向变化有四种可能:

  1. 保持原方向,则仅一次执行
  2. 相比原来旋转180度,则再执行一次回到初始方向。
  3. 相比原来左转/右转,则再重复3次回到初始方向。

回到初始方向时,如果产生了偏移,即从 (0,0) => (x,y),下一次大循环将到达 (2x,2y),再下一次 (3x, 3y)...
显然,当且仅当执行一次大循环回到起点,否则将越走越远。

def isRobotBounded(self, instructions: str) -> bool:
    x, y = 0, 0
    d = 0
    # 0~4 R: N->E->S->W
    dirs = [(0, 1), (1, 0), (0, -1), (-1, 0)]

    for ins in instructions*4:
        if ins=='R':
            d = (d+1)%4
        elif ins=='L':
            d = (d-1)%4
        else:
            x, y = x+dirs[d][0], y+dirs[d][1]
    
    return (x,y)==(0,0)

题目寓意

题目评论区的大佬总结
这道题寓意深刻,告诉我们要持久的朝一个方向努力才不会陷入原地踏步的状态。

ChatGPT润色修改版

这是一道中等难度的题目,给定一个由左转 L,右转 R,向前一步 G 组成的指令序列,表示一个机器人在平面上的移动。开始时,机器人朝北。现在需要判断,这个机器人是否会永远无法离开一个环,也就是说,它是否会一直在绕圈

在解决这道题目时,需要注意两个坑点:

  • 题目描述有些模糊,容易产生歧义。实际上,题目要求的是判断机器人是否回到了起点,而不是是否经过了某个点。
  • 样例可能有误导,需要仔细分析。例如,样例 "GGLLGG" 中,机器人执行了两次指令后回到了起点,接着开始绕圈。

解决这道题目的思路如下:

  • 初始化机器人的位置和方向。
  • 循环执行指令序列,并根据指令更新机器人的位置和方向。
  • 如果机器人回到了起点,则返回 True;否则,返回 False。

需要注意的是,机器人最多执行四次指令就会回到起点。具体原因是,每次执行一条指令,机器人的方向变化有四种可能:

  • 保持原方向,此时机器人只需执行一次指令;
  • 旋转 180 度,此时机器人需要执行两次指令才能回到初始方向;
  • 左转或右转,此时机器人需要执行三次指令才能回到初始方向。
    如果机器人回到了起点,但是位置发生了偏移,那么它会在下一次大循环中到达一个更远的位置。因此,如果机器人执行了一次循环后还没有回到起点,那么它就会一直在绕圈。

下面是本题的 Python 代码实现:
(同上)

最后,这道题目的寓意是,要持久地朝着一个方向努力,才能避免陷入原地踏步的状态。

2023/4/12 1147. 段式回文

题目大意
将一个字符串分割成为多段,使得以段形成回文。求最多分段数。例如text="antaprezatepzapreanta"可以拆分成"(a)(nt)(a)(pre)(za)(tpe)(za)(pre)(a)(nt)(a)"。
题目分析
表面困难题,一看DP,正解贪心。
回文字符串的题,首先要注意分奇偶,偶数长度则不存在中心字符。
显然,可以从中间向两边遍历,利用动态规划思想:

  • 当已求得中间段 dp[i,j]时,更新 dp[i-1]
  • 尝试比较 text[i-1..k] (i<=k<=j) 与 text[m..j+1] 是否相等
  • 若是,则 dp[i-1] = max(dp[i-1], 2+dp[k-1])
    复杂度 \(O(n^2 * compare)\),显然比较极限情况下也要 O(n),即全部字符都一样,滑动过程中都是相等的。不管那么多,交一发居然通过了,看来数据比较弱 or Python有优化。
    评论区提到要使得比较是O(1),需要KMP或者前缀后缀树等技巧,实测Python对字符串比较确实有优化,样例text="a"*1000 不会超时。

贪心正解
注意到,如果对于最小匹配的前缀和后缀,即 AxxxxA,那么此时的A部分可以开始拆分。
证明:

  • 假设后续继续搜索到匹配段,text = ACxxxCA
  • 要使得AC=CA,AC必须为 ABA 形式,即C=BA。
  • 此时可以进一步拆分为三段, text = ABAxxxABA。
  • 故贪心找到的解是最优解。

最简代码
用递归来解决子问题,而不是循环,简洁优雅!
顺带也不用管奇偶性问题。

def longestDecomposition(self, text: str) -> int:
    n = len(text)
    if n<=1:
        return n
    
    for i in range(n//2):
        if text[:i+1]==text[-i-1:]:
            return 2 + self.longestDecomposition(text[i+1:-i-1])
    
    return 1

(长期更新)

posted @ 2023-04-02 20:13  izcat  阅读(135)  评论(0编辑  收藏  举报