2024届计算机秋招100天备战:力扣每日打卡挑战
最近两个月力扣困难题不再落下,打卡全满勤,激发了持续刷题的斗志。这里将持续记录打卡过程中的难题和面试八股。
2023/4/2 1039. 多边形三角剖分的最低得分
题目大意:
多边形每个节点有一个数值,将多边形三角剖分,得分为所有三角形节点乘积之和。求三角剖分后的最低得分。
做题评价:
虽然中等题,但没能第一时间做出来。需要熟练掌握动态规划,避免先入为主而被错误方法引导。
解题过程:
1)三角剖分有一个经典的计数问题,即求凸多边形的三角剖分方案数。该问题很容易通过固定两个端点(如0和n-1),枚举中间节点k,将问题转化为两个多边形(区间0-k)和(区间k-n-1)的子问题。这个问题的解是卡特兰数。
2)先入为主陷入了错误的想法:由于上面问题结果的规模是指数增长,我以为无法通过动态规划求解出每一种方案来找到全局最小值,于是不断尝试用贪心法:
- 重复选择值最小的两个节点:需要节点不相邻,这个改对甚至通过了92 / 94 个测试用例。
- 重复选择值相乘最小的两个节点
- 交了三次,分别得到一些例外情况 [3-6-1-2] 和 [2-3-7-3]
3)重新思考子问题,即如何表示一个多边形的最低得分。
- 用dp[i][j] 表示 i~j 这些节点围成的多边形的最优解
- 初始化
dp[i][i+1] = 0
(两个节点)
dp[i][i+2] = v[i]*v[i+1]*v[i+2]
(三个节点) - 由边数从小到大遍历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])
- 返回答案 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对以上记录的评价
你的博客记录已经介绍了这个题目的主要思路,但是可能还可以进行以下改进:
- 给题目加上解释,解释题目的背景、输入、输出以及题目要求。
- 对算法进行更加详细的解释,解释每一步的意义、目的和思路,并且可以在代码注释中进行更加详细的说明,方便读者理解和复现。
- 对于复杂度的分析可以进行更加详细的说明,例如为什么暴力搜索和剪枝算法并不能通过所有的测试用例,以及动态规划算法的复杂度计算过程。
- 在博客中添加一些样例以及具体的解释,可以更好的帮助读者理解算法的实现过程以及解题思路。
- 对于算法的时间复杂度进行更加详细的说明,例如如何确定最大的复杂度、如何通过实验测试算法的时间复杂度等等。
- 可以对算法进行优化,提出更加高效的算法并进行实现,然后对比两种算法的时间复杂度和实现难度等指标。
- 最后可以添加一些自己的思考和心得,可以从算法实现过程、实验测试过程、思考过程等方面进行总结。
ChatGPT润色修改版
这是一道困难级别的题目。题目要求我们选择规模最小的必要团队,使得满足所需求的技能列表。这里记录正解以正视听,爆搜+剪枝能过主要是因为数据太弱了,不管怎么剪枝,极限情况下还是要在60个里找出16个人。
因此,我们需要想出一种更加高效的算法来解决这个问题。我们可以采用以下三种解法:
- 暴力搜索 由于最多有60个人,每个人可以选或不选,因此暴力搜索的时间复杂度是 2^60 = 10^18,不可行。
- 优先选中未满足的 这种方法是优先选中未满足的技能,最多选出16个人。复杂度为 C(60, 16) = 10^14,这样可以通过此题,但是复杂度还是太高,不够优秀。
- 动态规划 动态规划是一种非常高效的算法,我们可以使用动态规划+状态压缩来解决这个问题。
状态压缩的思路是:将每个技能映射到二进制的一位上,例如技能 \(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次呢?一次指令方向变化有四种可能:
- 保持原方向,则仅一次执行
- 相比原来旋转180度,则再执行一次回到初始方向。
- 相比原来左转/右转,则再重复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
(长期更新)