LeetCode笔记:Weekly Contest 201 比赛记录

  • LeetCode笔记:Weekly Contest 201
    • 0. 赛后总结
    • 1. 题目一
      • 1. 解题思路
      • 2. 代码实现
    • 2. 题目二
      • 1. 解题思路
      • 2. 代码实现
    • 3. 题目三
      • 1. 解题思路
      • 2. 代码实现
      • 3. 优化解法
    • 4. 题目四
      • 1. 解题思路
      • 2. 代码实现
      • 3. 优化解法
      • 4. 当前的最优解代码实现

0. 赛后总结

这次的比赛同样相对来说难度一般,hard的题目考的就是一道动态规划的题目,因此也算是顺利做出来了,就是耗时长了一点,花了1小时15分钟,然后中间还悲剧地错了3次,两次超时,一次sb错误,结果总耗时差不多快1个半小时了,但终究来说排名还算过得去,七百多名,目前来看挺满足了。

然后看大佬们的耗时,最快的只花了10分钟,然后前25分也都基本在20分钟这个量级,一比较就切实地感受到实力的差距。。。

然后这次做题最大的感想就是耗时问题,超时问题总是当前最常遇到也是最不想遇到的问题。如果是普通的wrong answer,那么分析bad-case一般来说总是能找到修正的思路,而遇到超时,真的是感觉几乎无从下手,那就很难受了。。。

这次的运气还算好,优化了实现方式之后,总算是提交成功了,但是效率依然是惨的一逼,简直不能看,还是要好好学习一下看看有没有什么更为优雅的解题思路。

btw,有关后面的这个实现问题,这里真的要大吼一句:永远,永远,永远不要尝试在传参过程中进行数组的复制!!! 这简直太伤效率了!

1. 题目一

给出题目一的试题链接如下:

  • Make The String Great

1. 解题思路

第一题看似有点复杂,但是终究是一道easy的题目,我们只需要注意例二中给出的情况,即在删除了bB之后,后面的A需要与上一个a进行比较,因此,这道题目就是一个比较典型的栈的问题考察。

我们考察某一字符:

  1. 如果该字符为当前第一个字符(栈为空),则该字符合法,加入到栈中;
  2. 如果该字符与栈中最后一个字符矛盾,则不但这个字符不能加入,我们还要从栈中删除末尾的字符;
  3. 如果该字符与栈中最后一个字符不矛盾,则我们将其顺利加入到栈中即可。

2. 代码实现

根据上述思路,我们可以给出其代码实现如下:

class Solution:
    def makeGood(self, s: str) -> str:
        def is_invalid(c1, c2):
            if abs(ord(c1) - ord(c2)) == abs(ord('a') - ord('A')):
                return True
            return False
        
        stack = []
        for c in s:
            if stack == []:
                stack.append(c)
            else:
                if is_invalid(stack[-1], c):
                    stack.pop()
                else:
                    stack.append(c)
        return "".join(stack)

提交代码后评测得到:耗时40ms,占用内存13.9MB。

当前最优的的实现耗时24ms,当时考察其实现,发现本质来说两者并没有根本的差异,只是实现上多少有点不同,就可读性而言,或者还是我的代码更高一些,因此就不再这里多加赘述了。

当然,由于题目限定字符串s的长度不大于100,因此如果不使用栈,暴力地按照题目的操作方法一步一步执行事实上也是能够解出这一题的,事实上我在比赛中就是这么做的,毕竟是第一思路。。。但这种方法在思路上多少会有点不清晰,且代码很难说优雅,因此这里也不多加赘述了。

2. 题目二

给出题目二的试题链接如下:

  • Find Kth Bit in Nth Binary String

1. 解题思路

这一题大约算是我这一次比赛中做的最满意的一道题了,它的解题思路其实也简单,就是一个递归问题而已。

显然,有两种特殊情况:

  1. n = 1 n=1 n=1,此时k一定为1,答案为0;
  2. k = 2 n − 1 k = 2^{n-1} k=2n1,此时答案为1;

