LeetCode Notes: Weekly Contest 201 Contest Record

0. Summary after the game

This competition is also relatively difficult. The hard question is a dynamic programming question, so it can be done smoothly, but it took a little longer, it took 1 hour and 15 minutes, and then it was tragically wrong in the middle. Three times, two timeouts, and one sb error. The result was almost an hour and a half, but in the end, the ranking was pretty decent, with more than seven hundred people. So far, it is quite satisfying.

Then, looking at the time consumption of the big guys, the fastest took only 10 minutes, and then the first 25 points were basically at the order of 20 minutes. As soon as you compare, you can really feel the difference in strength. . .

Then the biggest impression of doing the question this time is the time-consuming problem. The timeout problem is always the most common and least desirable problem. If it is an ordinary wrong answer, then analyzing bad-case generally can always find a way to fix it, and when it is timed out, it really feels almost impossible to start, it is very uncomfortable. . .

The luck this time is not bad. After optimizing the implementation method, the submission is finally successful, but the efficiency is still terrible, and it is almost impossible to see. It is still necessary to study hard to see if there are any more elegant problem-solving ideas. .

btw, regarding the following implementation problem, I really want to shout here: never, never, never try to copy the array in the process of passing parameters! ! ! This is too efficient!

1. Topic One

The link to the question given for question one is as follows:

1. Problem solving ideas

The first question seems a bit complicated, but after all, it is an easy question. We only need to pay attention to the situation given in Example 2, that is, after deleting bB, the latter Aneed to abe compared with the previous one . Therefore, this question is just one A survey of typical stack problems.

We examine a certain character:

  1. If the character is the current first character (the stack is empty), then the character is legal and added to the stack;
  2. If the character contradicts the last character in the stack, not only this character cannot be added, but we have to delete the last character from the stack;
  3. If the character does not contradict the last character in the stack, we can add it to the stack smoothly.

2. Code implementation

According to the above ideas, we can give the code implementation as follows:

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)

After submitting the code, the evaluation results: It takes 40ms and takes up 13.9MB of memory.

The current optimal implementation takes 24ms. I inspected its implementation at that time and found that there is no fundamental difference between the two in essence, but the implementation is somewhat different. In terms of readability, or my code is higher, So I won't repeat it here.

Of course, since the length of the string s is not greater than 100 for the title, if you do not use a stack, you can actually solve this problem step by step by violently following the operation method of the title. In fact, I did this in the competition. Yes, it is the first idea after all. . . But this method is somewhat unclear in terms of thinking, and the code is difficult to say elegant, so I won't repeat it here.

2. Topic 2

The link to the test questions for topic two is as follows:

1. Problem solving ideas

This question is probably the most satisfactory one I have done in this competition. The idea of ​​solving the problem is actually simple, just a recursive problem.

Obviously, there are two special cases:

  1. n = 1 n=1 n=1 , at this time k must be 1, and the answer is 0;
  2. k = 2 n − 1 k = 2^{n-1} k=2n 1 , the answer is 1 at this time;

Below, let's examine the general situation in addition to the above two situations:

  1. k < 2 n − 1 k < 2^{n-1} k<2n 1 , at this time,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>2n 1 , in this case,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)

Recursively perform operations, we can get any set of legal inputs (n, k) (n, k)(n,k ) solution.

2. Code implementation

The code implementation given above is as follows:

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))

After submitting the code for evaluation, it took 20ms and took up 13.8MB of memory. Belongs to the current first echelon.

3. Topic Three

The link to the test questions for topic three is as follows:

1. Problem solving ideas

The idea of ​​this question is quite intuitive, and it can be regarded as a recursive question.

When we examine any element in the array, he will only have two situations:

  1. Use this element as the starting point of a subarray, then the end of this subarray must be the nearest node k (if this node exists) that makes the sum of the target, and then we repeat the above investigation process from the k+1 node;
  2. If this element is not used, then we can directly examine the next node.

2. Code implementation

The above idea is not complicated, the rest is how to implement it. At the beginning I used a very violent recursive algorithm, which resulted in a timeout. After that, we optimized the search for the next starting point, that is, the pruning operation was performed so that the algorithm was finally passed, but there were still big flaws in execution efficiency.

Below, the implementation of our algorithm is given as follows:

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)

The above code was submitted and passed, but the evaluation found that it took 2560ms and took up 202.9MB of memory.

The current optimal time-consuming is only 624ms, and the occupied memory is only 19MB. Obviously our current solution is only feasible and far from the optimal solution.

3. Optimized solution

Looking at the current thinking of the optimal solution, the overall thinking is still the same, but, unlike our forward derivation, their solution is a reverse regression.

For any node n to the previous node m is the target, the optimal solution of the node is max (f (n − 1), 1 + f (m)) max(f(n-1), 1+f(m))max(f(n1),1+f(m))

In this way, we can treat it as a dynamic programming problem to answer.

The modified code is implemented as follows:

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]

Submit the code to get the evaluation result: it takes 812ms and takes up 33.8MB of memory.

Although it has not reached the optimal 624ms, the overall difference is almost the same.

4. Topic Four

The link to the test questions for question four is as follows:

1. Problem solving ideas

At first glance, the direct idea of ​​this question was whether it was possible to directly derive the optimal cutting method mathematically, but later found that it was almost impossible.

Therefore, the remaining way is to traverse all the programs programmatically and find the optimal solution.

That is, for each segmentation, the optimal solution is the most suitable segmentation with the lowest substitution price among all current possible segmentation.

2. Code implementation

The code implementation of the above ideas is as follows:

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)

After submitting the code, the evaluation results: It took 2956ms and took up 27MB of memory.

The current optimal solution only takes 544ms, which is almost an order of magnitude improvement in performance. Therefore, our method must be too violent.

Still have to learn their solutions.

3. Optimized solution

We examine the current optimal solution and find:

  • Although the code structure is quite different, the idea of ​​the solution should be the same in essence;
  • The difference in code is that we use the function's caching mechanism to implement dynamic programming, while the current optimal solution uses a two-dimensional array to record, that is, the standard dynamic programming method;

Therefore, in principle, our code may have performance deviations, but the performance should never be so bad that there must be some shortcomings in the code implementation.

In fact, in the competition, due to poor implementation methods, a large number of array copies and internal operations occurred during the transmission of internal parameters, which caused timeout problems.

After checking our code, we found that there is indeed an array operation process in parameter passing. Therefore, we modify it to completely remove the array in the parameter, and get the following code implementation:

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)

In this way, we can completely remove the array elements in the parameters to ensure that no additional array operations occur.

At this time, the evaluation effect of the code is improved to: it takes 844ms and takes up 17.7MB of memory.

There is still a certain efficiency gap from the current optimal solution, but it is already within an acceptable range.

4. The current optimal solution code implementation

Regarding the current optimal solution, forgive me for not understanding the details of his implementation, so I won’t explain more here, just put the code below, if readers are interested, you can read it yourself. Of course, if you can It would be better to explain in the comment section.

If one day in the future I understand its design, maybe I will also add it, but I feel that I don’t want to look at this in a short time. . .

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]

Guess you like

Origin blog.csdn.net/codename_cys/article/details/107902746