ARTS Week 5 | LeetCode 44 regular expression | Redis backup details | Golang retrospective memory skills

ARTS

ARTS is an activity launched by Chen Hao in the geek time column, aiming to stick to learning through sharing.

Each person writes an ARTS every week: Algorithm is an Algorithm problem, Review is to read an English article, technology / tips is to Share a small technology, and Share is to Share a point of view.

This week

This week's ARTS you will see:

  1. Two LeetCode character matching questions.
  2. Some details about Redis replica replication.
  3. A little skill of Go defer in the memory of backtracking problem.
  4. Does text and video affect the depth of content?

Algorithm

This week's algorithm questions are two questions about string matching, LeetCode 10.regular-expression-matching and LeetCode 44.wildcard-matching.

These two string matching questions are very typical dynamic programming questions, so they are very typical backtracking questions (manual dog head). If you haven't done it before, it's suggested to do 44 questions first and then 10 questions, because the former is much less difficult. The specific explanations are in the notes. If you have any questions, please comment.

First, 44. Wildcard matching

/*
 * @lc app=leetcode id=44 lang=golang
 *
 * [44] Wildcard Matching
 */

// @lc code=start

// dp[i][j] indicates that prefix substrings of length I and j in s and p can match
// 12 ms, 75.47%
func isMatch(s string, p string) bool {
    ls, lp := len(s), len(p)
    dp := make([][]bool, ls+1)
    for i := range dp {
        dp[i] = make([]bool, lp+1)
    }
    dp[0][0] = true
    for j := 0; j < lp; j++ {
        if p[j] == '*' {
            dp[0][j+1] = true
        } else {
            break
        }
    }

    for i := 1; i <= ls; i++ {
        for j := 1; j <= lp; j++ {
            if p[j-1] == '*' {
                // *Match empty string, * match any character, * match multiple characters
                dp[i][j] = dp[i][j-1] || dp[i-1][j-1] || dp[i-1][j]
            }
            if p[j-1] == '?' || p[j-1] == s[i-1] {
                dp[i][j] = dp[i-1][j-1]
            }
        }
    }
    return dp[ls][lp]
}

// Recall + memory 316 MS, foster than 16.04%
func isMatch_Backtracking(s string, p string) bool {
    ls, lp := len(s), len(p)
    mem := make(map[[2]int]bool, 0) //Time out without memory
    var f func(sc, pc int) bool
    f = func(sc, pc int) (ans bool) {
        if ret, ok := mem[[2]int{sc, pc}]; ok {
            return ret
        }

        // I even wrote the memory of gorang style!
        // Note here that ans and mem are both passed by closure reference
        defer func() {
            mem[[2]int{sc, pc}] = ans
        }()

        if sc == ls && pc == lp {
            ans = true
            return
        }
        if pc == lp {
            ans = false
            return
        }
        if p[pc] == '*' {
            // *Match empty string, * match any character, * match multiple characters
            if sc < ls {
                ans = f(sc, pc+1) || f(sc+1, pc+1) || f(sc+1, pc)
            } else {
                ans = f(sc, pc+1)
            }
            return
        }
        if sc < ls && (p[pc] == '?' || p[pc] == s[sc]) {
            ans = f(sc+1, pc+1)
            return
        }
        return
    }
    return f(0, 0)
}

// @lc code=end

Question 44 is still relatively simple. As long as you notice that the * sign can match white space, a single character and multiple characters can do it. Let's look at 10. Regular expression matching

/*
 * @lc app=leetcode id=10 lang=golang
 *
 * [10] Regular Expression Matching
 */

// @lc code=start

// The beginning of * is not allowed in the test case, and this corner case can be ignored
// And actually starting with * doesn't meet the definition of * itself