下面,我们来考察除上述两种情况外的一般情况:

  1. k < 2 n − 1 k < 2^{n-1} k<2n1,此时有 f ( n , k ) = f ( n − 1 , k ) f(n, k) = f(n-1, k) f(n,k)=f(n1,k)
  2. k > 2 n − 1 k > 2^{n-1} k>2n1,此时有 f ( n , k ) = ¬ f ( n − 1 , 2 n − k ) f(n, k) = \neg f(n-1, 2^{n} - k) f(n,k)=¬f(n1,2nk)

递归进行运算,我们即可得到任意一组合法输入 ( n , k ) (n, k) (n,k)的解。

2. 代码实现

给出上述思路下的代码实现如下:

class Solution:
    def findKthBit(self, n: int, k: int) -> str:
        def invert(c):
            return '1' if c == '0' else '0'
        
        if n == 1:
            return '0'
        
        mid = 2**(n-1)
        if k == mid:
            return '1'
        elif k < mid:
            return self.findKthBit(n-1, k)
        else:
            return invert(self.findKthBit(n-1, 2*mid-k))

提交代码评测得到:耗时20ms,占用内存13.8MB。属于当前第一梯队。

3. 题目三

给出题目三的试题链接如下:

  • Maximum Number of Non-Overlapping Subarrays With Sum Equals Target

1. 解题思路

这一题的思路多少还是比较直观的,也算是一个递归的问题。

我们考察array中的任意一个元素,他只会有两种情况:

  1. 使用这个元素作为一个subarray的起点,那么这个subarray的终点一定是使之总和为target的最邻近节点k(如果这个节点存在的话),然后我们从第k+1节点开始重复上述考察过程;
  2. 不使用这个元素,那么我们直接考察下一个节点即可。

2. 代码实现

上述思路并不复杂,剩下的就是如何实现,一开始我采用了一种非常暴力的递归算法,结果导致了超时。之后,我们对下一个起始点的寻找操作进行了优化,即进行了剪枝操作,使得算法最终得以通过,但是执行效率上依然有很大的缺陷。

下面,给出我们的算法实现如下:

class Solution:
    def maxNonOverlapping(self, nums: List[int], target: int) -> int:
        n = len(nums)
        cumsum = [0 for i in range(n+1)]
        for i in range(n):
            cumsum[i+1] = cumsum[i] + nums[i]
            
        cache = {
     }
        for idx, s in enumerate(cumsum):
            cache[s] = cache.get(s, []) + [idx]
            
        @lru_cache(None)
        def dp(idx):
            if idx > n:
                return 0
            s1 = dp(idx+1)
            aim = target + cumsum[idx]
            next_loc = [i for i in cache.get(aim, []) if i > idx]
            if next_loc == []:
                return s1
            else:
                return max(s1, 1 + dp(next_loc[0]))
            
        return dp(0)

上述代码提交通过,但是评测得到其耗时2560ms,占用内存202.9MB。

而当前最优的耗时仅为624ms,占用内存更是只有19MB,显然我们当前的解法只是可行而已,远不是最优解法。

3. 优化解法

看了一下当前最优解法的思路,整体而言思路还是相同的,但是,不同于我们正向的推导,他们的解法是反向的回归。

对于任意一个节点n到之前某一个节点m间的和是target,则该节点的最优解为 m a x ( f ( n − 1 ) , 1 + f ( m ) ) max(f(n-1), 1+f(m)) max(f(n1),1+f(m))

如此,我们就可以将之视为一个动态规划的题目进行解答。

修改之后得到的代码实现如下:

class Solution:
    def maxNonOverlapping(self, nums: List[int], target: int) -> int:
        n = len(nums)
        memory = {
     0: 0}
        prefix_sum = 0
        dp = [0 for i in range(n+1)]
        
        for i, k in enumerate(nums):
            prefix_sum += k
            if prefix_sum - target in memory.keys():
                dp[i+1] = max(1+dp[memory[prefix_sum - target]], dp[i])
            else:
                dp[i+1] = dp[i]
            memory[prefix_sum] = i+1
            
        return dp[-1]

