Her-Sheep LeetCode Algorithm Notes

website·Indexed 30 days ago

https://note.her-sheep.top

A collection of algorithm templates and problem solutions for LeetCode and other competitive programming platforms. Covers data structures and algorithms including Segment Trees, Fenwick Trees, KMP, Z-function, Suffix Array, Dijkstra, Floyd-Warshall, Union-Find, and various dynamic programming techniques like 01 Knapsack and Tree DP.

Tokens
18.8K
Snippets
70
Records
70
Agent score
47%

What's inside Her-Sheep LeetCode Notes

  1. Implement the Log Trick to find distinct subarray operation values

    The 'log trick' is an optimization for problems requiring the calculation of bitwise OR, AND, or GCD across all subarrays. Because these operations are monotonic (the result only changes a logarithmic number of times as the subarray grows), you can maintain a compressed list of distinct values and their corresponding boundaries instead of iterating through all $O(n^2)$ subarrays.

    This is particularly useful for counting how many subarrays result in a specific value or finding all unique values produced by these operations.

    // Generic implementation to count occurrences of each distinct value 
    // produced by a monotonic operation (like OR, AND, GCD) across all subarrays.
    func logTrick(nums []int, op func(x, y int) int) map[int]int {
        res := map[int]int{}
        type pair struct{ l, r, v int }
        dp := []pair{}
        for pos, cur := range nums {
            // Update existing subarray results with the current element
            for i := range dp {
                dp[i].v = op(dp[i].v, cur)
            }
            // Add the single-element subarray starting and ending at current position
            dp = append(dp, pair{pos, pos + 1, cur})
    
            // Merge adjacent subarrays that now have the same operation value
            ptr := 0
            for _, v := range dp[1:] {
                if dp[ptr].v != v.v {
                    ptr += 1
                    dp[ptr] = v
                } else {
                    dp[ptr].r = v.r
                }
            }
            dp = dp[:ptr+1]
            
            // Accumulate the count of subarrays for each distinct value
            for _, v := range dp {
                res[v.v] += v.r - v.l
            }
        }
        return res
    }
    
    // Example operation functions to use with logTrick
    func and(a, b int) int { return a & b }
    func or(a, b int) int  { return a | b }
    func gcd(a, b int) int {
        for a != 0 {
            a, b = b%a, a
        }
        return b
    }
  2. Calculate sliding window median using Red-Black Trees

    An alternative implementation for the sliding window median problem uses two Red-Black Trees (acting as ordered sets) to maintain the left and right halves of the window. This allows for efficient insertion, deletion, and retrieval of the minimum/maximum elements of each half to keep the window balanced and calculate the median.
    func medianSlidingWindow(nums []int, k int) [][2]int {
        ans := make([][2]int, len(nums)-k+1)
        var lSum, rSum, lSize, rSize int
        left := redblacktree.New[int, int]()
        right := redblacktree.New[int, int]()
    
        // Helper to handle duplicate values in the tree
        put := func(tr *redblacktree.Tree[int, int], x int) {
            c, ok := tr.Get(x)
            if ok { tr.Put(x, c+1) } else { tr.Put(x, 1) }
        }
        remove := func(tr *redblacktree.Tree[int, int], a int) {
            c, _ := tr.Get(a)
            if c == 1 { tr.Remove(a) } else { tr.Put(a, c-1) }
        }
        
        // Balance logic to move elements between left and right trees
        r2l := func() {
            val := right.Left().Key
            remove(right, val)
            put(left, val)
            lSum += val; rSum -= val; lSize++; rSize--
        }
        l2r := func() {
            val := left.Right().Key
            remove(left, val)
            put(right, val)
            rSum += val; lSum -= val; lSize--; rSize++
        }
        balance := func() {
            for lSize <= rSize { r2l() }
            for lSize > rSize+1 { l2r() }
        }
    
        for i, in := range nums {
            put(right, in)
            rSum += in; rSize++
            balance()
    
            l := i + 1 - k
            if l < 0 { continue }
    
            if k%2 > 0 {
                ans[l][0] = int(left.Right().Key)
            } else {
                ans[l][0] = int(right.Left().Key+left.Right().Key) / 2
            }
            ans[l][1] = ans[l][0]*(k&1) - lSum + rSum
    
            out := nums[l]
            if out <= left.Right().Key {
                remove(left, out); lSum -= out; lSize--
            } else {
                remove(right, out); rSum -= out; rSize--
            }
            balance()
        }
        return ans
    }
  3. Calculate sliding window median using Lazy Heaps

    This approach uses two heaps (a max-heap for the left half and a min-heap for the right half) to maintain the median of a sliding window. To handle elements leaving the window that are not at the heap top, a lazy-deletion mechanism is used. The heaps are balanced such that the left heap contains the smaller half of the elements and the right heap contains the larger half.
    func medianSlidingWindow(nums []int, k int) [][2]int {
        ans := make([][2]int, len(nums)-k+1)
        left := newLazyHeap()  // Max-heap (elements stored as negative)
        right := newLazyHeap() // Min-heap
    
        for i, in := range nums {
            // 1. Add new element to window
            if left.size == right.size {
                left.push(-right.pushPop(in))
            } else {
                right.push(-left.pushPop(-in))
            }
    
            l := i + 1 - k
            if l < 0 { continue }
    
            // 2. Calculate median
            if k%2 > 0 {
                ans[l][0] = int(-left.top())
            } else {
                ans[l][0] = int(right.top()-left.top()) / 2
            }
            ans[l][1] = ans[l][0]*(k&1) + left.sum + right.sum
    
            // 3. Remove element leaving the window
            out := nums[l]
            if out <= -left.top() {
                left.remove(-out)
                if left.size < right.size {
                    left.push(-right.pop())
                }
            } else {
                right.remove(out)
                if left.size > right.size+1 {
                    right.push(-left.pop())
                }
            }
        }
        return ans
    }
  4. Implement Binary Lifting for k-th ancestor lookup

    Binary lifting is used to precompute ancestors of nodes in a tree to allow $O(\log n)$ queries for the k-th ancestor. The core idea is to maintain a table pa[node][i] which stores the $2^i$-th ancestor of node.
    import "math/bits"
    
    // getPa precomputes the binary lifting table
    // n: number of nodes
    // parent: parent[i] is the direct parent of node i
    func getPa(n int, parent []int) [][]int {
        m := bits.Len(uint(n)) // Max power of 2 needed
        pa := make([][]int, n)
        for i, p := range parent {
            pa[i] = make([]int, m)
            pa[i][0] = p // 2^0 ancestor is the direct parent
        }
        for i := 0; i < m-1; i++ {
            for j := 0; j < n; j++ {
                if t := pa[j][i]; t != -1 {
                    // The 2^(i+1)-th ancestor is the 2^i-th ancestor of the 2^i-th ancestor
                    pa[j][i+1] = pa[t][i]
                } else {
                    pa[j][i+1] = -1
                }
            }
        }
        return pa
    }
  5. Calculate minimum cost to construct a string using AC Automaton

    To find the minimum cost to construct a target string using a set of available words with associated costs, use an AC automaton combined with dynamic programming. The DP state f[i] represents the minimum cost to construct the prefix of the target string of length i. For each character in the target string, traverse the AC automaton and check all matching patterns ending at the current position by following the last (suffix) links.
    func minimumCost(target string, words []string, costs []int) int {
        ac := &acam{root: &node{}}
        for i, w := range words {
            ac.put(w, costs[i])
        }
        ac.buildFail()
    
        n := len(target)
        f := make([]int, n+1)
        cur := ac.root
        for i, b := range target {
            cur = cur.son[b-'a']
            i++
            f[i] = math.MaxInt / 2
            if cur.len > 0 {
                f[i] = min(f[i], f[i-cur.len]+cur.cost)
            }
            // Traverse suffix links to find all matching shorter words
            for fail := cur.last; fail != ac.root; fail = fail.last {
                f[i] = min(f[i], f[i-fail.len]+fail.cost)
            }
        }
        if f[n] == math.MaxInt/2 {
            return -1
        }
        return f[n]
    }
  6. Construct a string with minimum cost using Go's suffixarray

    To solve the problem of constructing a target string with the minimum cost using a given set of words and their associated costs, you can use the index/suffixarray package in Go. The approach involves:

    1. Pre-processing the words to keep only the minimum cost for each unique word.
    2. Using suffixarray.New() on the target string to efficiently find all occurrences of each word via sa.Lookup().
    3. Building a dynamic programming (DP) array where dp[i] represents the minimum cost to construct the prefix of the target string of length i.
    4. Iterating through the target string and updating the DP state based on the occurrences of words that end at the current position.
    import (
    	"index/suffixarray"
    	"math"
    )
    
    func minimumCost(target string, words []string, costs []int) int {
    	minCost := map[string]uint16{}
    	for i, w := range words {
    		c := costs[i]
    		if minCost[w] == 0 {
    			minCost[w] = uint16(c)
    		} else {
    			minCost[w] = min(minCost[w], uint16(c))
    		}
    	}
    
    	n := len(target)
    	type pair struct{ l, cost uint16 }
    	from := make([][]pair, n+1)
    	sa := suffixarray.New([]byte(target))
    	for w, c := range minCost {
    		for _, l := range sa.Lookup([]byte(w), -1) {
    			r := l + len(w)
    			from[r] = append(from[r], pair{uint16(l), c})
    		}
    	}
    	const inf int = math.MaxInt / 2
    	dp := make([]int, n+1)
    	for i := 1; i <= n; i++ {
    		dp[i] = inf
    		for _, p := range from[i] {
    			dp[i] = min(dp[i], dp[p.l]+int(p.cost))
    		}
    	}
    	if dp[n] == inf {
    		return -1
    	}
    	return dp[n]
    }
  7. Find all primes up to N using the Sieve of Eratosthenes

    The Sieve of Eratosthenes is a classic algorithm to find all prime numbers up to a limit N. It works by iteratively marking the multiples of each prime number starting from 2 as composite (not prime).
    const N int = 1e5+1
    var primes [N]bool
    
    func init() {
        for i := 2; i < N; i++ {
            primes[i] = true
        }
        for i := 2; i < N; i++ {
            if primes[i] {
                for j := i * i; j < N; j += i {
                    primes[j] = false
                }
            }
        }
    }
  8. Implement a Fenwick Tree in Go for range sum queries

    A Fenwick Tree (Binary Indexed Tree) allows for updating elements and calculating prefix sums in O(log n) time. This implementation uses 1-based indexing for the underlying slice to simplify the bitwise operations used for navigation.
    type fenwick []int
    
    // newFenwickTree initializes a tree for n elements using indices 1 to n
    func newFenwickTree(n int) fenwick {
        return make(fenwick, n+1)
    }
    
    // update adds val to the element at index i (1 <= i <= n)
    // Time Complexity: O(log n)
    func (f fenwick) update(i int, val int) {
        for ; i < len(f); i += i & -i {
            f[i] += val
        }
    }
    
    // pre calculates the prefix sum from a[1] to a[i] (1 <= i <= n)
    // Time Complexity: O(log n)
    func (f fenwick) pre(i int) (res int) {
        for ; i > 0; i &= i - 1 {
            res += f[i]
        }
        return
    }
    
    // query calculates the range sum from a[l] to a[r] (1 <= l <= r <= n)
    // Time Complexity: O(log n)
    func (f fenwick) query(l, r int) int {
        if r < l {
            return 0
        }
        return f.pre(r) - f.pre(l-1)
    }
  9. Generate palindrome numbers up to 40,000 in Go

    This algorithm generates palindrome numbers by treating an integer i as the first half of the palindrome and mirroring it. It handles both odd-length palindromes (by skipping the middle digit during mirroring) and even-length palindromes (by mirroring all digits). In this specific implementation, iterating i from 1 to 399 generates palindromes up to approximately 40,000.
    pal := []int{}
    for i := 1; i < 400; i++ {
        p := i
        // Generate odd-length palindrome (length n * 2 - 1)
        // Skip the last digit of i when mirroring
        for x := i / 10; x > 0; x /= 10 {
            p = p*10 + x%10
        }
        pal = append(pal, p)
    
        if i < 100 {
            p := i
            // Generate even-length palindrome (length n * 2)
            // Mirror all digits of i
            for x := i; x > 0; x /= 10 {
                p = p*10 + x%10
            }
            pal = append(pal, p)
        }
    }
  10. Implement the Floyd-Warshall algorithm for all-pairs shortest paths

    The Floyd-Warshall algorithm computes the shortest paths between all pairs of vertices in a weighted graph. It uses a dynamic programming approach where the distance between two nodes i and j is updated if a shorter path exists through an intermediate node k. To prevent integer overflow during the addition of distances, initialize the distance matrix with a large value such as math.MaxInt / 2.
    func findTheCity(n int, edges [][]int, distanceThreshold int) (ans int) {
        w := make([][]int, n)
        for i := range w {
            w[i] = make([]int, n)
            for j := range w[i] {
                w[i][j] = math.MaxInt / 2 // Prevent overflow during addition
            }
        }
        for _, e := range edges {
            x, y, wt := e[0], e[1], e[2]
            w[x][y], w[y][x] = wt, wt
        }
    
        f := w
        for k := range f {
            for i := range f {
                for j := range f {
                    f[i][j] = min(f[i][j], f[i][k]+f[k][j])
                }
            }
        }
    
        minCnt := n
        for i, dis := range f {
            cnt := 0
            for j, d := range dis {
                if j != i && d <= distanceThreshold {
                    cnt++
                }
            }
            if cnt <= minCnt { // If counts are equal, the larger index i is preferred
                minCnt = cnt
                ans = i
            }
        }
        return ans
    }
  11. Implement a Segment Tree with Lazy Propagation in Go (Range Updates)

    Lazy propagation allows range updates in $O(\log n)$ time by deferring updates to child nodes until they are accessed. This implementation includes apply to update a node's value and lazy tag, and spread to push the tag to children.
    import "math/bits"
    
    const todoInit int = 0 // Initial value for lazy tag
    
    type lazySeg []struct {
        val  int
        todo int
    }
    
    func (lazySeg) mergeVal(a, b int) int  { return a + b }
    func (lazySeg) mergeTodo(a, b int) int { return a + b }
    
    func (t lazySeg) apply(node, l, r int, todo int) {
        cur := &t[node]
        cur.val += todo * (r - l + 1)
        cur.todo = t.mergeTodo(todo, cur.todo)
    }
    
    func (t lazySeg) spread(node, l, r int) {
        todo := t[node].todo
        if todo == todoInit { return }
        m := (l + r) / 2
        t.apply(node*2, l, m, todo)
        t.apply(node*2+1, m+1, r, todo)
        t[node].todo = todoInit
    }
    
    func (t lazySeg) maintain(node int) {
        t[node].val = t.mergeVal(t[node*2].val, t[node*2+1].val)
    }
    
    func (t lazySeg) build(a []int, node, l, r int) {
        t[node].todo = todoInit
        if l == r {
            t[node].val = a[l]
            return
        }
        m := (l + r) / 2
        t.build(a, node*2, l, m)
        t.build(a, node*2+1, m+1, r)
        t.maintain(node)
    }
    
    func (t lazySeg) update(node, l, r, ql, qr int, f int) {
        if ql <= l && r <= qr {
            t.apply(node, l, r, f)
            return
        }
        t.spread(node, l, r)
        m := (l + r) / 2
        if ql <= m { t.update(node*2, l, m, ql, qr, f) }
        if qr > m { t.update(node*2+1, m+1, r, ql, qr, f) }
        t.maintain(node)
    }
    
    func (t lazySeg) query(node, l, r, ql, qr int) int {
        if ql <= l && r <= qr { return t[node].val }
        t.spread(node, l, r)
        m := (l + r) / 2
        if qr <= m { return t.query(node*2, l, m, ql, qr) }
        if ql > m { return t.query(node*2+1, m+1, r, ql, qr) }
        return t.mergeVal(t.query(node*2, l, m, ql, qr), t.query(node*2+1, m+1, r, ql, qr))
    }
  12. Implement a basic Segment Tree in Go (Point Updates)

    A basic Segment Tree allows for point updates and range queries in $O(\log n)$ time. This implementation uses a 1-indexed array for tree nodes and 0-indexed for the underlying data range. To change the node indexing to 0-based, update child indices to node*2+1 and node*2+2.
    import "math/bits"
    
    type seg []struct {
        val int // Modify based on problem (e.g., sum, max, min)
    }
    
    func (seg) mergeVal(a, b int) int {
        return max(a, b) // Modify based on problem
    }
    
    func newSegmentTreeWithArray(a []int) seg {
        n := len(a)
        t := make(seg, 2<<bits.Len(uint(n-1)))
        t.build(a, 1, 0, n-1)
        return t
    }
    
    func (t seg) maintain(node int) {
        t[node].val = t.mergeVal(t[node*2].val, t[node*2+1].val)
    }
    
    func (t seg) build(a []int, node, l, r int) {
        if l == r {
            t[node].val = a[l]
            return
        }
        m := (l + r) / 2
        t.build(a, node*2, l, m)
        t.build(a, node*2+1, m+1, r)
        t.maintain(node)
    }
    
    func (t seg) update(node, l, r, i int, val int) {
        if l == r {
            t[node].val = t.mergeVal(t[node].val, val)
            return
        }
        m := (l + r) / 2
        if i <= m {
            t.update(node*2, l, m, i, val)
        } else {
            t.update(node*2+1, m+1, r, i, val)
        }
        t.maintain(node)
    }
    
    func (t seg) query(node, l, r, ql, qr int) int {
        if ql <= l && r <= qr {
            return t[node].val
        }
        m := (l + r) / 2
        if qr <= m {
            return t.query(node*2, l, m, ql, qr)
        }
        if ql > m {
            return t.query(node*2+1, m+1, r, ql, qr)
        }
        return t.mergeVal(t.query(node*2, l, m, ql, qr), t.query(node*2+1, m+1, r, ql, qr))
    }