// DP
func isMatch(s string, p string) bool {
    ls, lp := len(s), len(p)
    // dp[i][j] means that s[:i] can be matched by p[:j], which means that the length is ij
    // It can also be understood that the subscripts of s and p start from 1, and 0 means empty string
    // I don't know why
    dp := make([][]bool, ls+1)
    for i := 0; i <= ls; i++ {
        dp[i] = make([]bool, lp+1)
    }
    // base case
    dp[0][0] = true
    for j := 0; j < lp; j++ {
        if p[j] == '*' && dp[0][j-1] {
            dp[0][j+1] = true
        }
    }
    // j = 0 and I! = 0 must be false
    for i := 1; i <= ls; i++ {
        for j := 1; j <= lp; j++ {
            if p[j-1] == s[i-1] || p[j-1] == '.' {
                dp[i][j] = dp[i-1][j-1]
            }
            if p[j-1] == '*' {
                if p[j-2] != s[i-1] {
                    dp[i][j] = dp[i][j-2]
                }
                if p[j-2] == s[i-1] || p[j-2] == '.' {
                    // dp[i-1][j] means that the * sign matches multiple characters on the left, which is the most difficult to understand
                    // The problem of matching multiple * left characters is equivalent to looking at several consecutive characters in s that are the same as the * left characters
                    // If there are more than one consecutive character, then if s[i-1] is the later one
                    // So, by "shifting i to the left" in dp[i-1][j], it can be seen that j matches multiple characters on the left side of * equally
                    // Such as aaabc and a*bc
                    // *When matching to the third a, it is equivalent to reusing the A on the left side of * by converting to the matching results that depend on the previous a
                    // Because if i-1 can match the * sign pointed to by j, then i can also
                    // (Continued) because the characters to the left of * are the same as the repeating characters in s
                    // It's really hard to understand. If you can't, just remember
                    dp[i][j] = dp[i][j-2] || dp[i][j-1] || dp[i-1][j]
                }
            }
            // If it is neither equal nor * sign, it is false. Do not set such dp[i][j] (default is false)
        }
    }
    return dp[ls][lp]
}

// Backtracking
func isMatch_BackTracking(s string, p string) bool {
    ls, lp := len(s), len(p)
    var f func(sc, pc int) bool
    f = func(sc, pc int) bool {
        if sc == ls && pc == lp {
            return true
        }
        if pc == lp {
            return false
        }
        if p[pc] == '*' {
            var use0P, use1OrMoreP bool
            // Normally the * sign does not appear in the first character position
            // The condition SC < LS is that maybe s has come to the end, but p has not
            // At this time, you still need to move the position pc of p, but you don't need to compare s[sc] anymore
            // Because the end of sc is not really the end, we need to wait until p is also over
            if pc > 0 && sc < ls && (p[pc-1] == s[sc] || p[pc-1] == '.') {
                use1OrMoreP = f(sc+1, pc) || f(sc+1, pc+1)
            }
            use0P = f(sc, pc+1)
            return use0P || use1OrMoreP
        }
        var eq, bfStar bool
        if sc < ls && (p[pc] == s[sc] || p[pc] == '.') {
            eq = f(sc+1, pc+1)
        }
        if pc+1 < lp && p[pc+1] == '*' {
            bfStar = f(sc, pc+2)
        }
        return eq || bfStar
    }
    return f(0, 0)
}

// Backtracking with memo 100%
func isMatch_BackTrackingWithMemory(s string, p string) bool {
    ls, lp := len(s), len(p)
    var f func(sc, pc int) bool
    mem := make(map[[2]int]bool, 0)
    f = func(sc, pc int) bool {
        if ret, ok := mem[[2]int{sc, pc}]; ok {
            return ret
        }
        if sc == ls && pc == lp {
            return true
        }
        if pc == lp {
            return false
        }
        if p[pc] == '*' {
            var use0P, use1OrMoreP bool
            // Normally the * sign does not appear in the first character position
            if pc > 0 && sc < ls && (p[pc-1] == s[sc] || p[pc-1] == '.') {
                use1OrMoreP = f(sc+1, pc) || f(sc+1, pc+1)
            }
            use0P = f(sc, pc+1)
            mem[[2]int{sc, pc}] = use0P || use1OrMoreP
            return use0P || use1OrMoreP
        }
        var eq, bfStar bool
        if sc < ls && (p[pc] == s[sc] || p[pc] == '.') {
            eq = f(sc+1, pc+1)
        }
        if pc+1 < lp && p[pc+1] == '*' {
            bfStar = f(sc, pc+2)
        }
        mem[[2]int{sc, pc}] = eq || bfStar
        return eq || bfStar
    }
    return f(0, 0)
}