提交代码得到评测结果:耗时812ms,占用内存33.8MB。

虽然还没有到达最优的624ms,但整体已经相差无几了。

4. 题目四

给出题目四的试题链接如下:

  • Minimum Cost to Cut a Stick

1. 解题思路

第一眼看到这一题的直接想法是能不能数学上的直接推导出最优切割方法,但是后来发现几乎没啥可能性。

因此,剩下的方式就是程序上对所有的方案进行遍历而后找到最优解了。

即,针对每一次切分,最优解都是当前所有可能的切分中取代价最小者作为最合适的切分。

2. 代码实现

给出上述思路的代码实现如下:

import math

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = tuple(sorted(cuts))
        
        @lru_cache(None)
        def dp(st, ed, cuts):
            if len(cuts) == 0:
                return 0
            ans = math.inf
            for idx, cut in enumerate(cuts):
                s1 = dp(st, cut, cuts[:idx])
                s2 = dp(cut, ed, cuts[idx+1:])
                ans = min(ans, ed-st + s1 + s2)
            return ans

        return dp(0, n, cuts)

提交代码之后评测得到:耗时2956ms,占用内存27MB。

而当前的最优解答仅耗时544ms,近乎一个量级的性能提升,因此,必然我们的方法太过暴力。

还是要去学习一下他们的解法。

3. 优化解法

我们考察当前最优的解法,发现:

  • 虽然代码结构大不相同,但是解法的思路究其本质来说是应该是相同的;
  • 代码的差异在于我们使用函数的缓存机制来实现动态规划,而当前的最优方案使用的方式是二维数组来记录,即标准的动态规划方法;

因此,原则上来说,我们的代码可能会性能偏差,但是绝不应该性能差到这种程度,必然是在代码实现方面还存在着什么不足之处。

事实上,我们在比赛中就因为实现方法不佳导致内部参数传递过程中出现了数组的大量复制与内部操作,使得出现超时问题。

检查我们的代码后发现,当前确实还存在有参数传递中的数组操作过程,因此,我们将其进行修正,彻底去除参数中的数组,得到如下代码实现:

class Solution:
    def minCost(self, n: int, cuts: List[int]) -> int:
        cuts = [0] + sorted(cuts) + [n]
        
        @lru_cache(None)
        def dp(st, ed):
            if ed - st == 1:
                return 0
            return cuts[ed]-cuts[st] + min([dp(st, idx) + dp(idx, ed) for idx in range(st+1, ed)])
        
        return dp(0, len(cuts)-1)

如此一来,我们就可以彻底去除参数中的数组元素,确保不会出现额外的数组操作。

此时,代码的评测效果提升为:耗时844ms,占用内存17.7MB。

距离当前的最优方案还有一定的效率差距,但是已经处于可接受范围内了。

4. 当前的最优解代码实现

而有关当前的最优解,原谅我确实没有看懂他的细节实现,因此,这里就不多做解释了,仅仅将代码放在下方,如果有读者感兴趣的话可以自行阅读,当然,如果能在评论区解释一下就更好了。

未来如果哪一天我看懂了它的设计的话,也许我也会来补充一下,但是,感觉短时间内是不想来看这个了。。。

class Solution:
    def minCost(self, n: int, cutsOrder: List[int]) -> int:
        cutsOrder = [0] + sorted(cutsOrder) + [n]
        s = len(cutsOrder)
            
        dp = [[float('inf')] * (s) for _ in range(s)]
        for d in range(1, s):
            for l in range(s-d):
                if d == 1:
                    dp[l][l+d] = 0
                else:
                    dp[l][l+d] = min((dp[l][i] + dp[i][l+d]) for i in range(l+1, l+d)) + cutsOrder[l+d] - cutsOrder[l]
        return dp[0][s-1]

你可能感兴趣的:(leetcode笔记,leetcode)