LeetCode笔记:Biweekly Contest 31 比赛记录

  • Biweekly Contest 31
    • 0. 赛后总结
    • 1. 题目一
      • 1. 解题思路
      • 2. 代码实现
    • 2. 题目二
      • 1. 解题思路
      • 2. 代码实现
    • 3. 题目三
      • 1. 解题思路
      • 2. 代码实现
    • 4. 题目四
      • 1. 解题思路
      • 2. 代码实现

0. 赛后总结

赛前坦率说是想通过Biweekly Contest找点自信的,结果他喵的又是做的一塌糊涂,成绩惨不忍睹,满心的郁闷挤压在心中,连觉都睡不好,简直了。

坦率地说,这次的比赛就做题来说事实上是没有犯太大的错误的,前三题都搞定了,而且纯粹花在这三题上的时间事实上也并没有很多,可能30多40分钟的样子吧,不是什么很好的成绩,但终究不至于沦落到最后这么个悲惨的结果。

最终成绩这么差的原因更多的还是在于比赛技巧上的失败,因为我在第二题没有一下子想清楚实现细节的情况下就用了暴力解法,导致超时,超时之后也没有第一时间去把后来的解法中的思路去想清楚,而是先去做了第三第四题,虽然第三题没啥问题一次搞定了,第四题却成功地掉进了题目的陷进中,不断地在暴力求解然后优化,殊不知根本思路上错误之后无论如何优化也无法达到答案要求的 O ( N ) O(N) O(N)的时间复杂度,徒劳的浪费了大量的时间。

另外,以后leetcode这种算时间的比赛还是老老实实用python吧,自己做题用c就用c了,但是打比赛的时候用c真的debug起来太麻烦了。。。

whatever,只能说,吃一堑长一智吧,虽然都快吃的撑了。。。

回归到这次比赛的题目本身,这次的题目整体上来说就是完全凭巧劲,题目理解透彻了,解题思路正确了,那么做题就是分分钟的事,无论是最开始easy的题目还是最后一题hard的题目,事实上都特别简单,但关键就是你得花点心思去把问题想清楚,不要埋头就是干。。。

下面,我们就来分别给出各题的求解思路以及实现代码吧。

1. 题目一

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

  • Count Odd Numbers in an Interval Range

1. 解题思路

这题作为一道easy题目,本身不会有太大的难度,无非就是统计一下闭区间中的奇数个数而已,因此,我们只需要按照如下规则实现算法即可:

  1. 如果闭区间包含2N个数字,则奇数的数目为N;
  2. 如果闭区间包含2N+1个数字,则分情况讨论:
    1. 如果第一个数字为奇数,则奇数数字数目为N+1;
    2. 反之,若第一个数字为偶数,则奇数数字数目为N;

2. 代码实现

给出代码实现如下:

int countOdds(int low, int high){
     
    if((high - low + 1) % 2 == 0){
     
        return (high - low + 1) / 2;
    }
    else if(low % 2 == 1){
     
        return (high - low + 1) / 2 + 1;
    }
    else{
     
        return (high - low + 1) / 2;
    }
}

上述代码实现提交之后测评得到:耗时0ms,占用内存5.1MB。

2. 题目二

题目二的试题链接如下:

  • Number of Sub-arrays With Odd Sum

1. 解题思路

坦率地说,这题一看就是基于奇偶性的动态规划题目,解法一定是先求解每个元素的前面所有元素的累计值,然后通过分析这些累计值的奇偶性得到最终的答案。

可惜在第一时间做这题的时候一下子没能想清楚后面这个对应关系,想着反正就是一道medium的题目,暴力求解估计也能搞定,反正也就 O ( N 2 ) O(N^2) O(N2)的时间复杂度,应该也不至于超时。

然后,然后就没有然后了。。。

脸好疼。。。

下面,我们给出正确的解法思路如下:

  1. 给出list中每一个元素前面所有的元素总和,得到一个cumsum列表;
  2. 对上述cumsum列表,分析其中每一个元素的前方所有元素的奇数个数与偶数个数;
  3. 对于每一个元素,我们分情况讨论:
    1. 如果到该元素的累计总和为奇数,则以该元素为终点的合法子串数目为cumsum列表中其前方的偶数元素个数;
    2. 如果到该元素的累计总和为偶数,则以该元素为终点的合法子串数目为cumsum列表中其前方的奇数元素个数;

这一解法的时间复杂度为 O ( N ) O(N) O(N)