// @lc code=end

In fact, I think it's better to understand backtracking. Dynamic planning is faster in time, but it's more difficult to understand. In particular, dp[i-1][j] is a very difficult point for me to understand when * signs match multiple. In addition, if you use backtracking to solve these two problems, the memorized code looks disgusting, and you need to add a map to each return location. How can you realize the memorization more gracefully? At this time, the advantages of Go can be shown, which is put in Tips as a skill of this week.

Review article recommendation

This week's English article is Redis's official introduction to the backup (Replication) function: Replication.

This is a popular science article on the official website, mainly including the basic introduction of Redis backup function and precautions for use. For example, the following are some of the more important contents in this article.

  1. Synchronization requests are initiated by the master to the attached replica instance, which supports one master and many slaves.
  2. The master-slave synchronization uses asynchronous mode by default, but you can also use the WAIT command to let the master WAIT for the replica to perform the current backup.
  3. The backup function does not block the master's read and write requests.
  4. However, the replica instance will have a short reject request window when processing new and old data replacement.
  5. The backup function can be used to achieve high availability and read-write separation.
  6. For a master whose persistent memory data is closed to disk, restarting Redis may cause the replica instance to get an empty backup.
  7. The master node is responsible for maintaining a combination of Replication ID and offset to indicate the offset of the current master node and backup data.
  8. Replication ID and offset can uniquely identify backup data.
  9. In order to prevent the failure of the provisions in 8 due to the coexistence of multiple masters, when the new master is selected, a new Replication ID will be generated, and the old Replication ID will be retained as the secondary Replication ID to cope with the situation that some replica nodes still use the old Replication ID.
  10. It supports read-write separation. You can configure a replica to be read-only, or you can configure the master to write only when it has a certain number of replica instances and the latency is no less than the set value.
  11. The expire function of replica instance does not depend on its clock synchronization with the master. Its expire can be guaranteed in the following three ways:

    • Replica does not actively epxire a key. After the master side expire s, it will send DEL command to replica.
    • Replica will check the expire d key locally by comparing the clock. If the key has not received the DEL from the master, replica will disable the read request.
    • expire is not processed during Lua script execution.

Tips for Tip programming

One of the tricks to remember in the above questions LeetCode 10 and 44 is to use defer and name the return value.

After using the named return value, you only need to assign a value to the return value before each return. You don't need to update the map used for memorization before each return in order to memorize. However, we add defer in a reasonable position to uniformly update the return value to the memorized map. See the code snippet below.

    f = func(sc, pc int) (ans bool) {
        if ret, ok := mem[[2]int{sc, pc}]; ok {
            return ret
        }
        // Note here that ans and mem are both passed by closure reference
        defer func() {
            mem[[2]int{sc, pc}] = ans
        }()

        if sc == ls && pc == lp {
            ans = true
            return
        }
    ...
    }

Share flash

Will the carrier of video and text affect the learning effect?

  • The advantage of text is convenient and fast search and location, the advantage of video is more vivid image, the gap between the understanding ability of the author and the reader is smaller, and the requirement of imagination is much lower.

The text may be more suitable for readers to think while obtaining the content, and more suitable for learning things that need deep understanding.

Tags: Go Redis Programming less

Posted on Mon, 15 Jun 2020 00:16:24 -0400 by char skd1