0. 赛后总结
这次的比赛发挥倒是中规中矩,半个小时多点搞定了前三题,然后卡死在第四题上死活做不出来,不过大概是因为第四题太难的关系,做出来的人挺少的,结果就导致虽然整体做题感觉上和昨晚的biweekly contest差不多,但是排名倒是提高了不少,多少算是最近这段时间里面难得的一点安慰吧。
下面,话不多说,就直接给出赛题的整理吧。
1. 题目一
给出题目一的试题链接如下:
1. 解题思路
这题的解题思路还是比较清晰的,无非就是将原先的string按照给定的indices进行排序即可。
因此,一种比较直观的实现方法就是:
- 给出一个等长的string,然后将对应位置的字符按照indices中对应的内容进行替换。
2. 代码实现
下面,我们给出python的代码实现如下:
class Solution:
def restoreString(self, s: str, indices: List[int]) -> str:
ans = [c for c in s]
for i, idx in enumerate(indices):
ans[idx] = s[i]
return "".join(ans)
提交代码之后得到评测结果如下:耗时52ms,占用内存13.8MB,属于当前最优效果。
2. 题目二
给出第二题的试题链接如下:
1. 解题思路
这道题我们实际来考察一下解法,就可以很快速地得到代码地实现思路。
在做这个问题时,我们只需要从末尾开始,每一次遇到一个不连续的字符,就做一次翻转,这样,做完全部的翻转之后,我们就能得到同样前后变化顺序的字符串,即他与原始字符串要么相同,要么相反。
然后,我们考虑第一个字符,如果是0,则我们无需再做额外的操作,反之,我们多做一次反转即可。
例:
src: 00000 -> tgt: 10111
step 01: 00000 -> 00111
step 02: 00111 -> 01000
step 03: 01000 -> 10111
2. 代码实现
基于上述思路,我们即可快速地给出代码实现如下:
class Solution:
def minFlips(self, target: str) -> int:
count = 1 if target[0] == "1" else 0
flag = target[0]
for c in target:
if c != flag:
count += 1
flag = c
return count
提交代码得到评测结果如下:耗时60ms,占用内存14.3MB,属于当前最优效果。
3. 题目三
给出题目三的试题链接如下:
1. 解题思路
这一题的解题思路可以基于后续遍历进行改写。
对任意两个叶子节点,他们必定分立于某一个根节点的左右两子树,因此,他们的距离就是他们针对这一个根节点的深度之和。
故,整体的解题思路就可以写作如下:
- 对于任意一个根节点,找到其所有左子树下的叶子节点的深度以及右子树下叶子节点的深度;
- 两两计算左右子树中叶子节点的距离,如果不大于距离上限distance,则总计数加一;
- 将左右子树下的叶子节点深度全部加一并合并为一个集合进行返回,即可得到上一个根节点的左/右子树下的全部叶子数目及其对应的深度。
由于本人语文不太好,上述思路解释多少会有点晦涩。因此,下面我们直接给出代码实现,希望令上面的解释可以更好理解一点。。。
2. 代码实现
给出代码实现如下:
class Solution:
def countPairs(self, root: TreeNode, distance: int) -> int:
count = 0
def dfs(root):
nonlocal count
if root is None:
return []
elif root.left is None and root.right is None:
return [0]
left_leaves = dfs(root.left)
right_leaves = dfs(root.right)
for lleaf in left_leaves:
for rleaf in right_leaves:
if lleaf + rleaf + 2 <= distance:
count += 1
return [dep + 1 for dep in left_leaves + right_leaves]
dfs(root)
return count
提交代码得到评测结果如下:耗时296ms,占用内存15.1MB。
当前的最优结果耗时240ms,但是思路为同样的思路,只是在实现细节上有所不同。
4. 题目四
给出题目四的试题链接如下:
1. 解题思路
这道题比赛的时候留了将近一个小时来做,最终做了半个多小时之后放弃了,因为实在想不到什么非常好的解题思路,甚至说连一个比较合理的暴力求解思路都没能想到。
其主要难点在于以下几点:
- 每一次删除操作都可以发生在字符串的任意位置;
- 每次删除操作之后都可能有以下几种情况发生:
- 删除之后压缩字符长度没有发生变化,如:
a3 -> a2
- 删除操作之后本来不连贯的字符可以合并,如:
a3b2a4 -> a7
(删除了bb
) - 删除操作之后,连续字符总长下降一个量级,如:
a10 -> a9
- 进行删除后的连续字符串合并操作之后,连续字符总长增长一个量级,如:
a5a6 -> a11
- 删除之后压缩字符长度没有发生变化,如:
因此,直接按照字符串的子串簇的方式考虑每次最优先删除次序的方式就会很难考虑清楚每次操作究竟应该删除哪个子串,要删除多少长度。
比赛之后,研读了一下大佬的解法,发现大佬并没有考虑一次删除一个子串,而是每次删除一个字符,并基于此通过动态规划的方式对这个问题进行了解答。
下面,我们给出大佬的解题思路:
- 对于每个位置,我们都只可能保留或者删除该字符,我们只需要比较这两种情况下的得到的最优压缩子串长度,取较小值即可;
另外需要注意的是,在求解最优压缩子串长度的算法问题上,我们不能每次都暴力的将整个子串都传入进行求解,一来字符串的复制会消耗大量的时间,二来每次计算整个序列的压缩子串长度的时间复杂度是 O ( N ) O(N) O(N),其两两之间会存在大量的重复计算内容。
参考大佬的解题方法,他是采用了增量地计算每一段连续子串的压缩字符长度的方式进行求解的。
下面,我们基于大佬的解答思路来进行自己的代码实现。
2. 代码实现
依照上述解题思路,我们仿写得到代码实现如下:
import math
class Solution:
def getLengthOfOptimalCompression(self, s: str, k: int) -> int:
def cal_compressed_consecutive_string_len(strlen):
if strlen <= 1:
return strlen
else:
return 1 + len(str(strlen))
n = len(s)
@lru_cache(None)
def dp(idx, consecutive_len, pre_char, delete_num):
if delete_num > k:
return math.inf
if idx == n:
return cal_compressed_consecutive_string_len(consecutive_len)
# delete s[idx]
l1 = dp(idx+1, consecutive_len, pre_char, delete_num + 1)
# keep s[idx]
if s[idx] == pre_char:
l2 = dp(idx+1, consecutive_len+1, pre_char, delete_num)
else:
l2 = dp(idx+1, 1, s[idx], delete_num) + cal_compressed_consecutive_string_len(consecutive_len)
return min(l1, l2)
return dp(0, 0, '', 0)
提交代码评测得到:耗时2764ms,占用内存372.8MB。
这个耗时在昨天的查看结果中属于第一梯队的,但是今天再次查看的时候最优耗时已经被刷到了500ms,2764ms这个成绩已经被甩到50%之后了。。。
看了一下大佬们的优化代码,发现对其中做了很多细节的优化。
但是,处于代码可读性的考虑(qi shi jiu shi lan),这里就不做过多的展开了,如果有兴趣的话可以自行查看大佬们的优化算法。