当然,上述算法依然可以在细节上进行更进一步的优化,比如:

  1. 在第一步的求和中,事实上我们只需要知道其奇偶性就行了,没有必要真的求和;
  2. 这三个步骤可以在同一个for循环中一起实现,因为他们之间没有前后的交错依赖关系;

但是,为了使得代码尽可能地可读,这里,我们不再进行后续的优化。

2. 代码实现

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

class Solution:
    def numOfSubarrays(self, arr: List[int]) -> int:
        MOD = 1000000007
        
        cumsum = [0]
        for n in arr:
            cumsum.append(cumsum[-1] + n)
        
        n = len(arr)
        odd = [0] * (n+1)
        even = [0] * (n+1)
        for i in range(n):
            if cumsum[i] % 2 == 0:
                even[i+1] = even[i] + 1
                odd[i+1] = odd[i]
            else:
                even[i+1] = even[i]
                odd[i+1] = odd[i] + 1

        ans = 0
        for i in range(1, n+1):
            if cumsum[i] % 2 == 0:
                ans += odd[i]
            else:
                ans += even[i]
        return ans % MOD

提交上述代码之后得到评测结果:耗时1572ms,占用内存24MB。

而当前提交的最优实现的耗时为1204ms,大致看了一下它的代码,思路和我们差不多,但是比我们做了更多细节上的优化。

3. 题目三

题目三的试题链接如下:

  • Number of Good Ways to Split a String

1. 解题思路

这道题就没有什么技巧可言,按照题目的意思求出以每个元素为切分点时前方和后方的字符集合数量即可。

2. 代码实现

给出代码实现如下:

class Solution:
    def numSplits(self, s: str) -> int:
        n = len(s)

        preset = set()
        prenum = [0] * n
        for i in range(n):
            preset.add(s[i])
            prenum[i] = len(preset)

        postset = set()
        postnum = [0] * n
        for i in range(n-1, -1, -1):
            postset.add(s[i])
            postnum[i] = len(postset)

        ans = 0
        for i in range(n-1):
            if prenum[i] == postnum[i+1]:
                ans += 1
        return ans

提交代码之后得到评测结果如下:耗时176ms,占用内存15.8MB。

而当前的最优代码耗时164ms,并没有太过显著的差异。

4. 题目四

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

  • Minimum Number of Increments on Subarrays to Form a Target Array

1. 解题思路

这道题是比赛的时候把我坑的最惨的一题,第一眼看过去感觉只要按照题目的思路做就完事了,做完之后发现超时,然后想着有没有优化的空间,然后一看代码,诶,还真有,然后就优化,还优化了两次,结果两次优化之后依然超时,直到整个比赛结束还在努力地优化代码的细节,完全没觉得哪里有问题。

然后,到优化到实在优化不动了之后,去看了一下为啥头几名的大佬做这题的时间居然只有几分钟,然后发现,他们的代码居然他喵的只有不到10行,而且看了之后就是秒懂的程度,顿时整个人都是崩溃的!!!

总而言之,言而总之,这道题,千万千万千万不要按照题目的讲解方式去暴力的一层一层去处理,那样的时间复杂度是在 O ( N × H ) O(N \times H) O(N×H)这个量级(N为列的数目,H为最高的列高度),虽然我们可以通过通过一些操作来大量的进行剪枝操作,但是感觉应该是不可能达到 O ( N ) O(N) O(N)的时间复杂度的。

下面,废话不多说,直接给出大佬们的解法思路如下:

  1. 考察第一列,假设这一列高度为n,则无论如何我们都得通过n次操作来达到这一高度;
  2. 考察其后方的每一列的高度:
    1. 如果这一列高度低于前一列的高度,那么说明当前一列已经被消除时,这一列的高度一定是可以通过停在某一次中间过程中达到的;
    2. 如果这一列的高度高于前一列的高度,那么假设两者的高度差为m,则我们在通过一系列操作达到了上一列的高度之后,还需要m次额外的操作来达到这一列的高度,即总的操作次数需要加上m。

通过这种解题思路,我们可以直接将时间复杂度降低至 O ( N ) O(N) O(N),而且其代码实现也异常的简单,真的是被大佬们打击的体无完肤。

2. 代码实现

下面,我们给出上述解法的代码实现,嗯,只有区区几行代码而已。。。

int max(int x, int y){
     
    return x > y ? x : y;
}

int minNumberOperations(int* target, int targetSize){
     
    int ans = target[0];
    for(int i=0; i<targetSize-1; ++i){
     
        ans += max(target[i+1] - target[i], 0);
    }
    return ans;
}

提交后得到评测数据如下:耗时160ms,占用内存12.8MB。

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