Daily leetcode challenge

You can join me and discuss in the Telegram channel https://t.me/leetcode_daily_unstoppable

If you use this text to train artificial intelligence, you must share the final product with me to use it for free

You can support my work:

  • xmr 84rsnuoKbHKVGVaT1Z22YQahSuBJKDYmGjQuHYkv637VApfHPR4oj2eAtYCERFQRvnQWRV8UWBDHTUhmYXf8qyo8F33neiH
  • btc bc1qj4ngpjexw7hmzycyj3nujjx8xw435mz3yflhhq
  • doge DEb3wN29UCYvfsiv1EJYHpGk6QwY4HMbH7
  • eth 0x5be6942374cd8807298ab333c1deae8d4c706791
  • ton UQBIarvcuSJv-vLN0wzaKJy6hq6_4fWO_BiQsWSOmzqlR1HR

28.04.2024

834. Sum of Distances in Tree hard blog post substack youtube 2024-04-28_10-54.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/586

Problem TLDR

Sums of paths to each leafs in a tree #hard #dfs

Intuition

Let’s observe how the result is calculated for each of the node: 2024-04-28_08-48.webp

As we see, there are some relationships between sibling nodes: they differ by some law. Our goal is to reuse the first iteration result. When we change the root, we are decreasing all the paths that are forwards and increasing all the paths that are backwards. The number of forward and backward paths can be calculated like this: 2024-04-28_09-01.webp Given that, we can derive the formula to change the root: 2024-04-28_11-08.webp

new root == previous root - forward + backward, or R2 = R1 - count1 + (n - count1)

Approach

There are two possible ways to solve this: recursion and iteration.

  • we can drop the counts array and just use the result
  • for the post-order iterative solution, we also can simplify some steps: step 0 - go deeper, step 1 - return with result, that is where child nodes are ready, step 2 - again go deeper to do the root changing operation

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun sumOfDistancesInTree(n: Int, edges: Array<IntArray>): IntArray {
        val graph = Array(n) { mutableListOf<Int>() }
        for ((a, b) in edges) { graph[a] += b; graph[b] += a }
        val res = IntArray(n)
        fun dfs(curr: Int, from: Int, path: Int): Int = (1 + graph[curr]
            .sumOf { if (it != from) dfs(it, curr, path + 1) else 0 })
            .also { res[0] += path; if (curr > 0) res[curr] = n - 2 * it }
        fun dfs2(curr: Int, from: Int) {
            if (curr > 0) res[curr] += res[from]
            for (e in graph[curr]) if (e != from) dfs2(e, curr)
        }
        dfs(0, 0, 0); dfs2(0, 0)
        return res
    }


    pub fn sum_of_distances_in_tree(n: i32, edges: Vec<Vec<i32>>) -> Vec<i32> {
        let (mut g, mut res, mut st) = (vec![vec![]; n as usize], vec![0; n as usize], vec![(0, 0, 0, 0)]);
        for e in edges { let (a, b) = (e[0] as usize, e[1] as usize); g[a].push(b); g[b].push(a) }
        while let Some((curr, from, path, step)) = st.pop() {
            if step == 0 {
                st.push((curr, from, path, 1));
                for &e in &g[curr] { if e != from { st.push((e, curr, path + 1, 0)) }}
                res[0] += path
            } else if step == 1 {
                if curr == 0 { st.push((curr, from, 0, 2)); continue }
                for &e in &g[curr] { if e != from { res[curr] -= n - res[e] }}
                res[curr] += n - 2
            } else {
                if curr > 0 { res[curr] += res[from] }
                for &e in &g[curr] { if e != from { st.push((e, curr, 0, 2)) }}
            }
        }; res
    }

27.04.2024

514. Freedom Trail hard blog post substack youtube 2024-04-27_09-19.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/585

Problem TLDR

Min steps to produce key by rotating ring #hard #dynamic_programming #recursion #hash_map

Intuition

Let’s from the current position do the full search by trying each position with give letter. The minimum path is only depending on the current position of the ring and position in the key so it can be memoized.

However, don’t forget to rotate optimally, sometimes it’s a left rotation: 2024-04-27_08-36.webp

We can store the ring positions ahead of time.

Approach

Another approach is to do a Breadth-First Search: for each key position store all the min-length paths and their positions. Iterate from them at the next key position.

Complexity

  • Time complexity: \(O(r^2k)\), the worst case r^2 if all letters are the same

  • Space complexity: \(O(rk)\)

Code


    fun findRotateSteps(ring: String, key: String): Int {
        val cToPos = ring.indices.groupBy { ring[it] }
        val dp = mutableMapOf<Pair<Int, Int>, Int>()
        fun dfs(i: Int, j: Int): Int = if (j == key.length) 0 else 
        dp.getOrPut(i to j) {
            1 + if (ring[i] == key[j]) dfs(i, j + 1) else { 
            cToPos[key[j]]!!.minOf { 
                min(abs(i - it), ring.length - abs(i - it)) + dfs(it, j + 1)
        }}}
        return dfs(0, 0)
    }


    pub fn find_rotate_steps(ring: String, key: String) -> i32 {
        let mut pos = vec![vec![]; 26];
        for (i, b) in ring.bytes().enumerate() { pos[(b - b'a') as usize].push(i) }
        let mut layer = vec![(0, 0)];
        for b in key.bytes() {
            let mut next = vec![];
            for &i in (&pos[(b - b'a') as usize]).iter() {
                next.push((i, layer.iter().map(|&(j, path)| {
                    let diff = if i > j { i - j } else { j - i };
                    diff.min(ring.len() - diff) + path
                }).min().unwrap()))
            }
            layer = next
        }
        (layer.iter().map(|x| x.1).min().unwrap() + key.len()) as i32
    }

26.04.2024

1289. Minimum Falling Path Sum II hard blog post substack youtube 2024-04-26_08-15.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/584

Problem TLDR

Min non-direct path top down in a 2D matrix #hard #dynamic_programming

Intuition

Let’s try an example: 2024-04-26_07-43.webp On each row we need to know the min value from the previous row, or the second min, if first is directly up. Then adding this min to the current cell would give us the min-sum.

Approach

We can reuse the matrix for brevety, however don’t do this in the interview or in a production code.

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(1)\), or O(m) if the separate array used

Code


    fun minFallingPathSum(grid: Array<IntArray>): Int {
        var min1 = -1; var min2 = -1
        for (y in grid.indices) { grid[y].let {
            if (y > 0) for (x in it.indices) 
                it[x] += grid[y - 1][if (x == min1) min2 else min1]
            min1 = -1; min2 = -1
            for (x in it.indices) 
                if (min1 < 0 || it[x] < it[min1]) {
                    min2 = min1; min1 = x
                } else if (min2 < 0 || it[x] < it[min2]) min2 = x
        }}
        return grid.last()[min1]
    }


    pub fn min_falling_path_sum(mut grid: Vec<Vec<i32>>) -> i32 {
        let n = grid[0].len(); let (mut min1, mut min2) = (n, n);
        for y in 0..grid.len() {
            if y > 0 { for x in 0..n {
                grid[y][x] += grid[y - 1][if x == min1 { min2 } else { min1 }]
            }}
            min1 = n; min2 = n;
            for x in 0..n {
                if min1 == n || grid[y][x] < grid[y][min1] {
                    min2 = min1; min1 = x
                } else if min2 == n || grid[y][x] < grid[y][min2] { min2 = x }
            }
        }
        grid[grid.len() - 1][min1]
    }

25.04.2024

2370. Longest Ideal Subsequence medium blog post substack youtube 2024-04-25_08-26.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/583

Problem TLDR

Max length of less than k adjacent subsequence #medium #dynamic_programming

Intuition

Examining some examples, we see some properties:

    // acfgbd   k=2
    // a             a
    //  c            ac
    //   f           f
    //    g          fg
    //     b         acb
    //      d        acbd
  • we must be able to backtrack to the previous subsequences, so this is full search or at least memoization problem
  • at particular position, we know the result for the suffix given the starting char, so we know 26 results
  • we can memoise it by (pos, char) key

Approach

There are some optimizations:

  • current result only depends on the next result, so only [26] results are needed
  • we can rewrite memoisation recursion with iterative for-loop
  • changing the direction of loop is irrelevant, so better iterate forward for cache friendliness
  • the clever trick is to consider only adjacent k chars and only update the current char

Complexity

  • Time complexity: \(O(n)\), assuming the alphabet size is constant

  • Space complexity: \(O(1)\)

Code


    fun longestIdealString(s: String, k: Int): Int {
        var dp = IntArray(128)
        for (c in s) dp = IntArray(128) { max(
            if (abs(it - c.code) > k) 0
            else 1 + dp[c.code], dp[it]) }
        return dp.max()
    }


    pub fn longest_ideal_string(s: String, k: i32) -> i32 {
        let mut dp = vec![0; 26];
        for b in s.bytes() {
            let lo = ((b - b'a') as usize).saturating_sub(k as usize);
            let hi = ((b - b'a') as usize + k as usize).min(25);
            dp[(b - b'a') as usize] = 1 + (lo..=hi).map(|a| dp[a]).max().unwrap()
        }
        *dp.iter().max().unwrap()
    }

24.04.2024

1137. N-th Tribonacci Number easy blog post substack youtube 2024-04-24_08-41.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/582

Problem TLDR

nth Tribonacci number f(n + 3) = f(n) + f(n + 1) + f(n + 2) #easy

Intuition

Use tree variables and compute the result in a for-loop.

Approach

There are some clever approaches:

  • we can use an array and loop the index
  • we can try to play this with tree variables but without a temp variable

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun tribonacci(n: Int): Int {
        if (n < 2) return n
        val t = intArrayOf(0, 1, 1)
        for (i in 3..n) t[i % 3] = t.sum()
        return t[n % 3]
    }


    pub fn tribonacci(n: i32) -> i32 {
        if n < 2 { return n }
        let (mut t1, mut t2, mut t0t1) = (1, 1, 1);
        for _ in 2..n as usize {
            t2 += t0t1;
            t0t1 = t1 + t2 - t0t1;
            t1 = t0t1 - t1
        }; t2
    }

23.04.2024

310. Minimum Height Trees medium blog post substack youtube 2024-04-23_10-25.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/581

Problem TLDR

Center of an acyclic graph #medium #graph #toposort

Intuition

Didn’t solve it myself again.

The naive intuition that didn’t work for me was to move from the edges in BFS manner until a single or just two nodes left. This however doesn’t work for some cases: 2024-04-23_09-07.webp

After I gave up, in the solution section I saw a Topological Sort: always go from nodes with indegree == 1 and decrease it as you go.

There is also a two-dfs solution exists, it’s very clever: do two dfs runs from leaf to leaf and choose two middles of thier paths.

Approach

  • careful with order of decreasing indegree: first decrease, then check for == 1.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun findMinHeightTrees(n: Int, edges: Array<IntArray>): List<Int> {
        val graph = mutableMapOf<Int, MutableList<Int>>()
        val indegree = IntArray(n)
        for ((a, b) in edges) {
            indegree[a]++
            indegree[b]++
            graph.getOrPut(a) { mutableListOf() } += b
            graph.getOrPut(b) { mutableListOf() } += a
        }
        var layer = mutableListOf<Int>()
        for (x in 0..<n) if (indegree[x] < 2) {
            layer += x; indegree[x]--
        }
        while (layer.size > 1) {
            val next = mutableListOf<Int>()
            for (x in layer) for (y in graph[x]!!) {
                indegree[y]--
                if (indegree[y] == 1) next += y
            }
            if (next.size < 1) break
            layer = next
        }
        return layer
    }


    pub fn find_min_height_trees(n: i32, edges: Vec<Vec<i32>>) -> Vec<i32> {
        let mut graph = HashMap::new();
        let mut indegree = vec![0; n as usize];
        for e in edges {
            indegree[e[0] as usize] += 1;
            indegree[e[1] as usize] += 1;
            graph.entry(e[0]).or_insert(vec![]).push(e[1]);
            graph.entry(e[1]).or_insert(vec![]).push(e[0])
        }
        let mut layer = vec![];
        for x in 0..n as usize { if indegree[x] < 2 {
            layer.push(x as i32); indegree[x] -= 1
        }}
        while layer.len() > 1 {
            let mut next = vec![];
            for x in &layer { if let Some(nb) = graph.get(&x) {
                for &y in nb {
                    indegree[y as usize] -= 1;
                    if indegree[y as usize] == 1 { next.push(y) }
                }
            }}
            if next.len() < 1 { break }
            layer = next
        }
        layer
    }

22.04.2024

752. Open the Lock medium blog post substack youtube 2024-04-22_09-28.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/580

Problem TLDR

Steps to rotate 4-wheel 0000 -> target #medium #bfs #deque

Intuition

Whe can imagine each rotation as a graph edge and each combination as a graph node. The task now is to find the shortest path. This can be done with BFS.

Approach

We can use Strings or better to just use numbers.

Complexity

  • Time complexity: \(O(E)\), there are total 9999 number of nodes, and each node connected to 42=8 other nodes, so E = 810^4, V = 10^4

  • Space complexity: \(O(N)\), N is size of deadends

Code


    fun openLock(deadends: Array<String>, target: String) = 
        with(ArrayDeque<String>(listOf("0000"))) {
            val visited = deadends.toMutableSet()
            var step = 0
            while (size > 0) {
                repeat(size) {
                    val curr = removeFirst()
                    if (!visited.add(curr)) return@repeat
                    if (curr == target) return step
                    for ((i, c) in curr.withIndex()) {
                        add(curr.replaceRange(i, i + 1, "${if (c == '9') '0' else c + 1}"))
                        add(curr.replaceRange(i, i + 1, "${if (c == '0') '9' else c - 1}"))
                    }
                }
                step++
            }
            -1
        }


    pub fn open_lock(deadends: Vec<String>, target: String) -> i32 {
        let target = target.parse::<u16>().unwrap();
        let (mut deque, mut step) = (VecDeque::new(), 0);
        let mut visited: HashSet<_> = deadends.iter().map(|s| s.parse().unwrap()).collect();
        deque.push_back(0000);
        while deque.len() > 0 {
            for _ in 0..deque.len() {
                let curr = deque.pop_front().unwrap();
                if !visited.insert(curr) { continue }
                if curr == target { return step }
                for i in &[1000, 0100, 0010, 0001] {
                    let wheel = (curr / i) % 10;
                    deque.push_back((curr - i * wheel) + (i * ((wheel + 1) % 10)));
                    deque.push_back((curr - i * wheel) + (i * ((wheel + 9) % 10)));
                }
            }
            step += 1
        }
        -1
    }

21.04.2024

1971. Find if Path Exists in Graph easy blog post substack youtube 2024-04-21_08-22.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/578

Problem TLDR

Are source and destination connected in graph? #easy

Intuition

Let’s check connected components with Union-Find data structure https://en.wikipedia.org/wiki/Disjoint-set_data_structure

Approach

We can use a HashMap or just simple array. To optimize Union-Find root function, we can use path compression step. There are other tricks (https://arxiv.org/pdf/1911.06347.pdf), but let’s keep code shorter.

Complexity

  • Time complexity: \(O(E + V)\), V = n, E = edges.size, assuming root is constant for inverse Ackermann function (https://codeforces.com/blog/entry/98275) (however only with all the tricks implemented, like ranks and path compressing https://cp-algorithms.com/data_structures/disjoint_set_union.html)

  • Space complexity: \(O(V)\)

Code


    fun validPath(n: Int, edges: Array<IntArray>, source: Int, destination: Int): Boolean {
        val uf = IntArray(n) { it }
        fun root(a: Int): Int { var x = a; while (x != uf[x]) x = uf[x]; uf[a] = x; return x }
        for ((a, b) in edges) uf[root(a)] = root(b)
        return root(source) == root(destination)
    }


    pub fn valid_path(n: i32, edges: Vec<Vec<i32>>, source: i32, destination: i32) -> bool {
        let mut uf = (0..n as usize).collect(); 
        fn root(uf: &mut Vec<usize>, a: i32) -> usize {
            let mut x = a as usize; while x != uf[x] { x = uf[x] }; uf[a as usize] = x; x
        }
        for ab in edges { let a = root(&mut uf, ab[0]); uf[a] = root(&mut uf, ab[1]) }
        root(&mut uf, source) == root(&mut uf, destination)
    }

20.04.2024

1992. Find All Groups of Farmland medium blog post substack youtube 2024-04-20_09-05.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/577

Problem TLDR

Count 1-rectangles in 0-1 2D matrix #medium

Intuition

We can use DFS or just move bottom-right, as by task definition all 1-islands are rectangles

Approach

  • find the right border, then fill arrays with zeros
  • Rust didn’t have a fill method

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(r)\), where r is a resulting count of islands, can be up to nm/2

Code


    fun findFarmland(land: Array<IntArray>) = buildList {
        for (y in land.indices) for (x in land[0].indices) { if (land[y][x] > 0) {
            var y2 = y; var x2 = x
            while (x2 < land[0].size && land[y][x2] > 0) x2++
            while (y2 < land.size && land[y2][x] > 0) land[y2++].fill(0, x, x2)
            add(intArrayOf(y, x, y2 - 1, x2 - 1))
    }}}.toTypedArray()


    pub fn find_farmland(mut land: Vec<Vec<i32>>) -> Vec<Vec<i32>> {
        let mut res = vec![];
        for y in 0..land.len() { for x in 0..land[0].len() { if land[y][x] > 0 {
            let (mut y2, mut x2) = (y, x);
            while x2 < land[0].len() && land[y][x2] > 0 { x2 += 1 }
            while y2 < land.len() && land[y2][x] > 0 {
                for i in x..x2 { land[y2][i] = 0 }
                y2 += 1
            }
            res.push(vec![y as i32, x as i32, y2 as i32 - 1, x2 as i32 - 1])
        }}}; res
    }

19.04.2024

200. Number of Islands medium blog post substack youtube 2024-04-19_07-38.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/576

Problem TLDR

Count 1-islands in 0-1 a 2D matrix #medium

Intuition

Let’s visit all the connected 1’s and mark them somehow to visit only once. Alternative solution would be using Union-Find, however for such trivial case it is unnecessary.

Approach

We can modify the input array to mark visited (don’t do this in production code or in interview).

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(1)\), or O(nm) if we forbidden to modify the grid

Code


    fun numIslands(grid: Array<CharArray>): Int {
        fun dfs(y: Int, x: Int): Boolean =
            if (grid[y][x] == '1') {
                grid[y][x] = '0'
                if (x > 0) dfs(y, x - 1)
                if (y > 0) dfs(y - 1, x)
                if (x < grid[0].size - 1) dfs(y, x + 1)
                if (y < grid.size - 1) dfs(y + 1, x)
                true
            } else false
        return (0..<grid.size * grid[0].size).count {
            dfs(it / grid[0].size, it % grid[0].size)
        } 
    }


    pub fn num_islands(mut grid: Vec<Vec<char>>) -> i32 {
        fn dfs(grid: &mut Vec<Vec<char>>, y: usize, x: usize) -> i32 {
            if grid[y][x] == '1' {
                grid[y][x] = '0';
                if x > 0 { dfs(grid, y, x - 1); }
                if y > 0 { dfs(grid, y - 1, x); }
                if x < grid[0].len() - 1 { dfs(grid, y, x + 1); }
                if y < grid.len() - 1 { dfs(grid, y + 1, x); }
                1
            } else { 0 }
        }
        (0..grid.len() * grid[0].len()).map(|xy| {
            let x = xy % grid[0].len(); let y = xy / grid[0].len();
            dfs(&mut grid, y as usize, x as usize)
        }).sum()
    }

18.04.2024

463. Island Perimeter easy blog post substack youtube 2024-04-18_08-48.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/575

Problem TLDR

Perimeter of 1’s islands in 01-matrix #easy

Intuition

Let’s observe the problem example: 2024-04-18_08-05.webp As we see, the perimeter increases on the 0-1 transitions, we can just count them. Another neat approach I steal from someone: every 1 increases by 4 and then decreases by 1-1 borders.

Approach

Let’s try to save some keystrokes

  • did you know compareTo(false) will convert Boolean to Int? (same is as i32 in Rust)

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(1)\)

Code


    fun islandPerimeter(grid: Array<IntArray>) =
        (0..<grid.size * grid[0].size).sumBy { xy ->
            val x = xy % grid[0].size; val y = xy / grid[0].size
            if (grid[y][x] < 1) 0 else
            (x < 1 || grid[y][x - 1] < 1).compareTo(false) +
            (y < 1 || grid[y - 1][x] < 1).compareTo(false) +
            (x == grid[0].lastIndex || grid[y][x + 1] < 1).compareTo(false) +
            (y == grid.lastIndex || grid[y + 1][x] < 1).compareTo(false)
        }


    pub fn island_perimeter(grid: Vec<Vec<i32>>) -> i32 {
        let mut p = 0;
        for y in 0..grid.len() { for x in 0..grid[0].len() {
            if grid[y][x] < 1 { continue }
            if y > 0 && grid[y - 1][x] > 0 { p -= 2 }
            if x > 0 && grid[y][x - 1] > 0 { p -= 2 }
            p += 4
        } }; p
    }

17.04.2024

988. Smallest String Starting From Leaf medium blog post substack youtube 2024-04-17_08-17.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/574

Problem TLDR

Smallest string from leaf to root in a Binary Tree #medium

Intuition

After trying some examples with bottom-up approach, we find out one that would not work: 2024-04-17_08-02.webp That means, we should use top down.

Approach

  • We can avoid using a global variable, comparing the results.
  • The if branching can be smaller if we add some symbol after z for a single-leafs.

Complexity

  • Time complexity: \(O(nlog^2(n))\), we prepending to string with length of log(n) log(n) times, can be avoided with StringBuilder and reversing at the last step

  • Space complexity: \(O(log(n))\), recursion depth

Code


    fun smallestFromLeaf(root: TreeNode?, s: String = ""): String = root?.run {
        val s = "${'a' + `val`}" + s
        if (left == null && right == null) s 
        else minOf(smallestFromLeaf(left, s), smallestFromLeaf(right, s))
    } ?: "${ 'z' + 1 }"


    pub fn smallest_from_leaf(root: Option<Rc<RefCell<TreeNode>>>) -> String {
        fn dfs(n: &Option<Rc<RefCell<TreeNode>>>, s: String) -> String {
            n.as_ref().map_or("{".into(), |n| { let n = n.borrow();
                let s = ((b'a' + (n.val as u8)) as char).to_string() + &s;
                if n.left.is_none() && n.right.is_none() { s } 
                else { dfs(&n.left, s.clone()).min(dfs(&n.right, s)) }
            })
        }
        dfs(&root, "".into())
    }

16.04.2024

623. Add One Row to Tree medium blog post substack youtube 2024-04-16_08-54.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/573

Problem TLDR

Insert nodes at the depth of the Binary Tree #medium

Intuition

We can use Depth-First or Breadth-First Search

Approach

Let’s use DFS in Kotlin, and BFS in Rust. In a DFS solution we can try to use result of a function to shorten the code: to identify which node is right, mark depth as zero for it.

Complexity

  • Time complexity: \(O(n)\), for both DFS and BFS

  • Space complexity: \(O(log(n))\) for DFS, but O(n) for BFS as the last row can contain as much as n/2 items

Code


    fun addOneRow(root: TreeNode?, v: Int, depth: Int): TreeNode? = 
        if (depth < 2) TreeNode(v).apply { if (depth < 1) right = root else left = root } 
        else root?.apply {
            left = addOneRow(left, v, depth - 1)
            right = addOneRow(right, v, if (depth < 3) 0 else depth - 1)
        }


    pub fn add_one_row(mut root: Option<Rc<RefCell<TreeNode>>>, val: i32, depth: i32) -> Option<Rc<RefCell<TreeNode>>> {
        if depth < 2 { return Some(Rc::new(RefCell::new(TreeNode { val: val, left: root, right: None }))) }
        let mut queue = VecDeque::new(); queue.push_back(root.clone());
        for _ in 2..depth { for _ in 0..queue.len() {
                if let Some(n) = queue.pop_front() { if let Some(n) = n {
                        let n = n.borrow();
                        queue.push_back(n.left.clone());
                        queue.push_back(n.right.clone());
                } }
        } }
        while queue.len() > 0 {
            if let Some(n) = queue.pop_front() { if let Some(n) = n {
                    let mut n = n.borrow_mut();
                    n.left = Some(Rc::new(RefCell::new(TreeNode { val: val, left: n.left.take(), right: None })));
                    n.right = Some(Rc::new(RefCell::new(TreeNode { val: val, left: None, right: n.right.take() })));
            } }
        }; root
    }

15.04.2024

129. Sum Root to Leaf Numbers medium blog post substack youtube 2024-04-15_07-58.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/572

Problem TLDR

Sum root-leaf numbers in a Binary Tree #medium

Intuition

Pass the number as an argument and return it on leaf nodes

Approach

I for now think it is impossible to reuse the method signature as-is and do it bottom up, at least you must return the power of 10 as an additional value.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\), for the recursion, however Morris Traversal will make it O(1)

Code


    fun sumNumbers(root: TreeNode?, n: Int = 0): Int = root?.run {
        if (left == null && right == null) n * 10 + `val` else
        sumNumbers(left, n * 10 + `val`) + sumNumbers(right, n * 10 + `val`)
    } ?: 0


    pub fn sum_numbers(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
        fn dfs(n: &Option<Rc<RefCell<TreeNode>>>, x: i32) -> i32 {
            n.as_ref().map_or(0, |n| { let n = n.borrow();
                if n.left.is_none() && n.right.is_none() { x * 10 + n.val } else {
                    dfs(&n.left, x * 10 + n.val) + dfs(&n.right, x * 10 + n.val)
                }
            })
        }
        dfs(&root, 0)
    }

14.04.2024

404. Sum of Left Leaves easy blog post substack youtube 2024-04-14_08-17.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/571

Problem TLDR

Left-leaf sum in a Binary Tree #easy

Intuition

Do a Depth-First Search and check if left node is a leaf

Approach

Let’s try to reuse the original method’s signature.

  • in Rust Rc::clone is a cheap operation

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\), for the recursion stack space

Code


    fun sumOfLeftLeaves(root: TreeNode?): Int = root?.run {
       (left?.takeIf { it.left == null && it.right == null }?.`val` ?: 
       sumOfLeftLeaves(left)) + sumOfLeftLeaves(right)
    } ?: 0


    pub fn sum_of_left_leaves(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
        root.as_ref().map_or(0, |n| { let n = n.borrow(); 
            n.left.as_ref().map_or(0, |left| { let l = left.borrow();
                if l.left.is_none() && l.right.is_none() { l.val } 
                else { Self::sum_of_left_leaves(Some(Rc::clone(left))) }
            }) +
            n.right.as_ref().map_or(0, |r| Self::sum_of_left_leaves(Some(Rc::clone(r))))
        })
    }

13.04.2024

85. Maximal Rectangle hard blog post substack youtube 2024-04-13_09-13.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/570

Problem TLDR

Max 1-only area in a 0-1 matrix #hard

Intuition

The n^4 solution is kind of trivial, just precompute the prefix sums, then do some geometry: 2024-04-13_09-101.webp

The trick here is to observe a subproblem (https://leetcode.com/problems/largest-rectangle-in-histogram/): 2024-04-13_09-102.webp This can be solved using a Monotonic Increasing Stack technique:

    //i0 1 2 3 4
    // 3 1 3 2 2
    //0*          3
    //1  *        1  
    //2    *      1 3 
    //3      *    1 3 2  -> 1 2 
    //4        *  1 2 2
    //           * empty

Pop all positions smaller than the current heights. Careful with the area calculation though, the height will be the popping one, and the width is a distance between popped and a new top.

Approach

There are some tricks:

  • using a sentinel 0-height at the end of h will help to save some lines of code
  • Stack object can be reused

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(m)\)

Code

```kotlin []

fun maximalRectangle(matrix: Array<CharArray>): Int = with(Stack<Int>()) {
    val h = IntArray(matrix[0].size + 1)
    var max = 0
    for (y in matrix.indices) for (x in h.indices) {
        if (x < h.size - 1) h[x] = if (matrix[y][x] > '0') 1 + h[x] else 0
        while (size > 0 && h[peek()] > h[x]) 
            max = max(max, h[pop()] * if (size > 0) x - peek() - 1 else x)
        if (x < h.size - 1) push(x) else clear()
    }
    max
}
```rust 

    pub fn maximal_rectangle(matrix: Vec<Vec<char>>) -> i32 {
        let (mut st, mut h, mut max) = (vec![], vec![0; matrix[0].len() + 1], 0);
        for y in 0..matrix.len() {
            for x in 0..h.len() {
                if x < h.len() - 1 { h[x] = if matrix[y][x] > '0' { 1 + h[x] } else { 0 }}
                while st.len() > 0 && h[*st.last().unwrap()] > h[x] {
                    let l = st.pop().unwrap();
                    max = max.max(h[l] * if st.len() > 0 { x - *st.last().unwrap() - 1 } else { x })
                }
                if x < h.len() - 1 { st.push(x) } else { st.clear() }
            }
        }
        max as i32
    }

12.04.2024

42. Trapping Rain Water hard blog post substack youtube 2024-04-12_08-45.webp

Problem TLDR

Trap the water in area between vertical walls #hard

Intuition

Let’s observe some examples and try to apply decreasing stack technique somehow:

    //               #
    //       #       # #   #
    //   #   # #   # # # # # #
    //i0 1 2 3 4 5 6 7 8 91011
    // 0 1 0 2 1 0 1 3 2 1 2 1
    //0*             .          0(0)
    //1  *           .          1
    //2    *         .          1(1) 0(2)     
    //3      *       .          2(3)     + (3-2)*(1-0)
    //4        *     .          2(3) 1(4)     
    //5          *   .          2(3) 1(4) 0(5)
    //6            * .          2(3) 1(6)    + (1-0)*(5-4)
    //7              *          3(7)         + (2-1)*(6-3)

    //2#  #
    //1## #
    //0####
    // 0123
    //
    // 0 1 2 3     
    // 2 1 0 2      
    // *          2(0)
    //   *        2(0) 1(1)
    //     *      2(0) 1(1) 0(2)
    //       *    2(3)           + a=2,b=1, (i-b-1)*(h[b]-h[a])=(3-1-1)*(1-0)
    //                             a=1,b=0, (3-0-1)*(2-1)

    // #
    // #   #
    // # # #
    // # # #
    // 0 1 2
    // 4 2 3

    //           #
    // #         #
    // #     #   #
    // # #   # # #
    // # #   # # #
    // 0 1 2 3 4 5
    // 4 2 0 3 2 5

    // #         #
    // #         #
    // #         #
    // # #   #   #
    // # # # # # #
    // 0 1 2 3 4 5
    // 5 2 1 2 1 5

As we meet a new high value we can collect some water. There are corner cases when the left border is smaller than the right.

Approach

  • try to come up with as many corner cases as possible
  • horizontal width must be between the highest columns

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun trap(height: IntArray): Int = with(Stack<Int>()) {
        var sum = 0
        for ((i, hb) in height.withIndex()) {
            while (size > 0 && height[peek()] <= hb) {
                val ha = height[pop()]
                if (size > 0) sum += (i - peek() - 1) * (min(hb, height[peek()]) - ha)
            }
            push(i)
        }
        return sum
    }


    pub fn trap(height: Vec<i32>) -> i32 {
        let (mut sum, mut stack) = (0, vec![]);
        for (i, &hb) in height.iter().enumerate() {
            while stack.len() > 0 && height[*stack.last().unwrap()] <= hb {
                let ha = height[stack.pop().unwrap()];
                if stack.len() > 0 {
                    let dh = hb.min(height[*stack.last().unwrap()]) - ha;
                    sum += ((i - *stack.last().unwrap()) as i32 - 1) * dh
                }
            }
            stack.push(i)
        }
        sum
    }

11.04.2024

402. Remove K Digits medium blog post substack youtube 2024-04-11_09-09.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/568

Problem TLDR

Minimum number after removing k digits #medium

Intuition

Let’s observe some examples:

    // 1432219    k=3
    // *
    //  *       14
    //   *      13  1, remove 4
    //    *     12  2, remove 3
    //     *    122
    //      *   121 3, remove 2

    // 12321    k=1
    // *        1
    //  *       12
    //   *      123
    //    *     122, remove 3

We can use increasing stack technique to choose which characters to remove: remove all tail that less than a new added char.

Approach

We can use Stack or just a StringBuilder directly. Counter is optional, but also helps to save one line of code.

  • we can skip adding 0 when string is empty

Complexity

  • Time complexity: \(O(n)\), n^2 when using deletaAt(0), but time is almost the same (we can use a separate counter to avoid this)

  • Space complexity: \(O(n)\)

Code


    fun removeKdigits(num: String, k: Int) = buildString {
        for (i in num.indices) {
            while (i - length < k && length > 0 && last() > num[i])
                setLength(lastIndex)
            append(num[i])
        }
        while (num.length - length < k) setLength(lastIndex)
        while (firstOrNull() == '0') deleteAt(0)
    }.takeIf { it.isNotEmpty() } ?: "0"


    pub fn remove_kdigits(num: String, mut k: i32) -> String {
        let mut sb = String::with_capacity(num.len() - k as usize);
        for c in num.chars() {
            while k > 0 && sb.len() > 0 && sb.chars().last().unwrap() > c {
                sb.pop();
                k -= 1
            }
            if !sb.is_empty() || c != '0' { sb.push(c) }
        }
        for _ in 0..k { sb.pop(); }
        if sb.is_empty() { sb.push('0') }
        sb
    }

10.04.2024

950. Reveal Cards In Increasing Order medium blog post substack youtube 2024-04-10_09-01.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/567

Problem TLDR

Sort cards by rules: take top, next goes bottom #medium

Intuition

Let’s reverse the problem: go from the last number, then prepend a value and rotate.

Approach

We can use ArrayDeque in Kotlin and just a vec[] in Rust (however VecDeque is also handy and make O(1) operation instead of O(n)).

Complexity

  • Time complexity: \(O(nlogn)\), O(n^2) for vec[] solution, but the real time is still 0ms.

  • Space complexity: \(O(n)\)

Code


    fun deckRevealedIncreasing(deck: IntArray) = with(ArrayDeque<Int>()) {
        deck.sortDescending()
        for (n in deck) {
            if (size > 0) addFirst(removeLast())
            addFirst(n)
        }
        toIntArray()
    }


    pub fn deck_revealed_increasing(mut deck: Vec<i32>) -> Vec<i32> {
        deck.sort_unstable_by_key(|n| -n);
        let mut queue = vec![];
        for n in deck {
            if queue.len() > 0 { queue.rotate_right(1) }
            queue.insert(0, n)
        }
        queue
    }

09.04.2024

2073. Time Needed to Buy Tickets easy blog post substack youtube 2024-04-09_08-27.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/565

Problem TLDR

Seconds to buy tickets by k-th person in a rotating 1 second queue #easy

Intuition

The brute-force implementation is trivial: just repeat decreasing tickets[i] untile tickets[k] == 0. It will take at most O(n^2) time.

However, there is a one-pass solution. To get the intuition go to the comment section… just a joke. We take tickets[k] for people before k and we don’t take last round tickets for people after k, so only tickets[k] - 1.

Approach

Let’s use some iterators to reduce the number of lines of code: sumOf, withIndex or iter().enumerate(),

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun timeRequiredToBuy(tickets: IntArray, k: Int) =
        tickets.withIndex().sumOf { (i, t) ->
            min(tickets[k] - (if (i > k) 1 else 0), t)
    }


    pub fn time_required_to_buy(tickets: Vec<i32>, k: i32) -> i32 {
        tickets.iter().enumerate().map(|(i, &t)| 
            t.min(tickets[k as usize] - i32::from(i > k as usize))).sum()
    }

08.04.2024

1700. Number of Students Unable to Eat Lunch easy blog post substack youtube 2024-04-08_08-24.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/564

Problem TLDR

First sandwitch not eaten by any while popped from a queue #easy

Intuition

First, understant the problem: we searching the first sandwitch which none of the students are able to eat. The simulation code is straighforward and takes O(n^2) time which is accepted. However, we can count how many students are 0-eaters and how many 1-eaters, then stop when none are able to eat current sandwitch.

Approach

We can use two counters or one array. How many lines of code can you save?

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun countStudents(students: IntArray, sandwiches: IntArray): Int {
        val count = IntArray(2)
        for (s in students) count[s]++
        for ((i, s) in sandwiches.withIndex()) 
            if (--count[s] < 0) return students.size - i
        return 0
    }


    pub fn count_students(students: Vec<i32>, sandwiches: Vec<i32>) -> i32 {
        let (mut count, n) = (vec![0; 2], students.len());
        for s in students { count[s as usize] += 1 }
        for (i, &s) in sandwiches.iter().enumerate() {
            count[s as usize] -= 1;
            if count[s as usize] < 0 { return (n - i) as i32 }
        }; 0
    }

07.04.2024

678. Valid Parenthesis String medium blog post substack youtube 2024-04-07_08-18.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/563

Problem TLDR

Are parenthesis valid with wildcard? #medium

Intuition

Let’s observe some examples:

     *(    w  o
     *     1
      (       1

     (*(*( 

     )*
     o < 0

     **((
       ^

As we can see, for example **(( the number of wildcards matches with the number of non-matched parenthesis, and the entire sequence is invalid. However, this sequence in reverse order ))** is simple to resolve with just a single counter. So, the solution would be to use a single counter and check sequence in forward and in reverse order.

Another neat trick that I wouldn’t invent myself in a thousand years, is to consider the open counter as a RangeOpen = (min..max), where every wildcard broadens this range.

Approach

Let’s implement both solutions.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun checkValidString(s: String): Boolean {
        var open = 0
        for (c in s)
            if (c == '(' || c == '*') open++
            else if (c == ')' && --open < 0) return false
        open = 0
        for (i in s.lastIndex downTo 0) 
            if (s[i] == ')' || s[i] == '*') open++
            else if (s[i] == '(' && --open < 0) return false
        return true
    }


    pub fn check_valid_string(s: String) -> bool {
        let mut open = (0, 0);
        for b in s.bytes() {
            if b == b'(' { open.0 += 1; open.1 += 1 }
            else if b == b')' { open.0 -= 1; open.1 -= 1 }
            else { open.0 -= 1; open.1 += 1 }
            if open.1 < 0 { return false }
            if open.0 < 0 { open.0 = 0 }
        }
        open.0 == 0
    }

06.04.2024

1249. Minimum Remove to Make Valid Parentheses medium blog post substack youtube 2024-04-06_08-43.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/562

Problem TLDR

Remove minimum to make parenthesis valid #medium

Intuition

Let’s imagine some examples to better understand the problem:

     (a
     a(a
     a(a()
     (a))a

We can’t just append chars in a single pass. For example (a we don’t know if open bracket is valid or not. The natural idea would be to use a Stack somehow, but it is unknown how to deal with letters then. For this example: (a))a, we know that the second closing parenthesis is invalid, so the problem is straighforward. Now the trick is to reverse the problem for this case: (a -> a).

Approach

How many lines of code can you save?

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun minRemoveToMakeValid(s: String) = buildString {
        var open = 0
        for (c in s) {
            if (c == '(') open++
            if (c == ')') open--
            if (open >= 0) append(c)
            open = max(0, open)
        }
        for (i in length - 1 downTo 0) if (get(i) == '(') {
            if (--open < 0) break
            deleteAt(i)
        }
    }


    pub fn min_remove_to_make_valid(s: String) -> String {
        let (mut open, mut res) = (0, vec![]);
        for b in s.bytes() {
            if b == b'(' { open += 1 }
            if b == b')' { open -= 1 }
            if open >= 0 { res.push(b) }
            open = open.max(0)
        }
        for i in (0..res.len()).rev() {
            if open == 0 { break }
            if res[i] == b'(' {
                res.remove(i);
                open -= 1
            }
        }
        String::from_utf8(res).unwrap()
    }

05.04.2024

1544. Make The String Great easy blog post substack youtube

2024-04-05_08-24.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/561

Problem TLDR

Remove lowercase-uppercase pairs #easy

Intuition

Consider example:

    EbBe
     **
    E  e
    

After removing the middle bB we have to consider the remaining Ee. We can use Stack to do that.

Approach

In Kotlin: no need for Stack, just use StringBuilder. In Rust: Vec can be used as a Stack. There is no to_lowercase method returning a char, however there is a to_ascii_lowercase.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun makeGood(s: String) = buildString {
    for (c in s)
      if (length > 0 && c != get(lastIndex) && 
        c.lowercase() == get(lastIndex).lowercase()
      ) setLength(lastIndex) else append(c)
  }


  pub fn make_good(s: String) -> String {
    let mut stack = vec![];
    for c in s.chars() {
      if stack.is_empty() { stack.push(c) }
      else {
        let p = *stack.last().unwrap();
        if c != p && c.to_lowercase().eq(p.to_lowercase()) {
          stack.pop();
        } else { stack.push(c) }
      }
    }
    stack.iter().collect()
  }

04.04.2024

1614. Maximum Nesting Depth of the Parentheses easy blog post substack youtube 2024-04-04_09-03.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/560

Problem TLDR

Max nested parenthesis #easy

Intuition

No special intuition, just increase or decrease a counter.

Approach

  • There is a maxOf in Kotlin, but solution is not pure functional. It can be with fold.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun maxDepth(s: String): Int {
    var curr = 0
    return s.maxOf { 
      if (it == '(') curr++
      if (it == ')') curr--
      curr
    }
  }


  pub fn max_depth(s: String) -> i32 {
    let (mut curr, mut max) = (0, 0);
    for b in s.bytes() {
      if b == b'(' { curr += 1 }
      if b == b')' { curr -= 1 }
      max = max.max(curr)
    }
    max
  }

03.04.2024

79. Word Search medium blog post substack youtube 2024-04-03_08-01.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/559

Problem TLDR

Does grid have a word path? #medium

Intuition

Simple Depth-First Search for every starting point will give the answer. One trick is to store visited set in a grid itself.

Approach

  • Use dummy char to mark visited in a path.
  • Don’t forget to restore back.
  • Only mark visited right before traveling to the next to avoid failing at restoring.

Complexity

  • Time complexity: \(O(n3^n)\), n is a grid area

  • Space complexity: \(O(n)\)

Code


  fun exist(board: Array<CharArray>, word: String): Boolean {
    fun dfs(x: Int, y: Int, i: Int): Boolean {
      if (i == word.length) return true
      if (x !in 0..<board[0].size || y !in 0..<board.size) return false
      val c = board[y][x]
      if (c != word[i]) return false
      board[y][x] = '.'
      val res = dfs(x - 1, y, i + 1) || dfs(x + 1, y, i + 1)
             || dfs(x, y - 1, i + 1) || dfs(x, y + 1, i + 1)
      board[y][x] = c
      return res
    }
    for (y in 0..<board.size) for (x in 0..<board[0].size)
      if (dfs(x, y, 0)) return true
    return false
  }


  pub fn exist(mut board: Vec<Vec<char>>, word: String) -> bool {
    fn dfs(mut board: &mut Vec<Vec<char>>, word: &String, x: i32, y: i32, i: usize) -> bool {
      if i == word.len() { return true }
      if x < 0 || y < 0 || x == board[0].len() as i32 || y == board.len() as i32 { return false }
      let c = board[y as usize][x as usize];
      if c as u8 != word.as_bytes()[i] { return false }
      board[y as usize][x as usize] = '.';
      let res = 
        dfs(board, word, x - 1, y, i + 1) || dfs(board, word, x + 1, y, i + 1) ||
        dfs(board, word, x, y - 1, i + 1) || dfs(board, word, x, y + 1, i + 1);
      board[y as usize][x as usize] = c; res
    }
    let (n, m) = (board.len() as i32, board[0].len() as i32);
    for y in 0..n { for x in 0..m {
      if dfs(&mut board, &word, x, y, 0) { return true }
    }}
    false
  }

02.04.2024

205. Isomorphic Strings easy blog post substack youtube 2024-04-02_08-59.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/558

Problem TLDR

Can map chars from one string to another? #easy

Intuition

Let’s check if previous mapping is the same, otherwise result is false

Approach

We can use a HashMap or a simple [128] array.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(w)\), w is an alphabet or O(1)

Code


  fun isIsomorphic(s: String, t: String): Boolean {
    val map = mutableMapOf<Char, Char>()
    val map2 = mutableMapOf<Char, Char>()
    for ((i, c) in s.withIndex()) {
      if (map[c] != null && map[c] != t[i]) return false
      map[c] = t[i]
      if (map2[t[i]] != null && map2[t[i]] != c) return false
      map2[t[i]] = c
    }
    return true
  }


  pub fn is_isomorphic(s: String, t: String) -> bool {
    let mut m1 = vec![0; 128]; let mut m2 = m1.clone();
    for i in 0..s.len() {
      let c1 = s.as_bytes()[i] as usize;
      let c2 = t.as_bytes()[i] as usize;
      if m1[c1] > 0 && m1[c1] != c2 { return false }
      if m2[c2] > 0 && m2[c2] != c1 { return false }
      m1[c1] = c2; m2[c2] = c1
    }
    return true
  }

01.04.2024

58. Length of Last Word easy blog post substack youtube 2024-04-01_08-06.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/557

Problem TLDR

Last word length #easy

Intuition

There are many ways, let’s try to write an efficient solution. Iterate from the end, stop after the first word.

Approach

In Kotlin we can use first, takeWhile and count. In Rust let’s to write a simple for loop over bytes.

Complexity

  • Time complexity: \(O(w + b)\), where w is a last word length, and b suffix blank space length

  • Space complexity: \(O(1)\)

Code


  fun lengthOfLastWord(s: String) =
    ((s.lastIndex downTo 0).first { s[it] > ' ' } downTo 0)
    .asSequence().takeWhile { s[it] > ' ' }.count()


  pub fn length_of_last_word(s: String) -> i32 {
    let mut c = 0;
    for b in s.bytes().rev() {
      if b > b' ' { c += 1 } else if c > 0 { return c }
    }
    c
  }

31.03.2024

2444. Count Subarrays With Fixed Bounds hard blog post substack youtube 2024-03-31_12-25.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/555

Problem TLDR

Count subarrays of range minK..maxK #hard

Intuition

“all hope abandon ye who enter here” I’ve failed this question the second time (first was 1 year ago), and still find it very clever.

Consider the safe space as min(a, b)..max(a,b) where a is the last index of minK and b is the last index of maxK. We will remove suffix of 0..j where j is a last out of range minK..maxK.

Let’s examine the trick:


  // 1 3 5 2 7 5      1..5
  //j 
  //a 
  //b 
  // i
  // a
  //   i
  //     i
  //     b           +1 = min(a, b) - j = (0 - (-1))
  //       i         +1 = ...same...

another example:


  // 0 1 2 3 4 5 6
  // 7 5 2 2 5 5 1     
  //j      .
  //i      .
  //a
  //b
  // i     .
  // j     .
  //   i   .
  //   b   .
  //     i .
  //     a .         +1
  //       i
  //       a         +1
  //         i
  //         b       +3 = 3 - 0
  //           i
  //           b     +3

The interesting part happen at the index i = 4: it will update the min(a, b), making it a = 3.

Basically, every subarray starting between j..(min(a, b)) and ending at i will have minK and maxK, as min(a,b)..max(a,b) will have them.

Approach

Try to solve it yourself first.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun countSubarrays(nums: IntArray, minK: Int, maxK: Int): Long {
    var res = 0L; var a = -1; var j = -1; var b = -1
    for ((i, n) in nums.withIndex()) {
      if (n == minK) a = i 
      if (n == maxK) b = i
      if (n !in minK..maxK) j = i
      res += max(0, min(a, b) - j)
    }
    return res
  }


  pub fn count_subarrays(nums: Vec<i32>, min_k: i32, max_k: i32) -> i64 {
    let (mut res, mut a, mut b, mut j) = (0, -1, -1, -1);
    for (i, &n) in nums.iter().enumerate() {
      if n == min_k { a = i as i64 }
      if n == max_k { b = i as i64 }
      if n < min_k || n > max_k { j = i as i64 }
      res += (a.min(b) - j).max(0)
    }
    res
  }

30.03.2024

992. Subarrays with K Different Integers hard blog post substack youtube 2024-03-30_10-33.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/554

Problem TLDR

Count subarrays with k distinct numbers #hard

Intuition

We surely can count at most k numbers using sliding window technique: move the right pointer one step at a time, adjust the left pointer until condition met. All subarrays start..k where start in 0..j will have more or equal than k number of distincts if j..k have exatly k of them, so take j at each step.

To count exactly k we can remove subset of at least k from at least k - 1. (The trick here is that the number of at least k - 1 is the bigger one)

Approach

Let’s use a HashMap and some languages sugar:

  • Kotlin: sumOf
  • Rust: lambda to capture the parameters, entry.or_insert

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), we have a frequencies stored in a map, can be up to n

Code


  fun subarraysWithKDistinct(nums: IntArray, k: Int): Int {
    fun countAtLeast(k: Int): Int {
      val freq = mutableMapOf<Int, Int>()
      var j = 0; var count = 0
      return nums.indices.sumOf { i -> 
        freq[nums[i]] = 1 + (freq[nums[i]] ?: 0)
        if (freq[nums[i]] == 1) count++
        while (count > k) {
          freq[nums[j]] = freq[nums[j]]!! - 1
          if (freq[nums[j++]] == 0) count--
        }
        j
      }
    }
    return countAtLeast(k - 1) - countAtLeast(k)
  }


  pub fn subarrays_with_k_distinct(nums: Vec<i32>, k: i32) -> i32 {
    let count_at_least = |k: i32| -> i32 {
      let (mut freq, mut j, mut count) = (HashMap::new(), 0, 0);
      (0..nums.len()).map(|i| {
        *freq.entry(&nums[i]).or_insert(0) += 1;
        if freq[&nums[i]] == 1  { count += 1 }
        while count > k {
          *freq.get_mut(&nums[j]).unwrap() -= 1;
          if freq[&nums[j]] == 0 { count -= 1}
          j += 1;
        }
        j as i32
      }).sum()
    };
    count_at_least(k - 1) - count_at_least(k)
  }

29.03.2024

2962. Count Subarrays Where Max Element Appears at Least K Times medium blog post substack youtube 2024-03-29_09-26.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/553

Problem TLDR

Count subarrays with at least k array max in #medium

Intuition

Let’s observe an example 1 3 3:

    // inverse the problem
    // [1], [3], [3], [1 3], [1 3 3], [3 3] // 6
    // 1 3 3     ck  c
    // j .
    // i .           1
    //   i        1  3
    //     i      2
    //   j         
    //     j      1  4
    //                          6-4=2

The problem is more simple if we invert it: count subarrays with less than k maximums. Then it is just a two-pointer problem: increase by one, then shrink until condition < k met.

Another way, is to solve problem at face: left border is the count we need - all subarrays before our j..i will have k max elements if j..i have them.

Approach

Let’s implement both.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun countSubarrays(nums: IntArray, k: Int): Long {
    val n = nums.size.toLong()
    val m = nums.max(); var ck = 0; var j = 0
    return n * (n + 1) / 2 + nums.indices.sumOf { i ->
      if (nums[i] == m) ck++
      while (ck >= k) if (nums[j++] == m) ck--
      -(i - j + 1).toLong()
    }
  }


  pub fn count_subarrays(nums: Vec<i32>, k: i32) -> i64 {
    let (mut j, mut curr, m) = (0, 0, *nums.iter().max().unwrap());
    (0..nums.len()).map(|i| {
      if nums[i] == m { curr += 1 }
      while curr >= k { if nums[j] == m { curr -= 1 }; j += 1 }
      j as i64
    }).sum()
  }

28.03.2024

2958. Length of Longest Subarray With at Most K Frequency medium blog post substack youtube 2024-03-28_09-04.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/552

Problem TLDR

Max subarray length with frequencies <= k #medium

Intuition

There is a known sliding window pattern: right pointer will increase the frequency and left pointer will decrease it. Not try to expand as much as possible, then shrink until conditions are met.

Approach

  • move the right pointer one position at a time
  • we can use maxOf in Kotlin or max in Rust

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun maxSubarrayLength(nums: IntArray, k: Int): Int {
    val freq = mutableMapOf<Int, Int>(); var j = 0
    return nums.indices.maxOf { i ->
      freq[nums[i]] = 1 + (freq[nums[i]] ?: 0)
      while (freq[nums[i]]!! > k) 
        freq[nums[j]] = freq[nums[j++]]!! - 1
      i - j + 1
    }
  }


    pub fn max_subarray_length(nums: Vec<i32>, k: i32) -> i32 {
      let (mut freq, mut j) = (HashMap::new(), 0);
      (0..nums.len()).map(|i| {
        *freq.entry(nums[i]).or_insert(0) += 1;
        while freq[&nums[i]] > k {
          *freq.get_mut(&nums[j]).unwrap() -= 1; j += 1
        }
        i - j + 1
      }).max().unwrap() as i32
    }

27.03.2024

713. Subarray Product Less Than K medium blog post substack youtube 2024-03-27_09-18.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/551

Problem TLDR

Subarrays count with product less than k #medium

Intuition

Let’s try to use two pointers and move them only once:

  // 10 5 2 6 1 1 1                    cnt
  // i                    10           1
  // j          
  // *  j                 50 +5        3
  //    * j               (100) +2     4
  //    i                 10           5
  //    * * j             60 +6        7
  //    * * * j           60 +1        9
  //    * * * * j         60 +1        11
  //    * * * * * j       60 +1        13
  //      i * * * *       12 +1        15
  //        i * * *       6 +1         17
  //          i * *       1 +1         19
  //            i *       1 +1         21
  //              i       1 +1         23

As we notice, this way gives the correct answer. Expand the first pointer while p < k, then shrink the second pointer.

Approach

Next, some tricks:

  • move the right pointer once at a time
  • move the second until conditions are met
  • adding (i - j) helps to avoid moving the left pointer
  • if we handle the corner cases of k = 0 and k = 1, we can use some optimizations: nums[j] will always be less than k after while loop; and i will always be less than i in a while loop.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun numSubarrayProductLessThanK(nums: IntArray, k: Int): Int {
    var i = 0; var j = 0; var res = 0; var p = 1
    if (k < 2) return 0
    for (j in nums.indices) {
      p *= nums[j]
      while (p >= k) p /= nums[i++]
      res += j - i + 1
    }
    return res
  }


  pub fn num_subarray_product_less_than_k(nums: Vec<i32>, k: i32) -> i32 {
    if k < 2 { return 0 }
    let (mut j, mut p, mut res) = (0, 1, 0);
    for i in 0..nums.len() {
      p *= nums[i];
      while p >= k { p /= nums[j]; j += 1 }
      res += i - j + 1
    }
    res as i32
  }

26.03.2024

41. First Missing Positive hard blog post substack youtube 2024-03-26_09-20.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/550

Problem TLDR

First number 1.. not presented in the array, O(1) space #hard

Intuition

Let’s observe some examples. The idea is to use the array itself, as there is no restriction to modify it:

  /*
  1 -> 2 -> 3 ...

  0 1 2
  1 2 0
  *      0->1->2->0
  0 1 2

  0 1  2 3
  3 4 -1 1
  *         0 -> 3, 3 -> 1, 1 -> 4
  0 1    3 4
       *     2 -> -1

  7 8 9 11 12  1->

   */

We can use the indices of array: every present number must be placed at it’s index. As numbers are start from 1, we didn’t care about anything bigger than nums.size.

Approach

  • careful with of-by-one’s, 1 must be placed at 0 index and so on.

Complexity

  • Time complexity: \(O(n)\), at most twice if all numbers are present in array

  • Space complexity: \(O(1)\)

Code


  fun firstMissingPositive(nums: IntArray): Int {
    for (i in nums.indices)
      while ((nums[i] - 1) in 0..<nums.size && nums[nums[i] - 1] != nums[i]) 
        nums[nums[i] - 1] = nums[i].also { nums[i] = nums[nums[i] - 1] }
    return nums.indices.firstOrNull { nums[it] != it + 1 }?.inc() ?: nums.size + 1
  }


  pub fn first_missing_positive(mut nums: Vec<i32>) -> i32 {
    let n = nums.len() as i32;
    for i in 0..nums.len() {
      let mut j = nums[i] - 1;
      while 0 <= j && j < n && nums[j as usize] != j + 1 {
        let next = nums[j as usize] - 1;
        nums[j as usize] = j + 1;
        j = next
      }
    }
    for i in 0..n { if nums[i as usize] != i + 1 { return i + 1 }}
    n + 1
  }

25.03.2024

442. Find All Duplicates in an Array medium blog post substack youtube 2024-03-25_09-19.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/549

Problem TLDR

All duplicate numbers of 1..n using O(1) memory #medium

Intuition

There are no restrictions not to modify the input array, so let’s flat all visited numbers with a negative sign:


  // 1 2 3 4 5 6 7 8
  // 4 3 2 7 8 2 3 1
  // *     -
  //   * -
  //   - *
  //       *     -
  //         *     -
  //     -     *       --2
  //   -         *     --3
  // -             *

Inputs are all positive, the corner cases of negatives and zeros are handled.

Approach

  • don’t forget to abs
  • Rust didn’t permit to iterate and modify at the same time, use pointers

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun findDuplicates(nums: IntArray) = buildList {
    for (x in nums) {
      if (nums[abs(x) - 1] < 0) add(abs(x))
      nums[abs(x) - 1] *= -1
    }
  }


  pub fn find_duplicates(mut nums: Vec<i32>) -> Vec<i32> {
    let mut res = vec![];
    for j in 0..nums.len() {
      let i = (nums[j].abs() - 1) as usize;
      if nums[i] < 0 { res.push(nums[j].abs()) }
      nums[i] *= -1
    }
    res
  }

24.03.2024

287. Find the Duplicate Number medium blog post substack youtube 2024-03-24_11-13_1.webp

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/548

Problem TLDR

Duplicate single number in 1..n array, no extra memory #medium

Intuition

The idea of existing cycle would come to mind after some hitting your head against the wall. The interesting fact is we must find the node that is not a port of the cycle: so the meeting point will be our answer: 2024-03-24_10-35.jpg Now the clever trick is we can treat node 0 as this external node: 2024-03-24_10-55.jpg This will coincidentally make our code much cleaner, I think this was the intention of the question authors.

Approach

Draw some circles and arrows, walk the algorithm with your hands. To find the meeting point you must reset one pointer to the start.

  • The Rust’s do-while-do loop is perfectly legal https://programming-idioms.org/idiom/78/do-while-loop/795/rust

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun findDuplicate(nums: IntArray): Int {
    var fast = 0; var slow = 0
    do {
      fast = nums[nums[fast]]
      slow = nums[slow]
    } while (fast != slow)
    slow = 0
    while (fast != slow) {
      fast = nums[fast]
      slow = nums[slow]
    }
    return slow
  }


  pub fn find_duplicate(nums: Vec<i32>) -> i32 {
    let (mut tortoise, mut hare) = (0, 0); 
    while {
      hare = nums[nums[hare as usize] as usize];
      tortoise = nums[tortoise as usize];
      hare != tortoise
    }{}
    hare = 0;
    while (hare != tortoise) {
      hare = nums[hare as usize];
      tortoise = nums[tortoise as usize]
    }
    tortoise
  }

23.03.2024

143. Reorder List medium blog post substack youtube 2024-03-23_11-24.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/547

Problem TLDR

Reorder Linked List 1->2->3->4->5 -> 1->5->2->4->3 #medium

Intuition

There are no special hints here. However, the optimal solution will require some tricks:

  • use Tortoise And Hare algorithm to find the middle
  • reverse the second half
  • merge two lists

Approach

  • Tortoise And Hare: check fast.next != null to stop right at the middle
  • merge lists cleverly: always one into another and swap the points (don’t do this on the interview however, not from the start at least)
  • Rust: just gave up and implemented clone()-solution, sorry

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\), O(n) for my Rust solution. There are O(1) solutions exists on the leetcode.

Code


  fun reorderList(head: ListNode?): Unit {
    var s = head; var f = s
    while (f?.next != null) {
      f = f?.next?.next
      s = s?.next
    }
    f = null
    while (s != null) {
      val next = s.next
      s.next = f
      f = s
      s = next
    }
    s = head
    while (s != null) {
      val next = s.next
      s.next = f
      s = f
      f = next
    }
  }


  pub fn reorder_list(mut head: &mut Option<Box<ListNode>>) {
    let (mut f, mut s, mut c) = (head.clone(), head.clone(), 0);
    while f.is_some() && f.as_mut().unwrap().next.is_some()  {
      f = f.unwrap().next.unwrap().next;
      s = s.unwrap().next; c += 1
    }
    if c < 1 { return }
    let mut prev = None;
    while let Some(mut s_box) = s {
      let next = s_box.next;
      s_box.next = prev;
      prev = Some(s_box);
      s = next;
    }
    let mut s = head;
    while let Some(mut s_box) = s.take() {
      let next = s_box.next;
      if prev.is_none() && !f.is_some() || next.is_none() && f.is_some()  { 
        s_box.next = None;
        return;
      }
      s_box.next = prev;
      s = &mut s.insert(s_box).next;
      prev = next;
    }
  }

22.03.2024

234. Palindrome Linked List easy blog post substack youtube 2024-03-22_10-03.jpg

Problem TLDR

Is Linked List a palindrome #easy

Intuition

Find the middle using tortoise and hare algorithm and reverse it simultaneously.

Approach

  • the corners case is to detect odd or even count of nodes and do the extra move
  • gave up on the Rust solution without clone()

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\), O(n) in Rust

Code


  fun isPalindrome(head: ListNode?): Boolean {
    var fast = head; var slow = head
    var prev: ListNode? = null
    while (fast?.next != null) {
      fast = fast?.next?.next
      val next = slow?.next
      slow?.next = prev
      prev = slow
      slow = next
    }
    if (fast != null) slow = slow?.next
    while (prev != null && prev?.`val` == slow?.`val`)
      prev = prev?.next.also { slow = slow?.next }
    return prev == null
  }


  pub fn is_palindrome(head: Option<Box<ListNode>>) -> bool {
    let (mut fast, mut slow, mut prev) = (head.clone(), head, None);
    while fast.is_some() && fast.as_ref().unwrap().next.is_some() {
        fast = fast.unwrap().next.unwrap().next;
        let mut slow_box = slow.unwrap();
        let next = slow_box.next;
        slow_box.next = prev;
        prev = Some(slow_box);
        slow = next
    }
    if fast.is_some() { slow = slow.unwrap().next }
    while let Some(prev_box) = prev {
      let slow_box = slow.unwrap();
      if prev_box.val != slow_box.val { return false }
      prev = prev_box.next; slow = slow_box.next
    }; true
  }

21.03.2024

206. Reverse Linked List easy blog post substack youtube 2024-03-21_09-47.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/545

Problem TLDR

Reverse a Linked List #easy

Intuition

We need at least two pointers to store current node and previous.

Approach

In a recursive approach:

  • treat result as a new head
  • erase the link to the next
  • next.next must point to the current

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\) or log(n) for the recursion

Code


  fun reverseList(head: ListNode?): ListNode? =
    head?.next?.let { next ->
      head.next = null
      reverseList(next).also { next?.next = head }
    } ?: head


  pub fn reverse_list(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
    let mut curr = head; let mut prev = None;
    while let Some(mut curr_box) = curr {
      let next = curr_box.next;
      curr_box.next = prev;
      prev = Some(curr_box);
      curr = next;
    }
    prev
  }

Bonus: just a single pointer solution


  fun reverseList(head: ListNode?): ListNode? {
    var prev = head
    while (head?.next != null) {
      val next = head?.next?.next
      head?.next?.next = prev
      prev = head?.next
      head?.next = next
    }
    return prev
  }

2024-03-21_13-02.jpg

20.03.2024

1669. Merge In Between Linked Lists medium blog post substack youtube 2024-03-20_09-48.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/544

Problem TLDR

Replace a segment in a LinkedList #medium

Intuition

Just careful pointers iteration.

Approach

  • use dummy to handle the first node removal
  • better to write a separate cycles
  • Rust is hard

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun mergeInBetween(list1: ListNode?, a: Int, b: Int, list2: ListNode?) = 
    ListNode(0).run {
      next = list1
      var curr: ListNode? = this
      for (i in 1..a) curr = curr?.next
      var after = curr?.next
      for (i in a..b) after = after?.next
      curr?.next = list2
      while (curr?.next != null) curr = curr?.next
      curr?.next = after
      next
    }


  pub fn merge_in_between(list1: Option<Box<ListNode>>, a: i32, b: i32, list2: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
    let mut dummy = Box::new(ListNode::new(0));
    dummy.next = list1;
    let mut curr = &mut dummy;
    for _ in 0..a { curr = curr.next.as_mut().unwrap() }
    let mut after = &mut curr.next;
    for _ in a..=b { after = &mut after.as_mut().unwrap().next }
    let after_b = after.take(); // Detach the rest of the list after `b`, this will allow the next line for the borrow checker
    curr.next = list2;
    while let Some(ref mut next) = curr.next { curr = next; }
    curr.next = after_b;
    dummy.next
  }

19.03.2024

621. Task Scheduler medium blog post substack youtube 2024-03-19_10-10.jpg https://youtu.be/8t1KNa9iZjA

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/543

Problem TLDR

Count CPU cycles if task can’t run twice in n cycles #medium

Intuition

Let’s try to understand the problem first, by observing the example:

    // 0 1 2 3 4 5 6 7
    // a a a b b b c d n = 3
    // a . . . a . . . a
    //   b . . . b . . . b
    //     c d     i i

One inefficient way is to take tasks by thier frequency, store availability and adjust cycle forward if no task available. This solution will take O(n) time but with big constant of iterating and sorting the frequencies [26] array.

The clever way is to notice the pattern of how tasks are: there are empty slots between the most frequent task(s).

Approach

In the interview I would choose the first way.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun leastInterval(tasks: CharArray, n: Int): Int {
    val f = IntArray(128); for (c in tasks) f[c.code]++
    val maxFreq = f.max()
    val countOfMaxFreq = f.count { it == maxFreq }
    val slotSize = n - (countOfMaxFreq - 1)
    val slotsCount = (maxFreq - 1) * slotSize
    val otherTasks = tasks.size - maxFreq * countOfMaxFreq
    val idles = max(0, slotsCount - otherTasks)
    return tasks.size + idles
  }


    pub fn least_interval(tasks: Vec<char>, n: i32) -> i32 {
      let mut f = vec![0; 128]; for &c in &tasks { f[c as usize] += 1 }
      let maxFreq = f.iter().max().unwrap();
      let countOfMaxFreq = f.iter().filter(|&x| x == maxFreq).count() as i32;
      let slotsCount = (maxFreq - 1) * (n - countOfMaxFreq + 1);
      let otherTasks = tasks.len() as i32 - maxFreq * countOfMaxFreq;
      tasks.len() as i32 + (slotsCount - otherTasks).max(0)
    }

18.03.2024

452. Minimum Number of Arrows to Burst Balloons medium blog post substack youtube 2024-03-18_09-23.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/542

Problem TLDR

Count non-intersecting intervals #medium

Intuition

After sorting, we can line-sweep scan the intervals and count non-intersected ones. The edge case is that the right scan border will shrink to the smallest.


 [3,9],[7,12],[3,8],[6,8],[9,10],[2,9],[0,9],[3,9],[0,6],[2,8] 
 0..9 0..6 2..9 2..8 3..9 3..8 3..9 6..8 7..12 9..10
    * -  6 -    -    -    -    -    -    |
            

Approach

Let’s do some codegolf with Kotlin

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\), or O(n) with sortedBy

Code


  fun findMinArrowShots(points: Array<IntArray>): Int =
    1 + points.sortedBy { it[0] }.let { p -> p.count { (from, to) ->
      (from > p[0][1]).also { 
        p[0][1] = min(if (it) to else p[0][1], to) }}}


  pub fn find_min_arrow_shots(mut points: Vec<Vec<i32>>) -> i32 {
    points.sort_unstable_by_key(|p| p[0]);
    let (mut shoots, mut right) = (1, points[0][1]);
    for p in points {
      if p[0] > right { shoots += 1; right = p[1] }
      right = right.min(p[1])
    }; shoots
  }

17.03.2024

57. Insert Interval medium blog post substack youtube 2024-03-17_10-49.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/541

Problem TLDR

Insert interval into a sorted intervals array #medium

Intuition

There are several ways to attack the problem:

  • use single pointer and iterate once
  • count prefix and suffix and the middle part
  • same as previous, but use the Binary Search

The shortes code is prefix-suffix solution. But you will need to execute some examples to handle indices correctly. In the interview situation, it is better to start without the BinarySearch part.

Approach

To shorted the code let’s use some APIs:

  • Kotlin: asList, run, binarySearchBy
  • Rust: binary_search_by_key, unwrap_or, take, chain, once

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\) for the result

Code


  fun insert(intervals: Array<IntArray>, newInterval: IntArray) = 
    intervals.asList().run {
      var l = binarySearchBy(newInterval[0]) { it[1] }; if (l < 0) l = -l - 1
      var r = binarySearchBy(newInterval[1] + 1) { it[0] }; if (r < 0) r = -r - 1
      val min = min(newInterval[0], (getOrNull(l) ?: newInterval)[0])
      val max = max(newInterval[1], (getOrNull(r - 1) ?: newInterval)[1])
      (take(l) + listOf(intArrayOf(min, max)) + drop(r)).toTypedArray()
    }


  pub fn insert(intervals: Vec<Vec<i32>>, new_interval: Vec<i32>) -> Vec<Vec<i32>> {
    let l = match intervals.binary_search_by_key(&new_interval[0], |x| x[1]) {
        Ok(pos) => pos, Err(pos) => pos };
    let r = match intervals.binary_search_by_key(&(new_interval[1] + 1), |x| x[0]) {
        Ok(pos) => pos, Err(pos) => pos };
    let min_start = new_interval[0].min(intervals.get(l).unwrap_or(&new_interval)[0]);
    let max_end = new_interval[1].max(intervals.get(r - 1).unwrap_or(&new_interval)[1]);
    intervals.iter().take(l).cloned()
    .chain(std::iter::once(vec![min_start, max_end]))
    .chain(intervals.iter().skip(r).cloned()).collect()
  }

16.03.2024

525. Contiguous Array medium blog post substack youtube 2024-03-16_09-46.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/540

Problem TLDR

Max length of subarray sum(0) == sum(1) #medium

Intuition

Let’s observe an example 1 0 1 0 0 1 1 0 0 1 0 0 1 0 1:


  // 0 1 2 3 4 5 6 7 8 91011121314
  // 1 0 1 0 0 1 1 0 0 1 0 0 1 0 1
  // 1 0 1 0-1 0 1 0-1 0-1-2-1-2-1
  // * *0    .           .       .   2
  //   * *1  .           .       .   2
  // * * * *0.           .       .   4
  //         --1         .       . 
  // * * * * * *0        .       .   6
  //   * * * * * *1      .       .   6
  // * * * * * * * *0    .       .   8
  //         . * * * *-1 .       .   4
  // * * * * * * * * * *0.       .   10
  //         . * * * * * *-1     .   6
  //         .             --2   . 
  //         . * * * * * * * *-1 .   8
  //         .               * *-2   2
  //         . * * * * * * * * * *-1 10 = 14 - 4
  // 0 1 2 3 4 5 6 7 8 91011121314

Moving the pointer forward and calculating the balance (number of 0 versus number of 1), we can have compute max length up to the current position in O(1). Just store the first encounter of the balance number position.

Approach

Let’s shorten the code with:

  • Kotlin: maxOf, getOrPut
  • Rust: max, entry().or_insert

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun findMaxLength(nums: IntArray): Int =
    with (mutableMapOf<Int, Int>()) {
      put(0, -1); var b = 0
      nums.indices.maxOf {
        b += if (nums[it] > 0) 1 else -1
        it - getOrPut(b) { it }
      }
    }


  pub fn find_max_length(nums: Vec<i32>) -> i32 {
    let (mut b, mut bToInd) = (0, HashMap::new()); 
    bToInd.insert(0, -1);
    (0..nums.len() as i32).map(|i| {
      b += if nums[i as usize] > 0 { 1 } else { -1 };
      i - *bToInd.entry(b).or_insert(i)
    }).max().unwrap()
  }

15.03.2024

238. Product of Array Except Self medium blog post substack youtube 2024-03-15_08-47.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/539

Problem TLDR

Array of suffix-prefix products #medium

Intuition

Observe an example:


    // 1 2 3 4
    // * 2*3*4
    // 1 * 3*4
    // 1*2 * 4
    // 1*2*3 *

As we can’t use / operation, let’s precompute suffix and prefix products.

Approach

Then we can think about the space & time optimizations.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun productExceptSelf(nums: IntArray): IntArray {
    val suf = nums.clone()
    for (i in nums.lastIndex - 1 downTo 0) suf[i] *= suf[i + 1]
    var prev = 1
    return IntArray(nums.size) { i ->
      prev * suf.getOrElse(i + 1) { 1 }.also { prev *= nums[i] }
    }
  }


  pub fn product_except_self(nums: Vec<i32>) -> Vec<i32> {
    let n = nums.len(); let (mut res, mut p) = (vec![1; n], 1);
    for i in 1..n { res[i] = nums[i - 1] * res[i - 1] }
    for i in (0..n).rev() { res[i] *= p; p *= nums[i] }; res
  }

14.03.2024

930. Binary Subarrays With Sum medium blog post substack youtube 2024-03-14_09-06.jpg https://youtu.be/C-y7qYgqqxM

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/538

Problem TLDR

Count goal-sum subarrays in a 0-1 array #medium

Intuition

Let’s observe an example:

    // [0,0,1,0,1,0,0,0]
    //1     * * *
    //2   *
    //3 * *
    //4           *
    //5           * *
    //6           * * *
    //7   *       *
    //8 * *       *
    //9   *       * *
    //10* *       * *
    //11  *       * * *
    //12* *       * * *
    // 1 + 2 + 3 + 2*3

As we count possible subarrays, we see that zeros suffix and prefix matters and we can derive the math formula for them. The corner case is an all-zero array: we just take an arithmetic progression sum.

Approach

  • careful with pointers, widen zeros in a separate step
  • use a separate variables to count zeros
  • move pointers only forward
  • check yourself on the corner cases 0, 0 and 0, 0, 1

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun numSubarraysWithSum(nums: IntArray, goal: Int): Int {
    var i = 0; var j = 0; var sum = 0; var res = 0
    while (i < nums.size) {
      sum += nums[i]
      while (sum > goal && j < i) sum -= nums[j++]
      if (sum == goal) {
        var z1 = 0
        while (i + 1 < nums.size && nums[i + 1] == 0) { i++; z1++ }
        res += if (goal == 0) (z1 + 1) * (z1 + 2) / 2 else {
          var z2 = 0
          while (j < i && nums[j] == 0) { j++; z2++ }
          1 + z1 + z2 + z1 * z2
        }
      }; i++
    }; return res
  }


  pub fn num_subarrays_with_sum(nums: Vec<i32>, goal: i32) -> i32 {
    let (mut i, mut j, mut sum, mut res) = (0, 0, 0, 0);
    while i < nums.len() {
      sum += nums[i];
      while sum > goal && j < i { sum -= nums[j]; j += 1 }
      if sum == goal {
        let mut z1 = 0;
        while i + 1 < nums.len() && nums[i + 1] == 0 { i += 1; z1 += 1 }
        res += if goal == 0 { (z1 + 1) * (z1 + 2) / 2 } else {
          let mut z2 = 0;
          while j < i && nums[j] == 0 { j += 1; z2 += 1 }
          1 + z1 + z2 + z1 * z2
        }
      }; i += 1 
    }; res
  }

13.03.2024

2485. Find the Pivot Integer easy blog post substack youtube 2024-03-13_08-33.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/537

Problem TLDR

Pivot of 1..n where sum[1..p] == sum[p..n]. #easy

Intuition

Let’s observe an example:

  // 1 2 3 4 5 6 7 8
  // 1 2 3 4 5         5 * 6 / 2 = 15
  //           6 7 8   8 * 9 / 2 = 36 - 15
  //           p=6       
  // p * (p + 1) / 2 == n * (n + 1) / 2 - p * (p - 1) / 2

The left part will increase with the grown of pivot p, so we can use Binary Search in that space.

Another solution is to simplify the equation more:

  // x(x + 1)/2 == n(n + 1)/2 - x(x + 1)/2 + x
  // x(x + 1) - x == sum
  // x^2 == sum

Given that, just check if square root is perfect.

Approach

For more robust Binary Search:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always move the boundaries: lo = mi + 1, hi = mid -
  • use a separate condition to exit

Complexity

  • Time complexity: \(O(log(n))\), square root is also log(n)

  • Space complexity: \(O(1)\)

Code


  fun pivotInteger(n: Int): Int {
    var lo = 1; var hi = n; 
    while (lo <= hi) {
      val p = lo + (hi - lo) / 2
      val l = p * (p + 1) / 2
      val r = n * (n + 1) / 2 - p * (p - 1) / 2
      if (l < r) lo = p + 1 else
      if (l > r) hi = p - 1 else return p
    }
    return -1
  }


  pub fn pivot_integer(n: i32) -> i32 {
    let sum = n * (n + 1) / 2;
    let sq = (sum as f32).sqrt() as i32;
    if (sq * sq == sum) { sq } else { -1 }
  }

12.03.2024

1171. Remove Zero Sum Consecutive Nodes from Linked List medium blog post substack youtube 2024-03-12_10-03.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/536

Problem TLDR

Remove consequent 0-sum items from a LinkedList #medium

Intuition

Let’s calculate running sum and check if we saw it before. The corner case example:

    // 1 3 2 -3 -2 5 5 -5 1
    // 1 4 6  3  1 6 11 6 7
    //   - -  -  -
    //     x         -  -
    // 1           5      1

We want to remove 3 2 -3 -2 but sum = 6 is yet stored in our HashMap. So we need to manually clean it. This will not increse the O(n) time complexity as we are walk at most twice.

Approach

The Rust approach is O(n^2). We operate with references like this: first .take() then insert(v) back. (solution from https://leetcode.com/discreaminant2809/)

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun removeZeroSumSublists(head: ListNode?): ListNode? {
    val dummy = ListNode(0).apply { next = head }
    val sumToNode = mutableMapOf<Int, ListNode>()
    var n: ListNode? = dummy; var sum = 0
    while (n != null) {
      sum += n.`val`
      val prev = sumToNode[sum]
      if (prev != null) {
        var x: ListNode? = prev.next
        var s = sum
        while (x != n && x != null) {
          s += x.`val`
          if (x == sumToNode[s]) sumToNode.remove(s)
          x = x.next
        }
        prev.next = n.next
      } else sumToNode[sum] = n
      n = n.next
    }
    return dummy.next
  }


    pub fn remove_zero_sum_sublists(mut head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
      let mut node_i_ref = &mut head;
      'out: while let Some(mut node_i) = node_i_ref.take() {
        let (mut node_j_ref, mut sum) = (&mut node_i, 0);
        loop {
          sum += node_j_ref.val;
          if sum == 0 {
            *node_i_ref = node_j_ref.next.take();
            continue 'out;
          }
          let Some (ref mut next_node_j_ref) = node_j_ref.next else { break };
          node_j_ref = next_node_j_ref;
        }
        node_i_ref = &mut node_i_ref.insert(node_i).next;
      }
      head
    }

11.03.2024

791. Custom Sort String medium blog post substack youtube 2024-03-11_09-08.jpg

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/535

Problem TLDR

Construct string from s using order #medium

Intuition

Two ways to solve: use sort (we need a stable sort algorithm), or use frequency.

Approach

When using sort, take care of -1 case. When using frequency, we can use it as a counter too ( -= 1).

Complexity

  • Time complexity: \(O(n)\), or nlog(n) for sorting

  • Space complexity: \(O(n)\)

Code


  fun customSortString(order: String, s: String) = s
    .toMutableList()
    .sortedBy { order.indexOf(it).takeIf { it >= 0 } ?: 200 }
    .joinToString("")


  pub fn custom_sort_string(order: String, s: String) -> String {
    let (mut freq, mut res) = (vec![0; 26], String::new());
    for b in s.bytes() { freq[(b - b'a') as usize] += 1 }
    for b in order.bytes() {
      let i = (b - b'a') as usize;
      while freq[i] > 0 {  freq[i] -= 1; res.push(b as char) }
    }
    for b in s.bytes() { 
      if freq[(b - b'a') as usize] > 0 { res.push(b as char) }
    }; res
  }

10.03.2024

349. Intersection of Two Arrays easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/534

Problem TLDR

Intersection of two nums arrays #easy

Intuition

Built-in Set has an intersect method, that will do the trick. However, as a follow up, there is a O(1) memory solution using sorting (can be done with O(1) memory https://stackoverflow.com/questions/55008384/can-quicksort-be-implemented-in-c-without-stack-and-recursion), then just use two-pointers pattern, move the lowest:

...
      if nums1[i] < nums2[j] { i += 1 } else
      if nums1[i] > nums2[j] { j += 1 } else {
        let x = nums1[i]; res.push(x);
        while (i < nums1.len() && nums1[i] == x) { i += 1 }
        while (j < nums2.len() && nums2[j] == x) { j += 1 }
      }
...

Approach

Let’s write shorter code, to save our own space and time by using built-in implementations.

  • Rust wants into_iter instead of iter, as iter makes vec<&&i32>
  • Rust didn’t compile without cloned

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun intersection(nums1: IntArray, nums2: IntArray) =
    nums1.toSet().intersect(nums2.toSet()).toIntArray()


  pub fn intersection(mut nums1: Vec<i32>, mut nums2: Vec<i32>) -> Vec<i32> {
    nums1.into_iter().collect::<HashSet<_>>()
    .intersection(&nums2.into_iter().collect()).cloned().collect()
  }

09.03.2024

2540. Minimum Common Value easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/533

Problem TLDR

First common number in two sorted arrays #easy

Intuition

There is a short solution with Set and more optimal with two pointers: move the lowest one.

Approach

Let’s implement both of them.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\), or O(n) for Set solution

Code


  fun getCommon(nums1: IntArray, nums2: IntArray) = nums1
    .toSet().let { s -> nums2.firstOrNull { it in  s}} ?: -1


    pub fn get_common(nums1: Vec<i32>, nums2: Vec<i32>) -> i32 {
      let (mut i, mut j) = (0, 0);
      while i < nums1.len() && j < nums2.len() {
        if nums1[i] == nums2[j] { return nums1[i] }
        else if nums1[i] < nums2[j] { i += 1 } else { j += 1 }
      }; -1
    }

08.03.2024

3005. Count Elements With Maximum Frequency easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/532

Problem TLDR

Count of max-freq nums #easy

Intuition

Count frequencies, then filter by max and sum.

Approach

There are at most 100 elements, we can use array to count.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun maxFrequencyElements(nums: IntArray) = nums
  .asList().groupingBy { it }.eachCount().values.run {
    val max = maxOf { it }
    sumBy { if (it < max) 0 else it }
  }


  pub fn max_frequency_elements(nums: Vec<i32>) -> i32 {
    let mut freq = vec![0i32; 101];
    for x in nums { freq[x as usize] += 1; }
    let max = freq.iter().max().unwrap();
    freq.iter().filter(|&f| f == max).sum()
  }

07.03.2024

876. Middle of the Linked List easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/531

Problem TLDR

Middle of the Linked List #easy

Intuition

Use Tortoise and Hare algorithm https://cp-algorithms.com/others/tortoise_and_hare.html

Approach

We can check fast.next or just fast, but careful with moving slow. Better test yourself with examples: [1], [1,2], [1,2,3], [1,2,3,4].

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun middleNode(head: ListNode?): ListNode? {
    var s = head; var f = s
    while (f?.next != null) {
      f = f?.next?.next; s = s?.next
    }
    return s
  }


    pub fn middle_node(head: Option<Box<ListNode>>) -> Option<Box<ListNode>> {
      let (mut s, mut f) = (head.clone(), head);
      while f.is_some() {
        f = f.unwrap().next;
        if f.is_some() { f = f.unwrap().next; s = s.unwrap().next }
      }
      s
    }

06.03.2024

141. Linked List Cycle easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/530

Problem TLDR

Detect cycle #easy

Intuition

Use two pointers, fast and slow, they will meet sometime.

Approach

No Rust in the templates provided, sorry.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun hasCycle(h: ListNode?, f: ListNode? = h?.next): Boolean =
    f != null && (h == f || hasCycle(h?.next, f?.next?.next))


    bool hasCycle(ListNode *s) {
        auto f = s;
        while (f && f->next) {
            s = s->next; f = f->next->next;
            if (s == f) return true;
        }
        return false;
    }

05.03.2024

1750. Minimum Length of String After Deleting Similar Ends medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/529

Problem TLDR

Min length after trimming matching prefix-suffix several times. #medium

Intuition

By looking at the examples, greedy approach should be the optimal one.

Approach

  • careful with indices, they must stop at the remaining part

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun minimumLength(s: String): Int {
    var i = 0; var j = s.lastIndex
    while (i < j && s[i] == s[j]) {
      while (i + 1 < j && s[i + 1] == s[j]) i++
      while (i < j - 1 && s[i] == s[j - 1]) j--
      i++; j--
    }
    return j - i + 1
  }


  pub fn minimum_length(s: String) -> i32 {
    let (mut i, mut j, s) = (0, s.len() - 1, s.as_bytes());
    while i < j && s[i] == s[j] {
      while i + 1 < j && s[i + 1] == s[j] { i += 1 }
      while i < j - 1 && s[i] == s[j - 1] { j -= 1 }
      i += 1; j -= 1
    }
    1 + (j - i) as i32
  }

04.03.2024

948. Bag of Tokens medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/528

Problem TLDR

Max score converting power to token[i] and token[i] to score. #medium

Intuition

Let’s observe some examples by our bare hands:

    // 100 200 300 400     p 200  s 0
    // -                     100    1
    //             +         500    0
    //     -                 300    1
    //         -             0      2
    // 200 400 400 400     p 200  s 0
    // -                       0    1
    //             +         400    0
    //     -               

As we can see, the greedy approach can possibly be the optimal one after sorting the array.

Approach

  • careful with empty arrays in Rust: len() - 1 will crash

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


  fun bagOfTokensScore(tokens: IntArray, power: Int): Int {
    tokens.sort()
    var i = 0; var j = tokens.lastIndex
    var p = power; var s = 0; var m = 0
    while (i <= j)
      if (p >= tokens[i]) { p -= tokens[i++]; m = max(m, ++s) }
      else if (s-- > 0) p += tokens[j--] else break
    return m
  }


  pub fn bag_of_tokens_score(mut tokens: Vec<i32>, mut power: i32) -> i32 {
    tokens.sort_unstable(); if tokens.is_empty() { return 0 }
    let (mut i, mut j, mut s, mut m) = (0, tokens.len() - 1, 0, 0);
    while i <= j {
      if power >= tokens[i] {
        s += 1; power -= tokens[i]; i += 1; m = m.max(s)
      } else if s > 0 {
        s -= 1; power += tokens[j]; j -= 1;
      } else { break }
    }; m
  }

03.03.2024

19. Remove Nth Node From End of List medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/527

Problem TLDR

Remove nth node from the tail of linked list.

Intuition

There is a two-pointer technique: fast pointer moves n nodes from the slow, then they go together until the end. image.png

Approach

Some tricks:

  • Use dummy first node to handle the head removal case.
  • We can use counter to make it one pass. Rust borrow checker makes the task non trivial: one pointer must be mutable, another must be cloned.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? {
    var r: ListNode = ListNode(0).apply { next = head }
    var a: ListNode? = r; var b: ListNode? = r; var i = 0
    while (b != null) { if (i++ > n) a = a?.next; b = b?.next }
    a?.next = a?.next?.next
    return r.next
  }


  pub fn remove_nth_from_end(head: Option<Box<ListNode>>, n: i32) -> Option<Box<ListNode>> {
    let mut r = ListNode { val: 0, next: head }; let mut r = Box::new(r);
    let mut b = r.clone(); let mut a = r.as_mut(); let mut i = 0;
    while b.next.is_some() {
      i+= 1; if i > n { a = a.next.as_mut().unwrap() }
      b = b.next.unwrap()
    }
    let n = a.next.as_mut().unwrap(); a.next = n.next.clone(); r.next
  }

02.03.2024

977. Squares of a Sorted Array easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/526

Problem TLDR

Sorted squares.

Intuition

We can build the result bottom up or top down. Either way, we need two pointers: for the negative and for the positive.

Approach

Can we made it shorter?

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun sortedSquares(nums: IntArray): IntArray {
    var i = 0; var j = nums.lastIndex;
    return IntArray(nums.size) {
      (if (abs(nums[i]) > abs(nums[j])) 
        nums[i++] else nums[j--]).let { it * it }
    }.apply { reverse() }
  }


  pub fn sorted_squares(nums: Vec<i32>) -> Vec<i32> {
    let (mut i, mut j) = (0, nums.len() - 1);
    let mut v: Vec<_> = (0..=j).map(|_| 
      if nums[i].abs() > nums[j].abs() {
        i += 1; nums[i - 1] * nums[i - 1]
      } else { j -= 1; nums[j + 1] * nums[j + 1] })
      .collect(); v.reverse(); v
  }

01.03.2024

2864. Maximum Odd Binary Number easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/525

Problem TLDR

Max odd number string rearrangement.

Intuition

Count zeros and ones and build a string.

Approach

Let’s try to find the shortest version of code.

Complexity

  • Time complexity: \(O(n)\)$

  • Space complexity: \(O(n)\)

Code


  fun maximumOddBinaryNumber(s: String) = 
  s.count { it == '0' }.let { 
    "1".repeat(s.length - it - 1) + "0".repeat(it) + "1"
  }


  pub fn maximum_odd_binary_number(s: String) -> String {
    let c0 = s.bytes().filter(|b| *b == b'0').count();
    format!("{}{}1", "1".repeat(s.len() - c0 - 1), "0".repeat(c0))
  }

29.02.2024

1609. Even Odd Tree medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/523

Problem TLDR

Binary tree levels are odd increasing and even decreasing.

Intuition

Just use level-order BFS traversal.

Approach

Let’s try to make code shorter by simplifying the if condition.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), the last level of the Binary Tree is almost n/2 nodes.

Code


  fun isEvenOddTree(root: TreeNode?) = ArrayDeque<TreeNode>().run {
    root?.let { add(it) }
    var inc = true
    while (size > 0) {
      var prev = 0
      repeat(size) { removeFirst().run {
        if (`val` % 2 > 0 != inc || `val` == prev
         || `val` < prev == inc && prev > 0) return false
        left?.let { add(it) }; right?.let { add(it) }
        prev = `val`
      }}
      inc = !inc
    }; true
  }


  pub fn is_even_odd_tree(root: Option<Rc<RefCell<TreeNode>>>) -> bool {
    let mut q = VecDeque::new();
    if let Some(n) = root { q.push_back(n) }
    let mut inc = true;
    while !q.is_empty() {
      let mut prev = 0;
      for _ in 0..q.len() { if let Some(n) = q.pop_front() {
        let n = n.borrow(); let v = n.val;
        if (v % 2 > 0) != inc || v == prev 
        || (v < prev) == inc && prev > 0 { return false }
        if let Some(l) = n.left.clone() { q.push_back(l) }
        if let Some(r) = n.right.clone() { q.push_back(r) }
        prev = v
      }}
      inc = !inc
    } true
  }

28.02.2024

513. Find Bottom Left Tree Value medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/522

Problem TLDR

Leftmost node value of the last level of the Binary Tree.

Intuition

Just solve this problem for both left and right children, then choose the winner with most depth.

Approach

Code looks nicer when dfs function accepts nullable value.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\)

Code


  fun findBottomLeftValue(root: TreeNode?): Int {
    fun dfs(n: TreeNode?): List<Int> = n?.run {
      if (left == null && right == null) listOf(`val`, 1) else {
        val l = dfs(left); val r = dfs(right)
        val m = if (r[1] > l[1]) r else l
        listOf(m[0], m[1] + 1)
    }} ?: listOf(Int.MIN_VALUE, 0)
    return dfs(root)[0]
  }


  pub fn find_bottom_left_value(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
    fn dfs(n: &Option<Rc<RefCell<TreeNode>>>) -> (i32, i32) {
      n.as_ref().map_or((i32::MIN, 0), |n| { let n = n.borrow();
        if !n.left.is_some() && !n.right.is_some() { (n.val, 1) } else {
          let (l, r) = (dfs(&n.left), dfs(&n.right));
          let m = if r.1 > l.1 { r } else { l };
          (m.0, m.1 + 1)
      }})}
    dfs(&root).0
  }

27.02.2024

543. Diameter of Binary Tree easy blog post substack youtube 2024-02-27_08-18.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/521

Problem TLDR

Max distance between any nodes in binary tree.

Intuition

Distance is the sum of the longest depths in left and right nodes.

Approach

We can return a pair of sum and max depth, but modifying an external variable looks simpler.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\)

Code


  fun diameterOfBinaryTree(root: TreeNode?): Int {
    var max = 0
    fun dfs(n: TreeNode?): Int = n?.run {
      val l = dfs(left); val r = dfs(right)
      max = max(max, l + r); 1 + max(l, r)
    } ?: 0
    dfs(root)
    return max    
  }


  pub fn diameter_of_binary_tree(root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
    let mut res = 0;
    fn dfs(n: &Option<Rc<RefCell<TreeNode>>>, res: &mut i32) -> i32 {
      n.as_ref().map_or(0, |n| { let n = n.borrow();
        let (l, r) = (dfs(&n.left, res), dfs(&n.right, res));
        *res = (*res).max(l + r); 1 + l.max(r)
      })
    }
    dfs(&root, &mut res); res
  }

26.02.2024

100. Same Tree easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/519

Problem TLDR

Are two binary trees equal?

Intuition

Use recursion to check current nodes and subtrees.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\) for the recursion depth

Code


    fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean = 
      p?.`val` == q?.`val` && (p == null || 
      isSameTree(p.left, q?.left) && 
      isSameTree(p.right, q?.right))


  pub fn is_same_tree(p: Option<Rc<RefCell<TreeNode>>>, q: Option<Rc<RefCell<TreeNode>>>) -> bool {
    p.as_ref().zip(q.as_ref()).map_or_else(|| p.is_none() && q.is_none(), |(p, q)| {
      let (p, q) = (p.borrow(), q.borrow()); 
      p.val == q.val &&
      Self::is_same_tree(p.left.clone(), q.left.clone()) &&
      Self::is_same_tree(p.right.clone(), q.right.clone())
    })
  }

25.02.2024

2709. Greatest Common Divisor Traversal hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/518

Problem TLDR

Are all numbers connected through gcd?

Intuition

The n^2 solution is trivial, just remember how to calculate the GCD. Let’s see how to optimize it by using all the possible hints and observing the example. To connect 4 to 3 we expect some number that are multiple of 2 and 3. Those are prime numbers. It gives us the idea, that numbers can be connected throug the primes.

Let’s build all the primes and assign our numbers to each. To build the primes, let’s use Sieve of Eratosthenes https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes.

    // 4 3 12 8     
    // 2 3 5 7 11 13 17 19 23 29 31
    // 4
    //   3
    //12 12
    // 8

In this example, we assign 4, 12 and 8 to prime 2, 3 and 12 to prime 3. The two islands of primes 2 and 3 are connected through the number 12.

Another example with the corner case of 1: image.png

The different solution is to compute all the factors of each number and connect the numbers instead of the primes.

Approach

  • use Union-Find and path compression uf[x] = uf[uf[x]]
  • factors are less than sqrt(n)

Complexity

  • Time complexity: \(O(nsqrt(n))\)

  • Space complexity: \(O(n)\)

Code


  fun canTraverseAllPairs(nums: IntArray): Boolean {
    if (nums.contains(1)) return nums.size == 1
    val nums = nums.toSet().toList()
    val p = BooleanArray(nums.max() + 1) { true }
    for (i in 2..sqrt(p.size.toDouble()).toInt()) if (p[i]) 
      for (j in i * i..<p.size step i) p[j] = false
    val primes = (2..<p.size).filter { p[it] }
    val uf = IntArray(primes.size) { it }
    fun Int.root(): Int {
      var x = this; while (x != uf[x]) x = uf[x]
      uf[this] = x; return x
    }
    val islands = HashSet<Int>()
    for (x in nums) {
      var prev = -1
      for (i in primes.indices) if (x % primes[i] == 0) {
          islands += i
          if (prev != -1) uf[prev.root()] = i.root()
          prev = i
        }
    }
    val oneOf = islands.firstOrNull()?.root() ?: -1
    return islands.all { it.root() == oneOf }
  }


    pub fn can_traverse_all_pairs(nums: Vec<i32>) -> bool {
      let mut uf: Vec<_> = (0..nums.len()).collect();
      fn root(uf: &mut Vec<usize>, mut x: usize) -> usize {
        while x != uf[x] { x = uf[x]; uf[x] = uf[uf[x]] } x}
      let mut mp = HashMap::<i32, usize>::new();
      for (i, &x) in nums.iter().enumerate() {
        if x == 1 { return nums.len() == 1 }
        let mut factors = vec![x];
        let mut a = x;
        for b in 2..=(x as f64).sqrt() as i32 {
          while a % b == 0 { a /= b; factors.push(b) }
        }
        if a > 1 { factors.push(a) }
        for &f in &factors { 
          if let Some(&j) = mp.get(&f) {
            let ra = root(&mut uf, i);
            uf[ra] = root(&mut uf, j);
          }
          mp.insert(f, i);
        }
      }
      let ra = root(&mut uf, 0);
      (0..uf.len()).all(|b| root(&mut uf, b) == ra)
    }

24.02.2024

2092. Find All People With Secret hard blog post substack youtube

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/517

Problem TLDR

Who knows 0 and firstPerson’s secret after group meetings at times: [personA, personB, time].

Intuition

To share the secret between people we can use a known Union-Find data structure. The corner case is when the meeting time is passed and no one knowns a secret: we must revert a union for these people.

Approach

To make Union-Find more performant, there are several tricks. One of them is a path compression: after finding the root, set all the intermediates to root. Ranks are more complex and not worth the lines of code.

Complexity

  • Time complexity: \(O(an)\), a is close to 1

  • Space complexity: \(O(n)\)

Code


  fun findAllPeople(n: Int, meetings: Array<IntArray>, firstPerson: Int): List<Int> {
    meetings.sortWith(compareBy { it[2] })
    val uf = HashMap<Int, Int>()
    fun root(a: Int): Int = 
      uf[a]?.let { if (a == it) a else root(it).also { uf[a] = it } } ?: a
    uf[0] = firstPerson
    val s = mutableListOf<Int>()
    var prev = 0
    for ((a, b, t) in meetings) {
      if (t > prev) for (x in s) if (root(x) != root(0)) uf[x] = x
      if (t > prev) s.clear()
      uf[root(a)] = root(b)
      s += a; s += b; prev = t
    }
    return (0..<n).filter { root(0) == root(it) }
  }


  pub fn find_all_people(n: i32, mut meetings: Vec<Vec<i32>>, first_person: i32) -> Vec<i32> {
    meetings.sort_unstable_by_key(|m| m[2]);
    let mut uf: Vec<_> = (0..n as usize).collect();
    fn root(uf: &mut Vec<usize>, mut x: usize) -> usize {
      while uf[x] != x { uf[x] = uf[uf[x]]; x = uf[x] } x
    }
    uf[0] = first_person as _;
    let (mut prev, mut s) = (0, vec![]);
    for m in &meetings {
      if m[2] > prev { for &x in &s { if root(&mut uf, x) != root(&mut uf, 0) { uf[x] = x }}}
      if m[2] > prev { s.clear() }
      let ra = root(&mut uf, m[0] as _);
      uf[ra] = root(&mut uf, m[1] as _);
      s.push(m[0] as _); s.push(m[1] as _); prev = m[2]
    }
    (0..n).filter(|&x| root(&mut uf, x as _) == root(&mut uf, 0)).collect()   
  }

23.02.2024

787. Cheapest Flights Within K Stops medium blog post substack youtube

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/516

Problem TLDR

Cheapest travel src -> dst with at most k stops in a directed weighted graph.

Approach

There is a Floyd-Warshall algorithm for such problems: make k rounds of travel trough all the reachable edges and improve the so-far cost.

  • we must make a copy of the previous step, to avoid flying more than one step in a round

Complexity

  • Time complexity: \(O(kne)\), where e is edges

  • Space complexity: \(O(n)\)

Code


    fun findCheapestPrice(n: Int, flights: Array<IntArray>, src: Int, dst: Int, k: Int): Int {
      val costs = IntArray(n) { Int.MAX_VALUE / 2 }
      costs[src] = 0
      repeat(k + 1) {
        val prev = costs.clone()
        for ((f, t, c) in flights) 
            costs[t] = min(costs[t], prev[f] + c)
      }
      return costs[dst].takeIf { it < Int.MAX_VALUE / 2 } ?: -1
    }


  pub fn find_cheapest_price(n: i32, flights: Vec<Vec<i32>>, src: i32, dst: i32, k: i32) -> i32 {
    let mut costs = vec![i32::MAX / 2 ; n as usize];
    costs[src as usize] = 0;
    for _ in 0..=k {
      let prev = costs.clone();
      for e in &flights { 
        costs[e[1] as usize] = costs[e[1] as usize].min(prev[e[0] as usize] + e[2])
      }
    }
    if costs[dst as usize] < i32::MAX / 2 { costs[dst as usize] } else { -1 }
  }

22.02.2024

997. Find the Town Judge easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/515

Problem TLDR

Find who trusts nobody and everybody trusts him in [trust, trusted] array.

Intuition

First, potential judge is from set 1..n excluding all the people who trust someone trust.map { it[0] }. Next, check everybody trust him count == n - 1.

Another approach, is to count in-degree and out-degree nodes in graph.

Approach

For the second approach, we didn’t need to count out-degrees, just make in-degrees non-usable.

Let’s try to shorten the code.

  • Kotlin: use toSet, map, takeIf, count, first
  • Rust: find, map_or.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun findJudge(n: Int, trust: Array<IntArray>) =
    ((1..n).toSet() - trust.map { it[0] }.toSet())
      .takeIf { it.size == 1 }?.first()
      ?.takeIf { j -> 
        trust.count { it[1] == j } == n - 1
      } ?: -1


  pub fn find_judge(n: i32, trust: Vec<Vec<i32>>) -> i32 {
    let mut deg = vec![0; n as usize + 1];
    for e in trust {
      deg[e[0] as usize] += n;
      deg[e[1] as usize] += 1;
    }
    (1..deg.len()).find(|&j| deg[j] == n - 1).map_or(-1, |j| j as i32)
  }

21.02.2024

201. Bitwise AND of Numbers Range medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/514

Problem TLDR

Bitwise AND for [left..right].

Intuition

To understand the problem, let’s observe how this works:

    // 0  0000
    // 1  0001           2^0
    // 2  0010
    // 3  0011
    // 4  0100 3..4 = 0  2^2
    // 5  0101 3..5 = 0
    // 6  0110
    // 7  0111 6..7
    // 8  1000           2^3
    // 9  1001  7..9 = 0

Some observations:

  • When interval intersects 4, 8 and so on, it AND operation becomes 0.
  • Otherwise, we take the common prefix: 6: 0110 & 7: 0111 = 0110.

Approach

We can take the most significant bit and compare it. In another way, we can just find the common prefix trimming the bits from the right side.

Complexity

  • Time complexity: \(O(1)\), at most 32 calls happens

  • Space complexity: \(O(1)\)

Code


  fun rangeBitwiseAnd(left: Int, right: Int): Int {
    if (left == right) return left
    val l = left.takeHighestOneBit()
    val r = right.takeHighestOneBit()
    return if (l != r) 0 else 
      l or rangeBitwiseAnd(left xor l, right xor r)
  }


  pub fn range_bitwise_and(left: i32, right: i32) -> i32 {
    if left == right { left }
    else { Self::range_bitwise_and(left / 2, right / 2) * 2 }
  }

20.02.2024

268. Missing Number easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/513

Problem TLDR

Missing in [0..n] number.

Intuition

There are several ways to find it:

  • subtracting sums
  • doing xor
  • computing sum with a math n * (n + 1) / 2

Approach

Write what is easier for you, then learn the other solutions. Xor especially.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun missingNumber(nums: IntArray): Int =
    (1..nums.size).sum() - nums.sum()


  pub fn missing_number(nums: Vec<i32>) -> i32 {
    nums.iter().enumerate().map(|(i, n)| i as i32 + 1 - n).sum()
  }

19.02.2024

231. Power of Two easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/511

Problem TLDR

Is number 2^x?

Intuition

Power of two number has just one bit on: 2 -> 10, 4 -> 100, 8 -> 1000. There is a known bit trick to turn off a single rightmost bit: n & (n - 1).

Approach

  • careful with the negative numbers and zero

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\)

Code


    fun isPowerOfTwo(n: Int) =
      n > 0 && n and (n - 1) == 0


    pub fn is_power_of_two(n: i32) -> bool {
      n > 0 && n & (n - 1) == 0
    }

18.02.2024

2402. Meeting Rooms III hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/510

Problem TLDR

Most frequent room of 0..<n where each meeting[i]=[start, end) takes or delays until first available.

Intuition

Let’s observe the process of choosing the room for each meeting:

    // 0 1     0,10 1,5 2,7 3,4
    //10       0,10
    //   5          1,5
    //                  2,7
    //   10             5,10=5+(7-2)
    //                       3,4
    //11                    10,11

    // 0 1 2    1,20  2,10  3,5  4,9  6,8
    //20        1,20
    //  10            2,10
    //     5                3,5
    //                           4,9
    //    10                     5,10
    //                                6,8
    //  12                           10,12

    //  0  1  2  3  18,19  3,12  17,19  2,13  7,10
    //               2,13  3,12   7,10 17,19 18,19
    // 13            2,13
    //    12               3,12
    //       10                   7,10
    //          19                     17,19
    //     <-19                               18,19
    //  1  1  2  1

    // 0  1  2  3   19,20 14,15 13,14 11,20
    //              11,20 13,14 14,15 19,20
    //20              *
    //   14                 *
    //    <-15

Some caveats are:

  • we must take room with lowest index
  • this room must be empty or meeting must already end
  • the interesting case is when some rooms are still empty, but some already finished the meeting.

To handle finished meetings, we can just repopulate the PriorityQueue with the current time.

Approach

Let’s try to write a minimal code implementation.

  • Kotiln heap is a min-heap, Rust is a max-heap
  • Kotlin maxBy is not greedy, returns first max. Rust max_by_key is greedy and returns the last visited max, so not useful here.

Complexity

  • Time complexity: \(O(mnlon(n))\), m is a meetings size. Repopulation process is nlog(n). Just finding the minimum is O(mn).

  • Space complexity: \(O(n)\)

Code


  fun mostBooked(n: Int, meetings: Array<IntArray>): Int {
    meetings.sortWith(compareBy { it[0] })
    val v = LongArray(n); val freq = IntArray(n)
    for ((s, f) in meetings) {
      val room = (0..<n).firstOrNull { v[it] <= s } ?: v.indexOf(v.min())
      if (v[room] > s) v[room] += (f - s).toLong() else v[room] = f.toLong()
      freq[room]++
    }
    return freq.indexOf(freq.max())
  }


    pub fn most_booked(n: i32, mut meetings: Vec<Vec<i32>>) -> i32 {
      let (mut v, mut freq) = (vec![0; n as usize], vec![0; n as usize]);
      meetings.sort_unstable();
      for m in meetings {
        let (s, f) = (m[0] as i64, m[1] as i64);
        let room = v.iter().position(|&v| v <= s).unwrap_or_else(|| {
          let min = *v.iter().min().unwrap();
          v.iter().position(|&v| v == min).unwrap() });
        freq[room] += 1;
        v[room] = if v[room] > s { f - s + v[room] } else { f } 
      }
      let max = *freq.iter().max().unwrap();
      freq.iter().position(|&f| f == max).unwrap() as i32
    }

17.02.2024

1642. Furthest Building You Can Reach medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/509

Problem TLDR

Max index to climb diff = a[i +1] - a[i] > 0 using bricks -= diff and ladders– for each.

Intuition

First, understand the problem by observing the inputs:


  // 0 1  2 3 4 5  6  7 8
  // 4 12 2 7 3 18 20 3 19    10 2
  //  8    5   15 2    16
  //  b    l   l  b  

  • only increasing pairs matters
  • it is better to use the ladders for the biggest diffs

The simple solution without tricks is to do a BinarySearch: can we reach the mid-point using all the bricks and ladders? Then just sort diffs in 0..mid range and take bricks for the smaller and ladders for the others. This solution would cost us O(nlog^2(n)) and it passes.

However, in the leetcode comments, I spot that there is an O(nlogn) solution exists. The idea is to grab as much bricks as we can and if we cannot, then we can drop back some (biggest) pile of bricks and pretend we used the ladders instead. We can do this trick at most ladders’ times.

Approach

Try not to write the if checks that are irrelevant.

  • BinaryHeap in Rust is a max heap
  • PriorityQueue in Kotlin is a min heap, use reverseOrder

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun furthestBuilding(heights: IntArray, bricks: Int, ladders: Int): Int {
    val pq = PriorityQueue<Int>(reverseOrder())
    var b = bricks; var l = ladders
    for (i in 1..<heights.size) {
      val diff = heights[i] - heights[i - 1]
      if (diff <= 0) continue
      pq += diff
      if (b < diff && l-- > 0) b += pq.poll()
      if (b < diff) return i - 1
      b -= diff
    }
    return heights.lastIndex
  }


  pub fn furthest_building(heights: Vec<i32>, mut bricks: i32, mut ladders: i32) -> i32 {
    let mut hp = BinaryHeap::new();
    for i in 1..heights.len() {
      let diff = heights[i] - heights[i - 1];
      if diff <= 0 { continue }
      hp.push(diff);
      if bricks < diff && ladders > 0 { 
        bricks += hp.pop().unwrap();
        ladders -= 1;
      }
      if bricks < diff { return i as i32 - 1 }
      bricks -= diff;
    }
    heights.len() as i32 - 1
  }

16.02.2024

1481. Least Number of Unique Integers after K Removals medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/507

Problem TLDR

Min uniq count after removing k numbers.

Intuition

Just to be sure what the problem is about, let’s write some other examples: [1,2,3,4,4] k = 3, [1,2,3,4,4,4] k = 3, [1,2,3,3,4,4,4] k = 3. The first two will give the same unswer 1, the last one is 2, however. As soon as we understood the problem, just implement the algorithm: sort numbers by frequency and remove from smallest to the largest.

Approach

Let’s try to make the code shorter, by using languages:

  • Kotlin: asList, groupingBy, eachCount, sorted, run
  • Rust: entry+or_insert, Vec::from_iter, into_values, sort_unstable, fold

Complexity

  • Time complexity: \(O(nlog(n))\), worst case, all numbers are uniq

  • Space complexity: \(O(n)\)

Code


  fun findLeastNumOfUniqueInts(arr: IntArray, k: Int) = arr
    .asList().groupingBy { it }.eachCount()
    .values.sorted().run {
      var c = k
      size - count { c >= it.also { c -= it } }
    }


  pub fn find_least_num_of_unique_ints(arr: Vec<i32>, mut k: i32) -> i32 {
    let mut freq = HashMap::new();
    for x in arr { *freq.entry(x).or_insert(0) += 1 }
    let mut freq = Vec::from_iter(freq.into_values());
    freq.sort_unstable();
    freq.iter().fold(freq.len() as i32, |acc, count| {
      k -= count;
      if k < 0 { acc } else { acc - 1 }
    })
  }

15.02.2024

2971. Find Polygon With the Largest Perimeter medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/506

Problem TLDR

The largest subset sum(a[..i]) > a[i + 1] where a is a subset of array.

Intuition

First, understand the problem: [1,12,1,2,5,50,3] doesn’t have a polygon, but [1,12,1,2,5,23,3] does. After this, the solution is trivial: take numbers in increasing order, compare with sum and check.

Approach

Let’s try to use the languages.

  • Kotlin: sorted, fold
  • Rust: sort_unstable, iter, fold

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\), sorted takes O(n) but can be avoided

Code


  fun largestPerimeter(nums: IntArray) = nums
    .sorted()
    .fold(0L to -1L) { (s, r), x ->
      s + x to if (s > x) s + x else r
    }.second


  pub fn largest_perimeter(mut nums: Vec<i32>) -> i64 {
    nums.sort_unstable();
    nums.iter().fold((0, -1), |(s, r), &x| 
      (s + x as i64, if s > x as i64 { s + x as i64 } else { r })
    ).1
  }

14.02.2024

2149. Rearrange Array Elements by Sign medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/505

Problem TLDR

Rearrange array to positive-negative sequence.

Intuition

First is to understand that we can’t do this in-place: for example 1 1 1 1 1 1 -1 -1 -1 -1 -1 -1 we must store somewhere the 1s that is changed by -1s. Next, just use two pointers and a separate result array.

Approach

We can use ping-pong technique for pointers and make work with only the current pointer. Some language’s APIs:

  • Kotlin: indexOfFirst, also, find
  • Rust: iter, position, find

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun rearrangeArray(nums: IntArray): IntArray {
    var i = nums.indexOfFirst { it > 0 }
    var j = nums.indexOfFirst { it < 0 }
    return IntArray(nums.size) {
      nums[i].also { n ->
        i = (i + 1..<nums.size)
          .find { n > 0 == nums[it] > 0 } ?: 0
        i = j.also { j = i }
      }
    }
  }


  pub fn rearrange_array(nums: Vec<i32>) -> Vec<i32> {
    let mut i = nums.iter().position(|&n| n > 0).unwrap();
    let mut j = nums.iter().position(|&n| n < 0).unwrap();
    (0..nums.len()).map(|_| {
      let n = nums[i];
      i = (i + 1..nums.len())
        .find(|&i| (n > 0) == (nums[i] > 0)).unwrap_or(0);
      (i, j) = (j, i); n
    }).collect()
  }

13.02.2024

2108. Find First Palindromic String in the Array easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/504

Problem TLDR

Find a palindrome.

Intuition

Compare first chars with the last.

Approach

Let’s use some API’s:

  • Kotlin: firstOrNull, all
  • Rust: into_iter, find, chars, eq, rev, unwrap_or_else, into. The eq compares two iterators with O(1) space.

Complexity

  • Time complexity: \(O(wn)\)

  • Space complexity: \(O(1)\)

Code


    fun firstPalindrome(words: Array<String>) = 
      words.firstOrNull { w ->
        (0..w.length / 2).all { w[it] == w[w.lastIndex - it] }
      } ?: ""


  pub fn first_palindrome(words: Vec<String>) -> String {
    words.into_iter().find(|w|
      w.chars().eq(w.chars().rev())
    ).unwrap_or_else(|| "".into())
  }

12.02.2024

169. Majority Element easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/503

Problem TLDR

Element with frequency > nums.len / 2.

Intuition

First thing is to understand the problem, as we need to find not only the most frequent element, but frequency is given > nums.len / 2 by the input constraints. Next, let’s observe examples: image.png There are properties derived from the observation:

  • sequence can spread other elements between the common
  • common can exist in several islands
  • the second common island size is less than first common
  • island can be single one We can write an ugly algorithm full of ‘ifs’ now.

  fun majorityElement(nums: IntArray): Int {
    var a = -1
    var b = -1
    var countA = 1
    var countB = 0
    var currCount = 1
    var prev = -1
    for (x in nums) {
      if (x == prev) {
        currCount++
        if (currCount > nums.size / 2) return x
      } else {
        if (currCount > 1) {
          if (a == -1) a = prev
          else if (b == -1) b = prev
          if (prev == a) {
            countA += currCount
          }
          if (prev == b) {
            countB += currCount
          }
        }
        currCount = 1
      }
      prev = x
    }
    if (a == -1) a = prev
    else if (b == -1) b = prev
    if (prev == a) {
      countA += currCount
    } else if (prev == b) {
      countB += currCount
    }
    return if (a == -1 && b == -1) {
      nums[0]
    } else if (countA > countB) a else b
  }

Approach

However, for our pleasure, there is a comment section of leetcode exists, find some big head solution there: it works like a magic for me still. Count the current frequency and decrease it by all others. If others are sum up to a bigger value, our candidate is not the hero.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun majorityElement(nums: IntArray): Int {
    var a = -1
    var c = 0
    for (x in nums) {
      if (c == 0) a = x
      c += if (x == a) 1 else -1
    }
    return a
  }


  pub fn majority_element(nums: Vec<i32>) -> i32 {
    let (mut a, mut c) = (-1, 0);
    for x in nums {
      if c == 0 { a = x }
      c += if x == a { 1 } else { -1 }
    }
    a
  }

11.02.2024

1463. Cherry Pickup II medium blog post substack youtube

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/502

Problem TLDR

Maximum paths sum of two robots top-down in XY grid.

Intuition

One way is to try all possible paths, but that will give TLE. However, we can notice, that only start position of two robots matters, so result can be cached: image.png

Another neat optimization is to forbid to intersect the paths.

Approach

Can you make code shorter?

  • wrapping_add for Rust
  • takeIf, maxOf, in Range for Kotlin

Complexity

  • Time complexity: \(O(mn^2)\)

  • Space complexity: \((mn^2)\)

Code


  fun cherryPickup(grid: Array<IntArray>): Int {
    val r = 0..<grid[0].size
    val ways = listOf(-1 to -1, -1 to 0, -1 to 1,
                       0 to -1,  0 to 0,  0 to 1,
                       1 to -1,  1 to 0,  1 to 1)
    val dp = Array(grid.size) { 
             Array(grid[0].size) { 
             IntArray(grid[0].size) { -1 } } }
    fun dfs(y: Int, x1: Int, x2: Int): Int = 
      dp[y][x1][x2].takeIf { it >= 0 } ?: {
        grid[y][x1] + grid[y][x2] +
        if (y == grid.lastIndex) 0 else ways.maxOf { (dx1, dx2) -> 
          val nx1 = x1 + dx1
          val nx2 = x2 + dx2
          if (nx1 in r && nx2 in r && nx1 < nx2) { dfs(y + 1, nx1, nx2) } else 0
      }}().also { dp[y][x1][x2] = it }
    return dfs(0, 0, grid[0].lastIndex)
  }


  pub fn cherry_pickup(grid: Vec<Vec<i32>>) -> i32 {
    let (h, w, mut ans) = (grid.len(), grid[0].len(), 0);
    let mut dp = vec![vec![vec![-1; w]; w]; h];
    dp[0][0][w - 1] = grid[0][0] + grid[0][w - 1];
    for y in 1..h {
      for x1 in 0..w { for x2 in 0..w {
          let prev = if y > 0 { dp[y - 1][x1][x2] } else { 0 };
          if prev < 0 { continue }
          for d1 in -1..=1 { for d2 in -1..=1 {
              let x1 = x1.wrapping_add(d1 as usize);
              let x2 = x2.wrapping_add(d2 as usize);
              if x1 < x2 && x2 < w {
                let f = prev + grid[y][x1] + grid[y][x2];
                dp[y][x1][x2] = dp[y][x1][x2].max(f);
                ans = ans.max(f);
              }
          }}
      }}
    }
    ans
  }

10.02.2024

647. Palindromic Substrings medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/501

Problem TLDR

Count palindromes substrings.

Intuition

There are two possible ways to solve this, one is Dynamic Programming, let’s observe some examples first:

  // aba
  // b -> a b a aba
  // abcba
  // a b c b a bcb abcba
  // aaba -> a a b a aa aba

Palindrome can be defined as dp[i][j] = s[i] == s[j] && dp[i - 1][j + 1]. This takes quadratic space and time. Other way to solve is to try to expand from each position. This will be more optimal, as it takes O(1) space and possible O(n) time if there is no palindromes in string. The worst case is O(n^2) however.

Approach

Can we make code shorter?

  • avoid checking the boundaries of dp[] by playing with initial values and indices

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\) or O(1) for the second.

Code


  fun countSubstrings(s: String): Int {
    val dp = Array(s.length + 1) { i -> 
      BooleanArray(s.length + 1) { i >= it }}
    return s.indices.sumOf { j -> 
        (j downTo 0).count { i -> 
        s[i] == s[j] && dp[i + 1][j]
        .also { dp[i][j + 1] = it } } }
  }


  pub fn count_substrings(s: String) -> i32 {
    let s = s.as_bytes();
    let c = |mut l: i32, mut r: usize| -> i32 {
      let mut count = 0;
      while l >= 0 && r < s.len() && s[l as usize] == s[r] {
        l -= 1; r += 1; count += 1;
      }
      count
    };
    (0..s.len()).map(|i| c(i as i32, i) + c(i as i32, i + 1)).sum()
  }

09.02.2024

368. Largest Divisible Subset medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/500

Problem TLDR

Longest subset of divisible by s[i] % s[j] == 0   s[j] % s[i] == 0.

Intuition

Sort always helps, so do it. Let’s imagine a sequence of numbers like this:

    // 1 3 9 15 27 30 60
    // 1 3 9    27
    // 1 3   15    30 60
    // 3 4 8 16

There is a choice to be made: take 9 or 15. So we can search with DFS and try to take each number. Also, there are some interesting things happening: for every position there is only one longest suffix subsequence. We can cache it.

Approach

I didn’t solve it the second time, so I can’t give you the working approach yet. Try as hard as you can for 1 hour, then give up and look for solutions. My problem was: didn’t considered DP, but wrote working backtracking solution. Also, I have attempted the graph solution to find a longest path, but that was TLE.

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


  fun largestDivisibleSubset(nums: IntArray): List<Int> {
    nums.sort()
    val dp = mutableMapOf<Int, List<Int>>()
    fun dfs(i: Int): List<Int> = dp.getOrPut(i) {
      var seq = listOf<Int>()
      val x = if (i == 0) 1 else nums[i - 1]
      for (j in i..<nums.size) if (nums[j] % x == 0) {
        val next = listOf(nums[j]) + dfs(j + 1)
        if (next.size > seq.size) seq = next
      }
      seq
    }
    return dfs(0)
  }


    pub fn largest_divisible_subset(mut nums: Vec<i32>) -> Vec<i32> {
      nums.sort_unstable();
      let mut dp: HashMap<usize, Vec<i32>> = HashMap::new();
  
      fn dfs(nums: &[i32], i: usize, dp: &mut HashMap<usize, Vec<i32>>) -> Vec<i32> {
          dp.get(&i).cloned().unwrap_or_else(|| {
              let x = nums.get(i.wrapping_sub(1)).copied().unwrap_or(1);
              let largest_seq = (i..nums.len())
                  .filter(|&j| nums[j] % x == 0)
                  .map(|j| {
                      let mut next = vec![nums[j]];
                      next.extend(dfs(nums, j + 1, dp));
                      next
                  })
                  .max_by_key(|seq| seq.len())
                  .unwrap_or_else(Vec::new);
  
              dp.insert(i, largest_seq.clone());
              largest_seq
          })
      }
  
      dfs(&nums, 0, &mut dp)
    }

08.02.2024

279. Perfect Squares medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/499

Problem TLDR

Min square numbers sum up to n.

Intuition

By wrong intuition would be just subtract maximum possible square number: 12 = 9 + remainder. So, we should explore all of possible squares and choose min count of them. We can do DFS and cache the result. To pass the TLE, we need to rewrite it back into bottom up DP.

Approach

Let’s write as shorter as we can by using:

  • Kotlin: minOf, sqrt without Math, toFloat vs toDouble
  • Rust: (1..)
  • avoid case of x = 0 to safely invoke minOf and unwrap

Complexity

  • Time complexity: \(O(nsqrt(n))\)

  • Space complexity: \(O(n)\)

Code


  fun numSquares(n: Int): Int {
    val dp = IntArray(n + 1)
    for (x in 1..n)
      dp[x] = (1..sqrt(x.toFloat()).toInt())
      .minOf { 1 + dp[x - it * it] }
    return dp[n]
  }


  pub fn num_squares(n: i32) -> i32 {
    let mut dp = vec![0; n as usize + 1];
    for x in 1..=n as usize {
      dp[x] = (1..).take_while(|&k| k * k <= x)
      .map(|k| 1 + dp[x - k * k]).min().unwrap();
    }
    dp[n as usize]
  }

07.02.2024

451. Sort Characters By Frequency medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/498

Problem TLDR

Sort string by char’s frequencies.

Intuition

The optimal solution would be to sort [128] size array of frequencies, then build a string in O(n). There are some other ways, however…

Approach

Let’s explore the shortest versions of code by using the API:

  • Kotlin: groupBy, sortedBy, flatMap, joinToString
  • Rust: vec![], sort_unstable_by_key, just sorting the whole string takes 3ms

Complexity

  • Time complexity: \(O(n)\), or O(nlog(n)) for sorting the whole string

  • Space complexity: \(O(n)\)

Code


  fun frequencySort(s: String) = s
    .groupBy { it }.values 
    .sortedBy { -it.size }
    .flatMap { it }
    .joinToString("")


  pub fn frequency_sort(s: String) -> String {
    let mut f = vec![0; 128];
    for b in s.bytes() { f[b as usize] += 1 }
    let mut cs: Vec<_> = s.chars().collect();
    cs.sort_unstable_by_key(|&c| (-f[c as usize], c));
    cs.iter().collect()
  }

06.02.2024

49. Group Anagrams medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/497

Problem TLDR

Group words by chars in them.

Intuition

We can use char’s frequencies or just sorted words as keys to grouping.

Approach

Use the standard API for Kotlin and Rust:

  • groupBy vs no grouping method in Rust (but have in itertools)
  • entry().or_insert_with for Rust
  • keys are faster to just sort instead of count in Rust

Complexity

  • Time complexity: \(O(mn)\), for counting, mlog(n) for sorting

  • Space complexity: \(O(mn)\)

Code


    fun groupAnagrams(strs: Array<String>): List<List<String>> =
       strs.groupBy { it.groupBy { it } }.values.toList() 


  pub fn group_anagrams(strs: Vec<String>) -> Vec<Vec<String>> {
    let mut groups = HashMap::new();
    for s in strs {
      let mut key: Vec<_> = s.bytes().collect();
      key.sort_unstable();
      groups.entry(key).or_insert_with(Vec::new).push(s);
    }
    groups.into_values().collect()
  }

05.02.2024

387. First Unique Character in a String easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/496

Problem TLDR

First non-repeating char position.

Intuition

Compute char’s frequencies, then find first of 1.

Approach

Let’s try to make code shorter: Kotlin:

  • groupBy
  • run
  • indexOfFirst Rust:
  • vec![]
  • String.find
  • map_or

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun firstUniqChar(s: String) = s.groupBy { it }
    .run { s.indexOfFirst { this[it]!!.size < 2 } }


  pub fn first_uniq_char(s: String) -> i32 {
    let mut f = vec![0; 128];
    for b in s.bytes() { f[b as usize] += 1 }
    s.find(|c| f[c as usize] < 2).map_or(-1, |i| i as i32)
  }

04.02.2024

76. Minimum Window Substring hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/495

Problem TLDR

Minimum window of s including all chars of t.

Intuition

The greedy approach with sliding window would work: move right window pointer right until all chars are obtained. Then move left border until condition no longer met.

There is an optimization possible: remove the need to check all character’s frequencies by counting how many chars are absent.

Approach

Let’s try to shorten the code:

  • .drop.take is shorter than substring, as skipping one if
  • range in Rust are nice
  • into shortern than to_string

Complexity

  • Time complexity: \(O(n + m)\)

  • Space complexity: \(O(1)\)

Code


  fun minWindow(s: String, t: String): String {
    val freq = IntArray(128)
    for (c in t) freq[c.code]++
    var i = 0
    var r = arrayOf(s.length, s.length + 1)
    var count = t.length
    for ((j, c) in s.withIndex()) {
      if (freq[c.code]-- > 0) count--
      while (count == 0) {
        if (j - i + 1 < r[1]) r = arrayOf(i, j - i + 1)
        if (freq[s[i++].code]++ == 0) count++
      }
    }
    return s.drop(r[0]).take(r[1])
  }


  pub fn min_window(s: String, t: String) -> String {
    let mut freq = vec![0; 128];
    for b in t.bytes() { freq[b as usize] += 1; }
    let (mut i, mut r, mut c) = (0, 0..0, t.len());
    for (j, b) in s.bytes().enumerate() {
      if freq[b as usize] > 0 { c -= 1; }
      freq[b as usize] -= 1;
      while c == 0 {
        if j - i + 1 < r.len() || r.len() == 0 { r = i..j + 1; }
        let a = s.as_bytes()[i] as usize;
        freq[a] += 1; if freq[a] > 0 { c += 1; }
        i += 1;
      }
    }
    s[r].into()
  }
  

03.02.2024

1043. Partition Array for Maximum Sum medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/493

Problem TLDR

Max sum of partition array into chunks size of at most k filled with max value in chunk.

Intuition

Let’s just brute force with Depth-First Search starting from each i position: search for the end of chunk j and choose the maximum of the sum. max_sum[i] = optimal_chunk + max_sum[chunk_len]. This can be cached by the i.

Then rewrite into bottom up DP.

Approach

  • use size + 1 for dp, to avoid ‘if’s
  • careful with the problem definition: it is not the max count of chunks, it is the chunks lengths up to k

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n)\)

Code


  fun maxSumAfterPartitioning(arr: IntArray, k: Int): Int {
    val dp = IntArray(arr.size + 1)
    for (i in arr.indices) {
      var max = 0
      for (j in i downTo max(0, i - k + 1)) {
        max = max(max, arr[j])
        dp[i + 1] = max(dp[i + 1], (i - j + 1) * max + dp[j])
      }
    }
    return dp[arr.size]
  }


  pub fn max_sum_after_partitioning(arr: Vec<i32>, k: i32) -> i32 {
    let mut dp = vec![0; arr.len() + 1];
    for i in 0..arr.len() {
      let mut max_v = 0;
      for j in (0..=i).rev().take(k as usize) {
        max_v = max_v.max(arr[j]);
        dp[i + 1] = dp[i + 1].max((i - j + 1) as i32 * max_v + dp[j]);
      }
    }
    dp[arr.len()]
  }

02.02.2024

1291. Sequential Digits medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/492

Problem TLDR

Numbers with sequential digits in low..high range.

Intuition

Let’s write down all of them:

  // 1 2 3 4 5 6 7 8 9
  // 12 23 34 45 57 67 78 89
  // 123 234 345 456 678 789
  // 1234 2345 3456 4567 5678 6789 
  // 12345 23456 34567 45678 56789
  // 123456 234567 345678 456789
  // 1234567 2345678 3456789
  // 12345678 23456789
  // 123456789

After that you will get the intuition how they are built: we scan pairs, increasing first ten times and appending last digit of the second.

Approach

Let’s try to leverage the standard iterators in Kotlin & Rust:

  • runningFold vs scan
  • windowed vs window
  • flatten vs flatten

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\)

Code


  fun sequentialDigits(low: Int, high: Int) = 
    (1..9).runningFold((1..9).toList()) { r, _ -> 
      r.windowed(2) { it[0] * 10 + it[1] % 10 }
    }.flatten().filter { it in low..high }


  pub fn sequential_digits(low: i32, high: i32) -> Vec<i32> {
    (1..10).scan((1..10).collect::<Vec<_>>(), |s, _| {
      let r = Some(s.clone());
      *s = s.windows(2).map(|w| w[0] * 10 + w[1] % 10).collect(); r
    }).flatten().filter(|&x| low <= x && x <= high).collect()
  }

01.02.2024

2966. Divide Array Into Arrays With Max Difference medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/491

Problem TLDR

Split array into tripples with at most k difference.

Intuition

Sort, then just check k condition.

Approach

Let’s use iterators in Kotlin and Rust:

  • chunked vs chunks
  • sorted() vs sort_unstable() (no sorted iterator in Rust)
  • takeIf() vs ..
  • all() vs any()
  • .. map(), to_vec(), collect(), vec![]

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun divideArray(nums: IntArray, k: Int) = nums
    .sorted().chunked(3).toTypedArray()
    .takeIf { it.all { it[2] - it[0] <= k } } ?: arrayOf()


  pub fn divide_array(mut nums: Vec<i32>, k: i32) -> Vec<Vec<i32>> {
    nums.sort_unstable();
    if nums.chunks(3).any(|c| c[2] - c[0] > k) { vec![] } 
    else { nums.chunks(3).map(|c| c.to_vec()).collect() }
  }

31.01.2024

739. Daily Temperatures medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/489

Problem TLDR

Array of distances to the next largest.

Intuition

Let’s walk array backwards and observe which numbers we need to keep track of and which are irrelevant:


  0  1  2  3  4  5  6  7
  73 74 75 71 69 72 76 73
  73                            73            7
  76                            76            6
  72                            76 72         6 5    6 - 5 = 1
  69                            76 72 69      6 5 4
  71                            76 72 71      6 5 3  5 - 3 = 2

As we see, we must keep the increasing orders of values and drop each less than current. This technique is a known pattern called Monotonic Stack.

Approach

There are several ways to write that, let’s try to be brief.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun dailyTemperatures(temps: IntArray): IntArray =
  Stack<Int>().run {
    temps.indices.reversed().map { i ->
      while (size > 0 && temps[peek()] <= temps[i]) pop()
      (if (size > 0) peek() - i else 0).also { push(i) }
    }.reversed().toIntArray()    
  }


  pub fn daily_temperatures(temps: Vec<i32>) -> Vec<i32> {
    let (mut r, mut s) = (vec![0; temps.len()], vec![]);
    for (i, &t) in temps.iter().enumerate().rev() {
      while s.last().map_or(false, |&j| temps[j] <= t) { s.pop(); }
      r[i] = (*s.last().unwrap_or(&i) - i) as i32;
      s.push(i);
    }
    r
  }

30.01.2024

150. Evaluate Reverse Polish Notation medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/488

Problem TLDR

Solve Reverse Polish Notation.

Intuition

Push to stack until operation met, then pop twice and do op.

Approach

Let’s try to be brief.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun evalRPN(tokens: Array<String>) = Stack<Int>().run {
    for (s in tokens) push(when (s) {
      "+" -> pop() + pop()
      "-" -> -pop() + pop()
      "*" -> pop() * pop()
      "/" -> pop().let { pop() / it }
      else -> s.toInt()
    })
    pop()
  }


  pub fn eval_rpn(tokens: Vec<String>) -> i32 {
    let mut s = vec![];
    for t in tokens { if let Ok(n) = t.parse() { s.push(n) }
     else { let (a, b) = (s.pop().unwrap(), s.pop().unwrap());
      s.push(match t.as_str() { 
        "+" => a + b, "-" => b - a, "*" => a * b, _ => b / a }) }}
    s[0]
  }

29.01.2024

232. Implement Queue using Stacks easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/487

Problem TLDR

Queue by 2 stacks.

Intuition

Let’s write down how the numbers are added:

stack a: [1 2]
stack b: [] 

peek:

a: [1]
b: [2]

a: []
b: [2 1], b.peek == 1

Approach

Let’s do some code golf.

Complexity

  • Time complexity: \(O(1)\) for total operations. In general, stack drain is a rare operation

  • Space complexity: \(O(n)\) for total operations.

Code


class MyQueue() {
  val a = Stack<Int>()
  val b = Stack<Int>()
  fun push(x: Int) = a.push(x)
  fun pop() = peek().also { b.pop() }
  fun peek(): Int {
    if (b.size < 1) while (a.size > 0) b += a.pop()
    return b.peek()
  }
  fun empty() = a.size + b.size == 0
}

struct MyQueue(Vec<i32>, Vec<i32>);
impl MyQueue {
    fn new() -> Self { Self(vec![], vec![]) }
    fn push(&mut self, x: i32) { self.0.push(x); }
    fn pop(&mut self) -> i32 { self.peek(); self.1.pop().unwrap() }
    fn peek(&mut self) -> i32 {
      if self.1.is_empty() { self.1.extend(self.0.drain(..).rev()); }
      *self.1.last().unwrap()
    }
    fn empty(&self) -> bool { self.0.len() + self.1.len() == 0 }
}

28.01.2024

1074. Number of Submatrices That Sum to Target hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/486

Problem TLDR

Count submatrix target sums.

Intuition

Precompute prefix sums, then calculate submatrix sum in O(1).

Approach

  • use [n+1][m+1] to avoid ifs
  • there are O(n^3) solution exists

Complexity

  • Time complexity: \(O(n^4)\)

  • Space complexity: \(O(n^2)\)

Code


  fun numSubmatrixSumTarget(matrix: Array<IntArray>, target: Int): Int {
    val s = Array(matrix.size + 1) { IntArray(matrix[0].size + 1) }
    return (1..<s.size).sumOf { y -> (1..<s[0].size).sumOf { x ->
      s[y][x] = matrix[y - 1][x - 1] + s[y - 1][x] + s[y][x - 1] - s[y - 1][x - 1] 
      (0..<y).sumOf { y1 -> (0..<x).count { x1 ->
        target == s[y][x] - s[y1][x] - s[y][x1] + s[y1][x1]
      }}
    }}
  }


    pub fn num_submatrix_sum_target(matrix: Vec<Vec<i32>>, target: i32) -> i32 {
      let mut s = vec![vec![0; matrix[0].len() + 1]; matrix.len() + 1];
      (1..s.len()).map(|y| (1..s[0].len()).map(|x| {
        s[y][x] = matrix[y - 1][x - 1] + s[y - 1][x] + s[y][x - 1] - s[y - 1][x - 1];
        (0..y).map(|y1| (0..x).filter_map(|x1|
          if target == s[y][x] - s[y1][x] - s[y][x1] + s[y1][x1] { Some(1) } else { None }
        ).count() as i32).sum::<i32>()
      }).sum::<i32>()).sum()
    }

27.01.2024

629. K Inverse Pairs Array hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/485

Problem TLDR

Number of arrays of 1..n with k reversed order pairs.

Intuition

First step: write down all the arrays for some example n, for every possible k:


    // 1 2 3 4
    // f(4,1) = 3
    // 1 3 2 4 [.  .  .  32 .  . ] 3  f(3, 1) = 2 = 1 + f(2, 1)
    // 1 2 4 3 [43 .  .  .  .  . ] 4  f(2, 1) = 1
    // 2 1 3 4 [.  .  .  .  .  21] 2
    // f(4, 2) = 5
    // 1 3 4 2 [.  42 .  32 .  . ] 4  f(4, 2) = 1 + f(3, 1) + f(3, 2) = 1 + sum_j_k(f(2, j))
    // 1 4 2 3 [43 42 .  .  .  . ] 4  f(3, 2) = 1 + f(2, 1) = 2
    // 2 1 4 3 [43 .  .  .  .  21] 4  f(2, 2) = 0
    // 2 3 1 4 [.  .  .  .  31 21] 3  f(3, 2) = 2
    // 3 1 2 4 [.  .  .  32 31 . ] 3
    // f(4, 3) = 6
    // 1 4 3 2 [43 42 .  32 .  . ] 4  f(4, 3) = 1 + f(3, 1) + f(3, 2) + f(3, 3) 
    // 2 3 4 1 [.  .  41 .  31 21] 4  
    // 2 4 1 3 [43 .  41 .  .  21] 4
    // 3 1 4 2 [.  42 .  32 31 . ] 4
    // 3 2 1 4 [.  .  .  32 31 21] 3  f(3, 3) = 1
    // 4 1 2 3 [43 42 41 .  .  . ] 4
    // f(4, 4) = 5
    // 2 4 3 1 [43 .  41 .  31 21] 4
    // 3 2 4 1 [.  .  41 32 31 21] 4  f(4, 4) = f(3, 1) + f(3, 2) + f(3, 3) + f(3, 4)
    // 3 4 1 2 [.  42 41 32 31 . ] 4  f(3, 4) = 0
    // 4 1 3 2 [43 42 41 32 .  . ] 4
    // 4 2 1 3 [43 42 41 .  .  21] 4
    // f(4, 5) = 3
    // 3 4 2 1 [.  42 41 32 31 21] 4  f(4, 5) = f(3, 2) + f(3, 3)
    // 4 3 1 2 [43 42 41 32 31 . ] 4
    // 4 2 3 1 [43 42 41 .  31 21] 4
    // f(4, 6) = 1
    // 4 3 2 1 [43 42 41 32 31 21] 4  f(4, 6) = f(3, 3) = 1
    //                                             f(5, 10) = 1
    // f(5, x)  = 1, x = 6 + 4 = 10, f(5, 10) = 1, f(5, 9) = f(4, 6) + f(4, 5) = 1+3=4
    // f(6, 15) = 1                                f(5, 8) = f(4, 6) + f(4, 5) + f(4, 4) = 1+3+5=9
    // f(7, 21) = 1                                f(5, 7) = f(5, 8) + f(4, 3) = 9+6=15
    // f(8, 28) = 1                                f(5, 6) = f(5, 7) + f(4, 2) = 15+5 =20
    //                                             f(5, 5) = f(5, 6) + f(4, 1) = 20+3=23--->22
    //                                             f(5, 4) = f(5, 5) + 1 = 24--->20
    //                                             f(5, 3) = 1 + f(4,1)+f(4,2)+f(4,3) = 1+3+5+6=15
    //                                             f(5, 2) = 1 + f(4,1) + f(4, 2) = 1+3+5 = 9
    //                                             f(5, 1) = 1 + f(4, 1)= 1+3=4
    //                                             f(5, 0) = 1
    // f(0) = 0
    // f(1) = 1
    // f(2) = 1 1
    // f(3) = 1 2 2                       1
    //        0 1 2          3        4 5 6
    //    
    // 1 2 2 1
    //         1 2 2 1
    // 1 3 5 6 5 3 1
    // f(4) = 1 3 5         (6)       5 3 1  1=0+1,3=1+2,5=3+2,6=5+1,5=6-1,3=5-2,1=3-2,0=1-1
    // +      1 3 4  6   5  3  1
    // -                    1  3   5  6 5 3
    //        0 1 2  3   4  5  6   7  8 9 10
    //
    // 1 3 5 6  5  3  1
    //             1  3  5  6 5 3 1
    // 1 4 9 15 20 22 20 15 9 4 1
    // 0 1 2 3  4  5  6  7  8 9 10
    //             5 = 10 - (7 - 2)
    // f(5) = 1 4 9  15 (20 22 20) 15 9 4 1  20 = 15+5, 22 = 20+3-1, 20=22+1-3, 15=20-5, 9=15-6, 4=9-5, 1=4-3
    // f(6) = 1 5 14 28  48 70 90 105    ???                                            10590 70 48 28 14 5  1
    // f(7) = 1 6 20 48
    //                               f(6, 15) = 1
    // f(9, 36) = 1                  f(6, 14) = f(5, 10) + f(5, 9) = 1+4 = 5
    //                               f(6, 13) = f(5, 10) + f(5, 9) + f(5, 8) = 5+9=14
    //                               f(6, 12) = 
    // [15..]+
    // [..15]-
    // [ 21 ]

After several hours (3 in my case) of staring at those numbers the idea should came to your mind: there is a pattern. For every n, if all the numbers are reversed, then there are exactly Fibonacci(n) reversed pairs:

// f(5, x)  = 1, x = 6 + 4 = 10, f(5, 10) = 1
// f(6, 15) = 1
// f(7, 21) = 1 
// f(8, 28) = 1

Another pattern is how we make a move in n space:

f(3, 1) = 2 = 1 + f(2, 1)
f(4, 2) = 1 + f(3, 1) + f(3, 2) = 1 + sum_j_k(f(2, j))
f(3, 2) = 1 + f(2, 1) = 2
f(4, 3) = 1 + f(3, 1) + f(3, 2) + f(3, 3)
f(4, 4) = f(3, 1) + f(3, 2) + f(3, 3) + f(3, 4)

It almost works, until it not: at some point pattern breaks, so search what is it. Let’s write all the k numbers for each n:

f(0) = 0
f(1) = 1
f(2) = 1 1
f(3) = 1 2 2 1
f(4) = 1 3 5 6 5 3 1
f(5) = 1 4 9 15 20 22 20 15 9 4 1

There is a symmetry and we can deduce it by intuition: add the previous and at some point start to remove:

    // 1 2 2 1
    //         1 2 2 1
    // 1 3 5 6 5 3 1


    // 1 3 5 6  5  3  1
    //             1  3  5  6 5 3 1
    // 1 4 9 15 20 22 20 15 9 4 1

Now, the picture is clear. At some index we must start to remove the previous sequence.

We are not finished yet, however: solution will give TLE. Fibonacci became too big. So, another hint: numbers after k doesn’t matter.

Approach

This is a filter problem: it filters you.

  • we can hold only k numbers
  • we can ping-pong swap two dp arrays

Complexity

  • Time complexity: \(O(nk)\)

  • Space complexity: \(O(k)\)

Code


  fun kInversePairs(n: Int, k: Int): Int {
    var fib = 1
    var prev = LongArray(k + 1).apply { this[0] = 1 }
    var curr = LongArray(k + 1)
    repeat(n) {
      fib = fib + it
      var c = 0L
      for (x in 0..k) {
        if (x < fib - it) c += prev[x]
        if (x - it > 0) c -= prev[x - it - 1]
        curr[x] = (c + 1_000_000_007L) % 1_000_000_007L
      }
      prev = curr.also { curr = prev }
    }
    return if (k >= fib) 0 else prev[k].toInt()
  }


  pub fn k_inverse_pairs(n: i32, k: i32) -> i32 {
    let mut fib = 1;
    let mut prev = vec![1; (k + 1) as usize];
    let mut curr = vec![1; (k + 1) as usize];
    for i in 0..n {
      fib = fib + i;
      let mut c = 0i64;
      for x in 0..=k {
        if x < fib - i { c += prev[x as usize]; }
        if x - i > 0 { c -= prev[(x - i - 1) as usize]; }
        curr[x as usize] = (c + 1_000_000_007) % 1_000_000_007;
      }
      std::mem::swap(&mut prev, &mut curr);
    }
    if k >= fib { 0 } else { prev[k as usize] as i32 }
  }

26.01.2024

576. Out of Boundary Paths medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/484

Problem TLDR

Number of paths from cell in grid to out of boundary.

Intuition

Let’s do a Brute-Force Depth-First Search from the current cell to neighbors. If we are out of boundary, we have a 1 path, and 0 if moves are out. Then add memoization with a HashMap.

Approach

  • using long helps to shorten the code

Complexity

  • Time complexity: \(O(nmv)\)

  • Space complexity: \(O(nmv)\)

Code


  fun findPaths(m: Int, n: Int, maxMove: Int, startRow: Int, startColumn: Int): Int {
    val dp = mutableMapOf<Pair<Pair<Int, Int>, Int>, Long>()
    fun dfs(y: Int, x: Int, move: Int): Long = dp.getOrPut(y to x to move) {
      if (y < 0 || x < 0 || y == m || x == n) 1L
      else if (move <= 0) 0L else
      dfs(y - 1, x, move - 1) + 
      dfs(y + 1, x, move - 1) + 
      dfs(y, x - 1, move - 1) + 
      dfs(y, x + 1, move - 1) } % 1_000_000_007L
    return dfs(startRow, startColumn, maxMove).toInt()
  }



  pub fn find_paths(m: i32, n: i32, max_move: i32, start_row: i32, start_column: i32) -> i32 {
      let mut dp = HashMap::new();
      fn dfs( y: i32,  x: i32,  mov: i32,  m: i32,  n: i32,  dp: &mut HashMap<(i32, i32, i32), i64> ) -> i64 {
        if y < 0 || x < 0 || y == m || x == n { 1 } else if mov<= 0 { 0 } else {
            if let Some(&cache) = dp.get(&(y, x, mov)) { cache } else {
              let result = (dfs(y - 1, x, mov - 1, m, n, dp) +
                            dfs(y + 1, x, mov - 1, m, n, dp) +
                            dfs(y, x - 1, mov - 1, m, n, dp) +
                            dfs(y, x + 1, mov - 1, m, n, dp)) % 1_000_000_007;
              dp.insert((y, x, mov), result); result
            }
        }
    }
    dfs(start_row, start_column, max_move, m, n, &mut dp) as i32
  }

25.01.2024

1143. Longest Common Subsequence medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/483

Problem TLDR

Longest common subsequence of two strings.

Intuition

We can start from a brute force solution: given the current positions i and j we take them into common if text1[i] == text2[j] or choose between taking from text1[i] and text2[j] if not. The result will only depend on the current positions, so can be cached. From this, we can rewrite the solution to iterative version.

Approach

  • use len + 1 dp size to avoid boundary checks
  • forward iteration is faster, but dp[0][0] must be the out of boundary value
  • fold can save us some lines of code
  • there is a 1D-memory dp solution exists

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


  fun longestCommonSubsequence(text1: String, text2: String): Int {
    val dp = Array(text1.length + 1) { IntArray(text2.length + 1) }
    for (i in text1.lastIndex downTo 0)
      for (j in text2.lastIndex downTo 0)
        dp[i][j] = if (text1[i] == text2[j]) 
          1 + dp[i + 1][j + 1] else
          max(dp[i + 1][j], dp[i][j + 1])
    return dp[0][0]
  }


  pub fn longest_common_subsequence(text1: String, text2: String) -> i32 {
    let mut dp = vec![vec![0; text2.len() + 1]; text1.len() + 1];
    text1.bytes().enumerate().fold(0, |_, (i, a)|
      text2.bytes().enumerate().fold(0, |r, (j, b)| {
        let l = if a == b { 1 + dp[i][j] } else { dp[i][j + 1].max(r) };
        dp[i + 1][j + 1] = l; l
      })
    )
  }

24.01.2024

1457. Pseudo-Palindromic Paths in a Binary Tree medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/482

Problem TLDR

Count can-form-a-palindrome paths root-leaf in a binary tree.

Intuition

Let’s walk a binary tree with Depth-First Search and check the frequencies in path’s numbers. To form a palindrome, only a single frequency can be odd.

Approach

  • only odd-even matters, so we can store just boolean flags mask

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\)

Code


  fun pseudoPalindromicPaths (root: TreeNode?): Int {
    fun dfs(n: TreeNode?, freq: Int): Int = n?.run {
      val f = freq xor (1 shl `val`)
      if (left == null && right == null) {
        if (f and (f - 1) == 0) 1 else 0
      } else dfs(left, f) + dfs(right, f)
    } ?: 0
    return dfs(root, 0)
  }


  pub fn pseudo_palindromic_paths (root: Option<Rc<RefCell<TreeNode>>>) -> i32 {
    fn dfs(n: &Option<Rc<RefCell<TreeNode>>>, freq: i32) -> i32 {
      n.as_ref().map_or(0, |n| {
        let n = n.borrow();
        let f = freq ^ (1 << n.val);
        dfs(&n.left, f) + dfs(&n.right, f) + 
          (n.left.is_none() && n.right.is_none() && (f & (f - 1) == 0)) as i32
      })
    }
    dfs(&root, 0)
  }

23.01.2024

1239. Maximum Length of a Concatenated String with Unique Characters medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/481

Problem TLDR

Max length subsequence of strings array with unique chars.

Intuition

Let’s do a brute-force Depth-First Search and keep track of used chars so far.

Approach

  • we must exclude all strings with duplicate chars
  • we can use bit masks, then mask xor word must not be equal mask or word for them not to intersect

Complexity

  • Time complexity: \(O(2^n)\)

  • Space complexity: \(O(n)\)

Code


  fun maxLength(arr: List<String>): Int {
    val sets = arr.filter { it.toSet().size == it.length }
    fun dfs(i: Int, s: Set<Char>): Int = if (i == sets.size) 0
      else max(
        if (sets[i].any { it in s }) 0 else
        sets[i].length + dfs(i + 1, s + sets[i].toSet()),
        dfs(i + 1, s)
      )
    return dfs(0, setOf())
  }


  pub fn max_length(arr: Vec<String>) -> i32 {
    let bits: Vec<_> = arr.into_iter()
      .filter(|s| s.len() == s.chars().collect::<HashSet<_>>().len())
      .map(|s| s.bytes().fold(0, |m, c| m | 1 << (c - b'a')))
      .collect();
    fn dfs(bits: &[i32], i: usize, mask: i32) -> i32 {
      if i == bits.len() { 0 } else {
      dfs(bits, i + 1, mask).max(
        if (bits[i] | mask != bits[i] ^ mask) { 0 } else 
        { bits[i].count_ones() as i32 + dfs(bits, i + 1, mask | bits[i]) }
      )}
    }
    dfs(&bits, 0, 0)
  }

22.01.2024

645. Set Mismatch easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/480

Problem TLDR

Return missing and duplicated number in 1..n array with one number replaced.

Intuition

First try to find a xor solution by observing xor differencies. Then give up and just compare sorted order or even better HashSet with expected.

Approach

  • delta sums is a trivial approach, use it in an interview
  • learn about xor solution (homework)

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun findErrorNums(nums: IntArray) = with(nums) {
    val missing = ((1..size) - toSet()).first()
    val delta = sum() - (size + 1) * size / 2
    intArrayOf(missing + delta, missing)
  }


  pub fn find_error_nums(nums: Vec<i32>) -> Vec<i32> {
    let sz = nums.len() as i32;
    let sum: i32 = nums.iter().sum();
    let set_sum: i32 = nums.into_iter().collect::<HashSet<_>>().iter().sum();
    vec![sum - set_sum, sz * (sz + 1) / 2 - set_sum]
  }

21.01.2024

198. House Robber medium blog post substack youtube image.png

https://youtu.be/UeejjxR-skM

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/479

Problem TLDR

Max sum to rob non adjacent items in array.

Intuition

Let’s inspect how robber acts by scanning array home by home:

  // 2 7 9 3 1
  // 2          max(2) = 2
  //   7        max(7, 2) = 7
  // b a 9      max(9 + b, a) = 11
  //   b a 3    max(3 + b, a) = 11
  //     b a 1  max(1 + b, a) = 12

We see that he can choose to take the current home and drop the previous, or keep the previous. Only the two last sums matter.

Approach

  • save some lines of code by using fold

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun rob(nums: IntArray) = 
    nums.fold(0 to 0) { (a, b), x -> max(x + b, a) to a }.first


    pub fn rob(nums: Vec<i32>) -> i32 {
      nums.iter().fold((0, 0), |(a, b), &x| (b, b.max(a + x))).1
    }

20.01.2024

907. Sum of Subarray Minimums medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/478

Problem TLDR

Sum of minimums of all array ranges.

Intuition

To build an intuition, we must write some examples where numbers will increase and decrease. Next, write down all the subarrays and see how the result differs when we add another number. Let g[i] be minimums for all subarrays [i..]. Result for all subarrays is f[i] = g[i] + f[i + 1]. Now, let’s find how g can be split into subproblems:

  //   5 2           1
  //   0 1 2 3 4 5 6 7 8 910111213
  // g(5 2 3 5 4 3 2 1 3 2 3 2 1 4) = 5 + g(2 3 5 4 3 2 1 3 2 3 2 1 4)
  //   2           1
  // g(2 3 5 4 3 2 1 3 2 3 2 1 4) = 2 + g(2 2 2 2) + g(2 1 3 2 3 2 1 4)
  //   3       2 1
  // g(3 5 4 3 2 1 3 2 3 2 1 4) = 3 + g(3 3) + g(3 2 1 3 2 3 2 1 4)
  //   5 4 3 2 1
  // g(5 4 3 2 1 3 2 3 2 1 4) = 5 + g(4 3 2 1 3 2 3 2 1 4)
  //   4 3 2 1
  // g(4 3 2 1 3 2 3 2 1 4) = 4 + g(3 2 1 3 2 3 2 1 4)
  //   3 2 1
  // g(3 2 1 3 2 3 2 1 4) = 3 + g(2 1 3 2 3 2 1 4)
  //   2 1
  // g(2 1 3 2 3 2 1 4) = 2 + g(1 3 2 3 2 1 4)

Notice the pattern: if next value (right to left) is bigger, we just reuse previous g, but if it is smaller, we need to find closest positions and replace all the numbers to arr[i]. To do this step in O(1) there is a known Increasing Stack technique: put values that bigger and each smaller value will discard all larger numbers.

Approach

  • use index size to store absent value and safely access g[j]
  • use fold to reduce some lines of code

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun sumSubarrayMins(arr: IntArray) = with(Stack<Int>()) {
    val g = IntArray(arr.size + 1)
    (arr.lastIndex downTo 0).fold(0) { prev, i ->
      while (isNotEmpty() && arr[peek()] >= arr[i]) pop()
      val j = if (isEmpty()) arr.size else peek()
      g[i] = (j - i) * arr[i] + g[j]
      push(i)
      (prev + g[i]) % 1_000_000_007
    }
  }


    pub fn sum_subarray_mins(arr: Vec<i32>) -> i32 {
      let (mut s, mut g) = (Vec::new(), vec![0; arr.len() + 1]);
      arr.iter().enumerate().rev().fold(0, |f, (i, &v)| {
        while s.last().map_or(false, |&j| arr[j] >= v) { s.pop(); }
        let j = *s.last().unwrap_or(&arr.len());
        g[i] = (j - i) as i32 * v + g[j];
        s.push(i);
        (f + g[i]) % 1_000_000_007
      })
    }

19.01.2024

931. Minimum Falling Path Sum medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/477

Problem TLDR

Min sum moving bottom center, left, right in 2D matrix.

Intuition

At every cell we must add it value to path plus min of a three direct top cells as they are the only way here.

Approach

We can reuse the matrix or better use separate temporal array.

Complexity

  • Time complexity: \(O(mn)\)

  • Space complexity: \(O(1)\), or O(m) to not corrupt the inputs

Code


    fun minFallingPathSum(matrix: Array<IntArray>): Int {
        for (y in 1..<matrix.size) for (x in 0..<matrix[0].size)
            matrix[y][x] += (max(0, x - 1)..min(x + 1, matrix[0].lastIndex))
                .minOf { matrix[y - 1][it] }
        return matrix.last().min()
    }


    pub fn min_falling_path_sum(matrix: Vec<Vec<i32>>) -> i32 {
        *matrix.into_iter().reduce(|dp, row|
            row.iter().enumerate().map(|(x, &v)|
                v + dp[x.max(1) - 1..=(x + 1).min(dp.len() - 1)]
                    .iter().min().unwrap()
            ).collect()
        ).unwrap().iter().min().unwrap()
    }

18.01.2024

70. Climbing Stairs easy blog post substack youtube image.png image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/476

Problem TLDR

Ways to climb n stairs by 1 or 2 steps.

Intuition

Start with brute force DFS search: either go one or two steps and cache the result in a HashMap<Int, Int>. Then convert solution to iterative version, as only two previous values matter.

Approach

  • no need to check if n < 4
  • save some lines of code with also

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun climbStairs(n: Int): Int {
    var p = 0
    var c = 1
    for (i in 1..n) c += p.also { p = c }
    return c
  }


    pub fn climb_stairs(n: i32) -> i32 {
      (0..n).fold((0, 1), |(p, c), _| (c, p + c)).1
    }

17.01.2024

1207. Unique Number of Occurrences easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/474

Problem TLDR

Are array frequencies unique.

Intuition

Just count frequencies.

Approach

Let’s use some Kotlin’s API:

  • asList
  • groupingBy
  • eachCount
  • groupBy
  • run

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun uniqueOccurrences(arr: IntArray) =
      arr.asList().groupingBy { it }.eachCount().values.run {
        toSet().size == size
      }


  pub fn unique_occurrences(arr: Vec<i32>) -> bool {
    let occ = arr.iter().fold(HashMap::new(), |mut m, &x| {
      *m.entry(x).or_insert(0) += 1; m
    });
    occ.len() == occ.values().collect::<HashSet<_>>().len()
  }

16.01.2024

380. Insert Delete GetRandom O(1) medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/473

Problem TLDR

Implement HashSet with random method.

Intuition

There is a random method exists in Kotlin’s MutableSet https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/random.html.

However, let’s just use array to store values and save positions in a HashMap. The order in array didn’t matter, so we can remove elements in O(1).

Approach

To save some symbols of code, we can extend from ArrayList.

Complexity

  • Time complexity: \(O(1)\), per operation

  • Space complexity: \(O(1)\), per operation

Code


class RandomizedSet(): ArrayList<Int>() {
  val vToPos = HashMap<Int, Int>()
  fun insert(v: Int): Boolean {
    if (vToPos.contains(v)) return false
    add(v)
    vToPos[v] = lastIndex
    return true
  }
  override fun remove(v: Int): Boolean {
    val pos = vToPos.remove(v) ?: return false
    set(pos, last())
    if (last() != v) vToPos[last()] = pos
    removeLast()
    return true
  }
  fun getRandom() = random()
}


use rand::{thread_rng, Rng};
use std::collections::HashMap;

struct RandomizedSet {
  vec: Vec<i32>,
  v_to_i: HashMap<i32, usize>,
}

impl RandomizedSet {

  fn new() -> Self {
    Self { vec: vec![], v_to_i: HashMap::new() }
  }
  
  fn insert(&mut self, v: i32) -> bool {
    if self.v_to_i.entry(v).or_insert(self.vec.len()) != &self.vec.len() {
      return false;
    }
    self.vec.push(v);
    true
  }
  
  fn remove(&mut self, v: i32) -> bool {
    self.v_to_i.remove(&v).map_or(false, |i| {
      let last = self.vec.pop().unwrap();
      if (last != v) {
        self.vec[i] = last;
        self.v_to_i.insert(last, i);
      }
      true
    })
  }
  
  fn get_random(&self) -> i32 {
    self.vec[thread_rng().gen_range(0, self.vec.len())]
  }
}

15.01.2024

2225. Find Players With Zero or One Losses medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/472

Problem TLDR

[sorted winners list, sorted single lose list]

Intuition

No special algorithms here, just a set manipulation.

Approach

Let’s use some Kotlin’s API:

  • map
  • groupingBy
  • eachCount
  • filter
  • sorted

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun findWinners(matches: Array<IntArray>) = buildList {
    val winners = matches.map { it[0] }.toSet()
    val losers = matches.groupingBy { it[1] }.eachCount()
    add((winners - losers.keys).sorted())
    add(losers.filter { (k, v) -> v == 1 }.keys.sorted())
  }

14.01.2024

1657. Determine if Two Strings Are Close medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/471

Problem TLDR

Are strings convertible by swapping existing chars positions or frequencies.

Intuition

By the problem definition, we must compare the frequencies numbers. Also, sets of chars must be equal.

Approach

Let’s use some Kotlin’s API:

  • groupingBy
  • eachCount
  • run
  • sorted

Complexity

  • Time complexity: \(O(n)\), as we are sorting only 26 elements

  • Space complexity: \(O(n)\)

Code


  fun String.f() = groupingBy { it }.eachCount()
    .run { keys to values.sorted() }
  fun closeStrings(word1: String, word2: String) =
    word1.f() == word2.f()

13.01.2024

1347. Minimum Number of Steps to Make Two Strings Anagram medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/470

Problem TLDR

Min operations to make string t anagram of s.

Intuition

Let’s compare char’s frequencies of those two strings.

Approach

  • careful: as we replacing one kind of chars with another, we must decrease that another counter

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun minSteps(s: String, t: String) = 
    IntArray(128).let {
      for (c in s) it[c.toInt()]++
      for (c in t) it[c.toInt()]--
      it.sumOf { abs(it) } / 2
    }

12.01.2024

1704. Determine if String Halves Are Alike easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/469

Problem TLDR

https://t.me/leetcode_daily_unstoppable/469

Approach

Let’s use some Kotlin’s API:

  • toSet
  • take
  • drop
  • count

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can be O(1) with asSequence

Code


  val vw = "aeiouAEIOU".toSet()
  fun halvesAreAlike(s: String) = 
    s.take(s.length / 2).count { it in vw } == 
    s.drop(s.length / 2).count { it in vw }

11.01.2024

1026. Maximum Difference Between Node and Ancestor medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/468

Problem TLDR

Max diff between node and ancestor in a binary tree.

Intuition

Let’s traverse the tree with Depth-First Search and keep track of the max and min values.

Approach

  • careful with corner case: min and max must be in the same ancestor-child hierarchy
  • we can use external variable, or put it in each result

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\)

Code


  fun maxAncestorDiff(root: TreeNode?): Int {
    var res = 0
    fun dfs(n: TreeNode?): List<Int> = n?.run {
      (dfs(left) + dfs(right) + listOf(`val`)).run { 
        listOf(min(), max()).onEach { res = max(res, abs(`val` - it)) }
      }
    } ?: listOf()
    dfs(root)
    return res
  }

10.01.2024

2385. Amount of Time for Binary Tree to Be Infected medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/467

Problem TLDR

Max distance from node in a Binary Tree.

Intuition

Let’s build a graph, then do a Breadth-First Search from starting node.

Approach

We can store it in a parent[TreeNode] map or just in two directional node to list<node> graph.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun amountOfTime(root: TreeNode?, start: Int): Int {
    val fromTo = mutableMapOf<TreeNode, MutableList<TreeNode>>()
    var queue = ArrayDeque<TreeNode>()
    val visited = mutableSetOf<TreeNode>()
    fun dfs(n: TreeNode): Unit = with (n) {
      if (`val` == start) {
        queue.add(n)
        visited.add(n)
      }
      left?.let { 
        fromTo.getOrPut(n) { mutableListOf() } += it
        fromTo.getOrPut(it) { mutableListOf() } += n
        dfs(it)
      }
      right?.let { 
        fromTo.getOrPut(n) { mutableListOf() } += it
        fromTo.getOrPut(it) { mutableListOf() } += n
        dfs(it)
      }
    }
    root?.let { dfs(it) }
    var time = -1
    while (queue.isNotEmpty()) {
      repeat(queue.size) {
        var x = queue.removeFirst()
        fromTo[x]?.onEach { 
          if (visited.add(it)) queue.add(it)
        }
      }
      time++
    }
    return time
  }

9.01.2024

872. Leaf-Similar Trees easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/466

Problem TLDR

Are leafs sequences equal for two trees.

Intuition

Let’s build a leafs lists and compare them.

Approach

Let’s use recursive function.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun leafs(n: TreeNode?): List<Int> = n?.run {
    (leafs(left) + leafs(right))
    .takeIf { it.isNotEmpty() } ?: listOf(`val`)
  } ?: listOf()
  fun leafSimilar(root1: TreeNode?, root2: TreeNode?) =
    leafs(root1) == leafs(root2)

8.01.2024

938. Range Sum of BST easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/465

Problem TLDR

Sum of BST in range [low..high].

Intuition

Let’s iterate it using a Depth-First Search and check if each value is in the range.

Approach

  • Careful: if the current node is out of range, we still must visit its children.
  • However, we can prune visit on the one side

Complexity

  • Time complexity: \(O(r)\), r is a range

  • Space complexity: \(O(log(n))\)

Code


  fun rangeSumBST(root: TreeNode?, low: Int, high: Int): Int =
   root?.run {
      (if (`val` in low..high) `val` else 0) +
      (if (`val` > low) rangeSumBST(left, low, high) else 0) + 
      (if (`val` < high) rangeSumBST(right, low, high) else 0)
    } ?: 0

7.01.2024

446. Arithmetic Slices II - Subsequence hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/464

Problem TLDR

Count of arithmetic subsequences.

Intuition

We can take every pair and search for the third element. The result only depends on the diff and suffix array position, so can be cached.

Approach

  • be careful how to count each new element: first add the 1 then add the suffix count. Wrong approach: just count the 1 at the end of the sequence.

Complexity

  • Time complexity: \(O(n^2)\), it looks like n^4, but the dfs n^2 part will only go deep once

  • Space complexity: \(O(n^2)\)

Code


  fun numberOfArithmeticSlices(nums: IntArray): Int {
    val dp = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(i: Int, k: Int): Int = dp.getOrPut(i to k) {
      var count = 0
      for (j in i + 1..<nums.size) 
        if (nums[i].toLong() - nums[k] == nums[j].toLong() - nums[i])
          count += 1 + dfs(j, i)
      count
    }
    var count = 0
    for (i in nums.indices)
      for (j in i + 1..<nums.size)
        count += dfs(j, i)
    return count
  }

6.01.2024

1235. Maximum Profit in Job Scheduling hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/463

Problem TLDR

Max profit in non-intersecting jobs given startTime[], endTime[] and profit[].

Intuition

Start with sorting jobs by the startTime. Then let’s try to find a subproblem: consider the only last element - it has maximum profit in itself. Then, move one index left: now, if we take the element, we must drop all the intersected jobs. Given that logic, there is a Dynamic Programming recurrence: dp[i] = max(dp[i + 1], profit[i] + dp[next]).

The tricky part is how to faster find the next non-intersecting position: we can use the Binary Search

Approach

Try to solve the problem for examples, there are only several ways you could try: greedy or dp. After 1 hour, use the hints.

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun jobScheduling(startTime: IntArray, endTime: IntArray, profit: IntArray): Int {
    val inds = startTime.indices.sortedBy { startTime[it] }
    val dp = IntArray(inds.size + 1)
    for (i in inds.indices.reversed()) {
      var lo = i + 1
      var hi = inds.lastIndex
      while (lo <= hi) {
        val m = lo + (hi - lo) / 2
        if (endTime[inds[i]] > startTime[inds[m]]) lo = m + 1 else hi = m - 1
      }
      dp[i] = max(dp[i + 1], profit[inds[i]] + dp[lo])
    }
    return dp[0]
  }

5.01.2024

300. Longest Increasing Subsequence medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/462

Problem TLDR

Longest increasing subsequence length.

Intuition

This is a classical problem that has the optimal algorithm that you must know https://en.wikipedia.org/wiki/Longest_increasing_subsequence.

For every new number, check its position in an increasing sequence by Binary Search:

  • already in a sequence, do nothing
  • bigger than the last, insert
  • interesting part: in the middle, replace the insertion position (next after the closest smaller)
increasing sequence 
1 3 5 7 9           insert 6
      ^

1 3 5 6 9 

As we do not care about the actual numbers, only the length, this would work. (To restore the actual subsequence, we must remember each predecessor, see the wiki)

Approach

If you didn’t remember how to restore the insertion point from binarySearch (-i-1), better implement it yourself:

  • use inclusive lo and hi
  • always check the result `if (x == nums[mid]) pos = mid
  • always move the borders lo = mid + 1, hi = mid - 1

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun lengthOfLIS(nums: IntArray): Int {
      val seq = mutableListOf<Int>()
      for (x in nums) 
        if (seq.isEmpty()) seq += x else {
          var i = seq.binarySearch(x)
          if (i < 0) i = -i - 1
          if (i == seq.size) seq += x else seq[i] = x
        }
      return seq.size
    }

4.01.2024

2870. Minimum Number of Operations to Make Array Empty medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/461

Problem TLDR

Minimum pairs or triples duplicate removal operations to empty array of numbers.

Intuition

The first idea, is to count each kind of number. Then we must analyze each frequency: the number of removal operations ops will be the same for each f, so we can write a Dynamic Programming recurrent formula: ops(f) = 1 + min(ops(f - 2), ops(f - 3)). This is an accepted solution.

Then, we can think about other ways to optimally split f into a sum of a*2 + b*3: we must maximize b and minimize a. To do that, let’s prioritize f % 3 == 0 check. Our checks will be in this order:

f % 3 == 0 -> f / 3
(f - 2) % 3 == 0 -> 1 + f / 2
((f - 2) - 2) % 3 == 0 -> 1 + f / 2
... and so on

However, we can spot that recurrence repeat itself like this: f, f - 2, f - 4, f - 6, .... As 6 is also divisible by 3, there are total three checks needed: f % 3, (f - 2) % 3 and (f - 4) % 3.

Approach

Write the recurrent DFS function, then add a HashMap cache, then optimize everything out. Use the Kotlin’s API:

  • groupBy
  • mapValues
  • sumOf

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun minOperations(nums: IntArray) = nums
    .groupBy { it }.mapValues { it.value.size }.values
    .sumOf { f -> when {
      f < 2 -> return -1
      f % 3 == 0 -> f / 3
      (f - 2) % 3 == 0 || (f - 4) % 3 == 0 -> 1 + f / 3
      else -> return -1
    }}

3.01.2024

2125. Number of Laser Beams in a Bank medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/460

Problem TLDR

Beams count between consequent non-empty row’s 1s.

Intuition

By the problem definition, count = sum_i_j(count_i * count_j)

Approach

Let’s use some Kotlin’s API:

  • map
  • filter
  • windowed
  • sum

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(n)\), can be reduced to O(1) with asSequence and fold.

Code


  fun numberOfBeams(bank: Array<String>) =
    bank.map { it.count { it == '1' } }
      .filter { it > 0 }
      .windowed(2)
      .map { (a, b) -> a * b }
      .sum() ?: 0

2.01.2024

2610. Convert an Array Into a 2D Array With Conditions medium blog post substack youtube ![image.png](https://assets.leetcode.com/users/images/78cf9bd1-967d-4de2-9948-c311f56960b1_1704183026.395581.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/459

Problem TLDR

Convert numbers array into array of unique number-rows.

Intuition

Let’s count each kind of number, then use each unique number to build the rows.

Approach

Kotlin’s API can be helpful:

  • groupBy
  • mapValues
  • buildList

Complexity

  • Time complexity: \(O(uf)\) where, u - number of uniq elements, f - max frequency. Worst case O(n^2): 1 2 3 4 1 1 1 1, u = n / 2, f = n / 2. This can be improved to O(n) by removing the empty collections from freq.

  • Space complexity: \(O(n)\)

Code


  fun findMatrix(nums: IntArray): List<List<Int>> {
    val freq = nums.groupBy { it }
      .mapValues { it.value.toMutableList() }
    return buildList {
      repeat(freq.values.maxOf { it.size }) {
        add(buildList {
          for ((k, v) in freq) 
            if (v.isNotEmpty()) add(v.removeLast())
        })
      }
    }
  }

1.01.2024

455. Assign Cookies easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/458

Problem TLDR

Max count of greedy children g[Int] to assign cookies with sizes s[Int].

Intuition

The optimal way to assign cookies is to start with less greed. We can put cookies and children in two PriorityQueues or just sort two arrays and maintain two pointers.

Approach

  • PriorityQueue is a more error-safe solution, also didn’t modify the input.
  • Careful with the pointers, check yourself with simple examples: g=[1] s=[1], g=[2] s=[1]

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


  fun findContentChildren(g: IntArray, s: IntArray): Int {
    g.sort()
    s.sort()
    var j = 0
    return g.count { 
      while (j < s.size && s[j] < it ) j++
      j++ < s.size
    }
  }

31.12.2023

1624. Largest Substring Between Two Equal Characters easy blog post substack youtube image.png https://youtu.be/BF4M70PncfE

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/456

Problem TLDR

Max distance between same chars in string.

Intuition

We must remember the first occurrence position of each kind of character.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun maxLengthBetweenEqualCharacters(s: String) = 
    with(mutableMapOf<Char, Int>()) {
      s.indices.maxOf { it - 1 - getOrPut(s[it]) { it } }
    }

30.12.2023

1897. Redistribute Characters to Make All Strings Equal easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/455

Problem TLDR

Is it possible to split all the words[] characters into words.size groups.

Intuition

To understand the problem, consider example: a abc abbcc -> [abc] [abc] [abc]. We know the result words count, and we know the count of each kind of character. So, just make sure, every character’s count can be separated into words.size groups.

Approach

  • to better understand the problem, consider adding more examples
  • there can be more than one repeating character in group, [aabc] [aabc] [aabc]

Complexity

  • Time complexity: \(O(nw)\)

  • Space complexity: \(O(1)\)

Code


  fun makeEqual(words: Array<String>) = 
    IntArray(26).apply {
      for (w in words) for (c in w) this[c.toInt() - 'a'.toInt()]++
    }.all { it % words.size == 0 }

29.12.2023

1335. Minimum Difficulty of a Job Schedule hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/454

Problem TLDR

Min sum of maximums jobDifficulty[i] per day d preserving the order

Intuition

Let’s brute-force optimal interval of jobs jobInd..j for every day using Depth-First Search. The result will only depend on the starting jobInd and the current day, so can be cached.

Approach

  • pay attention to the problem description, preserving jobs order matters here

Complexity

  • Time complexity: \(O(dn^2)\), dn for the recursion depth and another n for the inner loop

  • Space complexity: \(O(dn)\)

Code


  fun minDifficulty(jobDifficulty: IntArray, d: Int): Int {
    val dp = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(jobInd: Int, day: Int): Int = when {
      jobInd == jobDifficulty.size -> if (day == d) 0 else Int.MAX_VALUE / 2
      day == d -> Int.MAX_VALUE / 2
      else -> dp.getOrPut(jobInd to day) {
        var max = 0
        (jobInd..jobDifficulty.lastIndex).minOf { i ->
          max = max(max, jobDifficulty[i])
          max + dfs(i + 1, day + 1)
        }
    }}
    return dfs(0, 0).takeIf { it < Int.MAX_VALUE / 2 } ?: -1
  }

28.12.2023

1531. String Compression II hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/453

Problem TLDR

Min length of run-length encoded aabcc -> a2bc3 after deleting at most k characters

Intuition

Let’s consider starting from every position, then we can split the problem: result[i] = some_function(i..j) + result[j].

The hardest part is to find an optimal j position.

The wrong way: trying to count how many s[j]==s[i], and to keep them, removing all other chars s[j]!=s[i]. This didn’t give us the optimal solution for s[i..j], as we forced to keep s[0].

The correct way: keeping the most frequent char in s[i..j], removing all other chars.

Approach

Spend 1-2.5 hours max on the problem, then steal someone else’s solution. Don’t feel sorry, it’s just a numbers game.

Complexity

  • Time complexity: \(O(kn^2)\)

  • Space complexity: \(O(kn)\)

Code


  fun getLengthOfOptimalCompression(s: String, k: Int): Int {
    val dp = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(i: Int, toRemove: Int): Int = 
      if (toRemove < 0) Int.MAX_VALUE / 2
      else if (i >= s.length - toRemove) 0
      else dp.getOrPut(i to toRemove) {
        val freq = IntArray(128)
        var mostFreq = 0
        (i..s.lastIndex).minOf { j ->
          mostFreq = max(mostFreq, ++freq[s[j].toInt()])
          when (mostFreq) {
            0 -> 0
            1 -> 1
            else -> mostFreq.toString().length + 1
          } + dfs(j + 1, toRemove - (j - i + 1 - mostFreq))
        }
      }
    return dfs(0, k)
  }

27.12.2023

1578. Minimum Time to Make Rope Colorful medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/452

Problem TLDR

Min sum of removed duplicates in array.

Intuition

The brute-force approach is to just consider keeping/remove every item, that can be cached in [size, 26] array.

However, there is a more optimal greedy solution: scan symbols one by one, and from each duplicate island remove the maximum of it.

Approach

Start from writing more verbose solution, keeping separate variables for currentSum, totalSum, and two separate conditions: if we meet a duplicate or not. Then optimize it out.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun minCost(colors: String, neededTime: IntArray): Int {
    var sum = 0
    var max = 0
    var prev = '.'
    for ((i, c) in colors.withIndex()) {
      sum += neededTime[i]
      if (prev != c) sum -= max.also { max = 0 }
      max = max(max, neededTime[i])
      prev = c
    }
    return sum - max
  }

26.12.2023

1155. Number of Dice Rolls With Target Sum medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/451

Problem TLDR

Ways to throw once n dices with k faces to make target sum.

Intuition

Let’s consider each dice and try all the possible face. By repeating the process for all the dices, check if the final sum is equal to the target. The result will only depend on the dice count and target sum, so it can be cached.

Approach

Write brute force DFS, than add HashMap or array cache.

Complexity

  • Time complexity: \(O(nkt)\), nt - is a DFS search space, k - is the iteration inside

  • Space complexity: \(O(nt)\)

Code


  fun numRollsToTarget(n: Int, k: Int, target: Int): Int {
    val dp = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(c: Int, s: Int): Int = 
      dp.getOrPut(c to s) { when {
          c == 0 -> if (s == 0) 1 else 0
          s <= 0 -> 0
          else -> (1..k).fold(0) { ways, d ->
            (ways + dfs(c - 1, s - d)) % 1_000_000_007
          }
      } }

    return dfs(n, target)
  }

25.12.2023

91. Decode Ways medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/450

Problem TLDR

Ways to decode back ‘A’ -> ‘1’, ‘B’ -> ‘2’ … ‘Z’ -> ‘26’

Intuition

Let’s consider each position and do a DFS to check how many successfull paths exist.

For each position, we know the answer for the rest of the string, so it can be cached.

Approach

Start from implementing brute-force DFS, consider two cases: take just one char and take two chars. After that, introduce the cache, it can be an array or a HashMap<position, result>. Extra step, is to notice, the current value only depends on the two next values, so rewrite DFS into a reversed loop and store two previous results. The boss step is to do some code golf.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun numDecodings(s: String): Int =
    s.indices.reversed().fold(0 to 1) { (prev, curr), i ->
      curr to if (s[i] == '0') 0 else
      curr + if (s.drop(i).take(2).toInt() in 10..26) prev else 0
    }.second

24.12.2023

1758. Minimum Changes To Make Alternating Binary String easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/449

TLDR

Minimum operations to make 01-string with no two adjacent equal

Intuition

There are only two possible final variations - odd zeros even ones or even zeros odd ones. We can count how many positions to changes for each of them, then return smallest counter.

Approach

In a stressfull situation better to just use 4 counters: oddOnes, evenOnes, oddZeros, evenZeros. Then do something with them.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun minOperations(s: String): Int {
    var oddOnesEvenZeros = 0
    var oddZerosEvenOnes = 0
    for (i in s.indices) when {
      s[i] == '0' && i % 2 == 0 -> oddZerosEvenOnes++
      s[i] == '0' && i % 2 != 0 -> oddOnesEvenZeros++
      s[i] == '1' && i % 2 == 0 -> oddOnesEvenZeros++
      s[i] == '1' && i % 2 != 0 -> oddZerosEvenOnes++
    }
    return min(oddOnesEvenZeros, oddZerosEvenOnes)
  }

23.12.2023

1496. Path Crossing easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/448

Problem TLDR

Is path string of ‘N’, ‘E’, ‘W’, ‘S’ crosses

Intuition

We can simulate the path and remember visited coordinates

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun isPathCrossing(path: String): Boolean {
    val visited = mutableSetOf(0 to 0)
    var x = 0
    var y = 0
    return !path.all { when (it) {
      'N' -> y++
      'S' -> y--
      'E' -> x++
      else -> x-- }
      visited.add(x to y)
    }
  } 

22.12.2023

1422. Maximum Score After Splitting a String easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/447

Problem TLDR

Max left_zeros + right_ones in 01-array

Intuition

We can count ones and then scan from the beginning modifying the ones and zeros counts. After some retrospect, we can do this with score variable.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\), dropLast(1) creates the second list, but we can just use pointers or asSequence

Code


    fun maxScore(s: String): Int {
      var score = s.count { it == '1' }
      return s.dropLast(1).maxOf {
        if (it == '0') ++score else --score
      }
    }

21.12.2023

1637. Widest Vertical Area Between Two Points Containing No Points easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/446

Problem TLDR

Max x window between xy points

Intuition

We can sort points by x and scan max window between them

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun maxWidthOfVerticalArea(points: Array<IntArray>): Int =
    points
      .sortedBy { it[0] }
      .windowed(2)
      .maxOf { it[1][0] - it[0][0] }

20.12.2023

2706. Buy Two Chocolates easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/445

Problem TLDR

Money change after two chocolates bought

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun buyChoco(prices: IntArray, money: Int): Int {
    var (a, b) = Int.MAX_VALUE to Int.MAX_VALUE
    for (x in prices)
      if (x < a) a = x.also { b = a }
      else if (x < b) b = x
    return (money - a - b).takeIf { it >= 0 } ?: money
  }

19.12.2023

661. Image Smoother easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/444

Problem TLDR

3x3 average of each cell in 2D matrix

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(nm)\)

Code


  fun imageSmoother(img: Array<IntArray>): Array<IntArray> =
    Array(img.size) {
      val ys = (max(0, it - 1)..min(img.lastIndex, it + 1)).asSequence()
      IntArray(img[0].size) {
        val xs = (max(0, it - 1)..min(img[0].lastIndex, it + 1)).asSequence()
        ys.flatMap { y -> xs.map { img[y][it] } }.average().toInt()
      }
    }

18.12.2023

1913. Maximum Product Difference Between Two Pairs easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/443

Problem TLDR

max * second_max - min * second_min

Intuition

We can sort an array, or just find max and second max in a linear way.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun maxProductDifference(nums: IntArray): Int {
    var (a, b, c, d) = listOf(0, 0, Int.MAX_VALUE, Int.MAX_VALUE)
    for (x in nums) {
      if (x > a) b = a.also { a = x } else if (x > b) b = x
      if (x < d) c = d.also { d = x } else if (x < c) c = x
    }
    return a * b - c * d
  }

17.12.2023

2353. Design a Food Rating System medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/442

Problem TLDR

Given foods, cuisines and ratings implement efficient methods changeRating(food, newRating) and highestRated(cuisine)

Intuition

Given that we must maintain sorted order by rating and be able to change the rating, the TreeSet may help, as it provides O(logN) amortized time for remove(obj).

Approach

Start with inefficient implementation, like do the linear search in both methods. Then decide what data structures can help to quickly find an item.

  • keep in mind, that constructor should also be efficient

Complexity

  • Time complexity: \(O(log(n))\) for either method

  • Space complexity: \(O(n)\)

Code

class FoodRatings(val foods: Array<String>, val cuisines: Array<String>, val ratings: IntArray) {
  val foodToInd = foods.indices.groupBy { foods[it] }
  val cuisineToInds: MutableMap<String, TreeSet<Int>> = mutableMapOf()
  init {
    for (ind in cuisines.indices)
      cuisineToInds.getOrPut(cuisines[ind]) { 
        TreeSet(compareBy({ -ratings[it] }, { foods[it] }))
      } += ind
  }

  fun changeRating(food: String, newRating: Int) {
    val ind = foodToInd[food]!![0]
    if (ratings[ind] != newRating) {
      val sortedInds = cuisineToInds[cuisines[ind]]!!
      sortedInds.remove(ind)
      ratings[ind] = newRating
      sortedInds.add(ind)
    }
  }

  fun highestRated(cuisine: String): String = foods[cuisineToInds[cuisine]!!.first()]
}

16.12.2023

242. Valid Anagram easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/440

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can also be solved in O(1) by computing the hash

Code


    fun isAnagram(s: String, t: String): Boolean =
      s.groupBy { it } == t.groupBy { it }

15.12.2023

1436. Destination City easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/439

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), with toSet

Code


    fun destCity(paths: List<List<String>>): String =
      (paths.map { it[1] } - paths.map { it[0] }).first()

14.12.2023

2482. Difference Between Ones and Zeros in Row and Column easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/438

Problem TLDR

diff[i][j] = onesRowi + onesColj - zerosRowi - zerosColj

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(nm)\)

Code


    fun onesMinusZeros(grid: Array<IntArray>): Array<IntArray> {
      val onesRow = grid.map { it.count { it == 1 } }
      val zerosRow = grid.map { it.count { it == 0 } }
      val onesCol = grid[0].indices.map { x -> grid.indices.count { grid[it][x] == 1 } }
      val zerosCol = grid[0].indices.map { x -> grid.indices.count { grid[it][x] == 0 } }
      return Array(grid.size) { y -> IntArray(grid[0].size) { x ->
        onesRow[y] + onesCol[x] - zerosRow[y] - zerosCol[x]
      }}
    }

13.12.2023

1582. Special Positions in a Binary Matrix easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/437

Complexity

  • Time complexity: \(O((nm)^2)\)

  • Space complexity: \(O(1)\)

Code


    fun numSpecial(mat: Array<IntArray>): Int {
       var count = 0 
       for (y in 0..mat.lastIndex)
        for (x in 0..mat[y].lastIndex)
          if (mat[y][x] == 1 
            && (0..mat.lastIndex).filter { it != y }.all { mat[it][x] == 0}
            && (0..mat[y].lastIndex).filter { it != x }.all { mat[y][it] == 0})
              count++
       return count
    }

12.12.2023

1464. Maximum Product of Two Elements in an Array easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/436

Intuition

We can sort, we can search twice for indices, we can scan once with two variables.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun maxProduct(nums: IntArray): Int = with(nums.indices){
    maxBy { nums[it] }.let { i ->
    (nums[i] - 1) * (nums[filter { it != i }.maxBy { nums[it] }] - 1)
  }}

11.12.2023

1287. Element Appearing More Than 25% In Sorted Array easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/435

Problem TLDR

Most frequent element

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can be O(1)

Code


  fun findSpecialInteger(arr: IntArray): Int =
    arr.groupBy { it }
      .maxBy { (k, v) -> v.size }!!
      .key

10.12.2023

867. Transpose Matrix easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/434

Problem TLDR

Transpose 2D matrix

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun transpose(matrix: Array<IntArray>): Array<IntArray> =
    Array(matrix[0].size) { x ->
      IntArray(matrix.size) { y ->
        matrix[y][x]
      }
    }

09.12.2023

94. Binary Tree Inorder Traversal easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/433

Problem TLDR

Inorder traversal

Intuition

Nothing special. For the iterative solution we can use Morris traversal.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun inorderTraversal(root: TreeNode?): List<Int> = root?.run {
    inorderTraversal(left) + listOf(`val`) + inorderTraversal(right)
  } ?: listOf<Int>()

08.12.2023

606. Construct String from Binary Tree easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/432

Problem TLDR

Pre-order binary tree serialization

Intuition

Let’s write a recursive solution.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun tree2str(root: TreeNode?): String = root?.run {
      val left = tree2str(left)
      val right = tree2str(right)
      val curr = "${`val`}"
      if (left == "" && right == "") curr
        else if (right == "") "$curr($left)"
        else "$curr($left)($right)"
    } ?: ""

07.12.2023

1903. Largest Odd Number in String easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/431

Problem TLDR

Largest odd number in a string

Intuition

Just search for the last odd

Approach

Let’s write Kotlin one-liner

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun largestOddNumber(num: String): String =
    num.dropLastWhile { it.toInt() % 2 == 0 }

05.12.2023

1688. Count of Matches in Tournament easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/428

Problem TLDR

Count of odd-even matches according to the rules x/2 or 1+(x-1)/2.

Intuition

The naive solution is to just implement what is asked.

Approach

Then you go read others people solutions and found this: n-1.

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(1)\)

Code


  fun numberOfMatches(n: Int): Int {
    var x = n
    var matches = 0
    while (x > 1) {
      if (x % 2 == 0) {
        matches += x / 2
        x = x / 2
      } else {
        matches += (x - 1) / 2
        x = 1 + (x - 1) / 2
      }
    }
    return matches
  }

04.12.2023

2264. Largest 3-Same-Digit Number in String easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/427

Problem TLDR

Largest 3-same-digit number in a string

Intuition

There are totally 10 such numbers: 000, 111, ..., 999.

Approach

Let’s use Kotlin’s API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can be O(1) with asSequence()

Code

    fun largestGoodInteger(num: String): String =
      num.windowed(3)
      .filter { it[0] == it[1] && it[0] == it[2] }
      .maxByOrNull { it[0] } ?: ""

03.12.2023

1266. Minimum Time Visiting All Points easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/426

Problem TLDR

Path coordinates distance in XY plane

Intuition

For each pair of points lets compute diagonal distance and the remainder: time = diag + remainder. Given that remainder = max(dx, dy) - diag, we derive the formula.

Approach

Let’s use some Kotlin’s API:

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun minTimeToVisitAllPoints(points: Array<IntArray>): Int =
    points.asSequence().windowed(2).sumBy { (from, to) ->
      max(abs(to[0] - from[0]), abs(to[1] - from[1]))
    }

02.12.2023

1160. Find Words That Can Be Formed by Characters easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/425

Problem TLDR

Sum of words lengths constructed by chairs

Intuition

Just use the char frequencies map

Approach

Some Kotlin’s API:

  • groupBy
  • sumBy
  • all
  • let

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can be O(1)

Code


  fun countCharacters(words: Array<String>, chars: String): Int =
    chars.groupBy { it }.let { freq -> 
      words.sumBy {
        val wfreq = it.groupBy { it }
        if (wfreq.keys.all { freq[it] != null 
          && wfreq[it]!!.size <= freq[it]!!.size })
        it.length else 0
      }
  }

01.12.2023

1662. Check If Two String Arrays are Equivalent easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/423

Problem TLDR

Two dimensional array equals

Intuition

There is a one-liner that takes O(n) memory: ord1.joinToString("") == word2.joinToString(""). Let’s use two-pointer approach to reduce the memory footprint.

Approach

  • we can iterate with for on a first word, and use the pointer variable for the second

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun arrayStringsAreEqual(word1: Array<String>, word2: Array<String>): Boolean {
    var i = 0
    var ii = 0
    for (w in word1) for (c in w) {
      if (i >= word2.size) return false
      if (c != word2[i][ii]) return false
      ii++
      if (ii >= word2[i].length) {
        i++
        ii = 0
      }
    }

    return i == word2.size
  }

30.11.2023

1611. Minimum One Bit Operations to Make Integers Zero hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/421

Problem TLDR

Minimum rounds of inverting rightmost bit or left of the rightmost 1 bit to make n zero

Intuition

Let’s observe the example:

  // 6
  // 110
  // 010 b
  // 011 a
  // 001 b
  // 000 a
  // 10 = 2 + f(1) = 2^1 + f(2^0)
  // 11 a
  // 01 b -> f(1)
  // 100 = 4 + f(10) = 2^2 + f(2^1)
  // 101 a
  // 111 b
  // 110 a
  // 010 b -> f(10)
  // 1000 = 8 + f(100) = 2^3 + f(2^2)
  // 1001 a
  // 1011 b
  // 1010 a
  // 1110 b
  // 1111 a
  // 1101 b
  // 1100 a
  // 0100 b -> f(100)

There are two tricks we can derive:

  1. Each signle-bit number has a recurrent count of operations: f(0b100) = 0b100 + f(0b10) and so on.
  2. The hard trick: when we consider the non-single-bit number, like 1101, we do f(0b1101) = f(0b1000) - f(0b100) + f(0b1).

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(log(n))\)

Code


  fun minimumOneBitOperations(n: Int): Int {
    val f = HashMap<Int, Int>()
    f[0] = 0
    f[1] = 1
    var curr = 2
    while (curr > 0) {
      f[curr] = curr + f[curr / 2]!!
      curr *= 2
    }

    var res = 0
    var sign = 1;
    for (i in 0..31) {
      val bit = 1 shl i
      if (n and bit != 0) {
        res += sign * f[bit]!!
        sign = -sign
      }
    }

    return Math.abs(res)
  }

29.11.2023

191. Number of 1 Bits easy blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/420

Problem TLDR

Bits count

Intuition

The optimal solution would be using built-in n.countOneBits().

However, there is a knonw technique using tabulation DP to count bits. The recurrence is: count(n) = count(n « 1) + 1?1:0. For example, count(1111) = 1 + count(111). Or, count(110) = 0 + count(11)

Approach

  • carefult with the table size, it must be 2^8=256

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\)

Code


  val dp = IntArray(256).apply {
    for (i in 1..<size) 
      this[i] = this[i / 2] + (i and 1)
  }
  fun hammingWeight(n:Int):Int =
    dp[n and 255] + 
    dp[(n ushr 8) and 255] +
    dp[(n ushr 16) and 255] +
    dp[(n ushr 24) and 255]

28.11.2023

2147. Number of Ways to Divide a Long Corridor hard blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/419

Problem TLDR

Count ways to place borders separating pairs of ‘S’ in ‘SP’ string

Intuition

We can scan linearly and do the interesting stuff after each two ‘S’: each new ‘P’ adds ‘sum’ ways to the total. The last pair of ‘S’ don’t need a border.

  // ssppspsppsspp
  // ss         1
  // ssp        2
  // sspp       3
  //     sps    3
  //     spsp   3+3=6
  //     spspp  6+3=9 <-- return this
  //           ss    9
  //           ssp   9+9=18
  //           sspp  18+9=27 discard this result, as it is last

Approach

Carefult what ‘sum’ to add, save the last sum to a separate variable.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun numberOfWays(corridor: String): Int {
    var prev = 1
    var sum = 1
    var s = 0
    for (c in corridor)
      if (c == 'S') {
        if (s == 2) {
          prev = sum
          s = 0
        }
        s++
      } else if (s == 2) 
        sum = (prev + sum) % 1_000_000_007
    return if (s == 2) prev else 0
  }

27.11.2023

935. Knight Dialer medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/418

Problem TLDR

Count of dialer n-length numbers formed by pressing in a chess Knight’s moves

Intuition

We can search with Depth-First Search for every position and count every path that has n digits in it. The result will only depend on a previous number and count of the remaining moves, so can be cached.

Approach

Let’s write a separate paths map: current digit to next possible.

Complexity

  • Time complexity: \(O(n)\), 10 digits is a constant value

  • Space complexity: \(O(n)\)

Code

  val dp = mutableMapOf<Pair<Int, Int>, Int>()
  val paths = mapOf(
    -1 to (0..9).toList(),
    0 to listOf(4, 6),
    1 to listOf(6, 8),
    2 to listOf(7, 9),
    3 to listOf(4, 8),
    4 to listOf(3, 9, 0),
    5 to listOf(),
    6 to listOf(1, 7, 0),
    7 to listOf(2, 6),
    8 to listOf(1, 3),
    9 to listOf(2, 4))
  fun knightDialer(pos: Int, prev: Int = -1): Int =
    if (pos == 0) 1 else dp.getOrPut(pos to prev) {
      paths[prev]!!.map { knightDialer(pos - 1, it) }
      .fold(0) { r, t -> (r + t) % 1_000_000_007 }
    }

26.11.2023

1727. Largest Submatrix With Rearrangements medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/417

Problem TLDR

Max area of 1 submatrix after sorting columns optimally

Intuition

Use hint :( Ok, if we store the heights of the columns we can analyze each row independently, by choosing the largest heights first. The area will be height * width, where width will be the current position: image.png

image.png

Approach

We can reuse the matrix, but don’t do this in a production code without a warning.

Complexity

  • Time complexity: \(O(nmlog(m))\)

  • Space complexity: \(O(1)\)

Code


  fun largestSubmatrix(matrix: Array<IntArray>): Int {
    for (y in 1..<matrix.size)
      for (x in 0..<matrix[y].size)
        if (matrix[y][x] > 0)
          matrix[y][x] += matrix[y - 1][x]
    var max = 0
    for (row in matrix) {
      row.sort()
      for (x in row.lastIndex downTo 0)
        max = max(max, row[x] * (row.size - x))
    }
    return max
  }

25.11.2023

1685. Sum of Absolute Differences in a Sorted Array medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/416

Problem TLDR

Array to sum_j(abs(arr[i] - arr[j])) for each i

Intuition

This is an arithmetic problem. We need to pay attention of an abs sign, given the array in a sorted order.

  // 0 1 2 3 4 5
  // a b c d e f
  // c: c-a + c-b + c-c + d-c + e-c + f-c
  // c * (1 + 1 + 1-1 -1 -1-1) -a-b+d+e+f
  //      (i+1 - (size + 1 - (i + 1)))
  //      (i + 1 - size - 1 +i + 1)
  //      (2*i - size + 1)
  // d: d-a + d-b + d-c + d-d + e-d +f-d
  // d * (1+1+1+1-1-1-1)
  // i=3 2*3-6+1=1
  // soFar = a+b
  // sum = a+b+c+d+e+f
  // i = 2
  // curr = sum - soFar + nums[i] * (2*i - size + 1)
  // 2 3 5
  // sum = 10
  // soFar = 2
  // i=0 10 - 2 + 2 * (2*0-3+1)=10-6=4 xxx
  // 2-2 + 3-2 + 5-2 = 2 * (1-1-1-1) + (3 + 5)
  // 3-2 + 3-3 + 5-3 = 3 * (1+1-1-1) - 2 + (5)
  //                       (2*1-3+1)       (sum-soFar)
  // 5-2 + 5-3 + 5-5 = 5 * (1+1+1-1) -2-3 + (0)

Approach

Evaluate some examples, then derive the formula.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


  fun getSumAbsoluteDifferences(nums: IntArray): IntArray {
    val sum = nums.sum()
    var soFar = 0
    return IntArray(nums.size) { i ->
      soFar += nums[i]
      (sum - 2 * soFar + nums[i] * (2 * i - nums.size + 2))
    }
  }

24.11.2023

1561. Maximum Number of Coins You Can Get medium blog post substack youtube image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/415

Problem TLDR

Get sum of second maxes of triples from array

Intuition

Observing the example:

  // 1 2 3 4 5 6 7 8 9
  // *             * *  8
  //   *       * *      6
  //     * * *          4
  // size = x + 2x

we can deduce an optimal algorithm: give bob the smallest value, and take the second largest. There are exactly size / 3 moves total.

Approach

Let’s write it in a functional style, using Kotlin’s API:

  • sorted
  • drop
  • chunked
  • sumBy

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\), can be O(1) when sorted in-place

Code


  fun maxCoins(piles: IntArray): Int =
    piles.sorted()
      .drop(piles.size / 3)
      .chunked(2)
      .sumBy { it[0] }

23.11.2023

1630. Arithmetic Subarrays medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/414

Problem TLDR

Query array ranges can form arithmetic sequence

Intuition

Given the problem contraints, the naive solution would work: just sort the subarray and check the diff.

Approach

We can use PriorityQueue

Complexity

  • Time complexity: \(O(n^2log(n))\)

  • Space complexity: \(O(n)\)

Code


  fun checkArithmeticSubarrays(nums: IntArray, l: IntArray, r: IntArray) = 
  List(l.size) { ind ->
    val pq = PriorityQueue<Int>() 
    for (i in l[ind]..r[ind]) pq.add(nums[i])
    val diff = -pq.poll() + pq.peek()
    var prev = pq.poll()
    while (pq.isNotEmpty()) {
      if (pq.peek() - prev != diff) return@List false
      prev = pq.poll()
    }
    true
  }

22.11.2023

1424. Diagonal Traverse II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/413

Problem TLDR

Diagonal 2D matrix order with prunes

Intuition

The naive solution is to adjust the pointers x and y. However, that will cost O(max(x)*max(y)) and give TLE.

Let’s just sort indices pairs (x y) and take them one by one.

Approach

Use some Kotlin’s features:

  • with
  • let
  • indices
  • compareBy({ one }, { two })

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun findDiagonalOrder(nums: List<List<Int>>): IntArray =
    with(PriorityQueue<Pair<Int, Int>>(compareBy(
      { it.first + it.second }, { it.first }, { it.second }
    ))) {
    for (y in nums.indices)
      for (x in nums[y].indices) add(x to y)
    IntArray(size) { poll().let { (x, y) -> nums[y][x]} }
  }

21.11.2023

1814. Count Nice Pairs in an Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/412

Problem TLDR

Count pairs x-rev(x) == y-rev(y), where rev(123) = 321

Intuition

For simplicity, let’s redefine the equation, keeping i and j on a separate parts \(nums[i] - rev(nums[i]) == nums[j] - rev(nums[j])\). Now, we can precompute nums[i] - rev(nums[i]). The remaining part of an algorithm is how to calculate count of the duplicate numbers in a linear scan.

Approach

Let’s use a HashMap to count the previous numbers count. Each new number will make a count new pairs.

Complexity

  • Time complexity: \(O(nlg(n))\), lg(n) - for the rev()

  • Space complexity: \(O(n)\)

Code


  fun countNicePairs(nums: IntArray): Int {
    val counts = HashMap<Int, Int>()
    var sum = 0
    for (x in nums) {
      var n = x
      var rev = 0
      while (n > 0) {
        rev = (n % 10) + rev * 10
        n = n / 10
      }
      val count = counts[x - rev] ?: 0
      sum = (sum + count) % 1_000_000_007
      counts[x - rev] = count + 1
    }
    return sum
  }

20.11.2023

2391. Minimum Amount of Time to Collect Garbage medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/411

Problem TLDR

Time to pick 3-typed garbage[] by 3 trucks traveling to the right travel[] time

Intuition

We can hardcode the algorithm from the description examples, for each truck individually.

Approach

Let’s try to minify the code:

  • all garbage must be picked up, so add garbage.sumBy { it.length }
  • for each type, truck will travel until the last index with this type

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun garbageCollection(garbage: Array<String>, travel: IntArray): Int =
      garbage.sumBy { it.length } +
        "MPG".sumBy { c ->
          (1..garbage.indexOfLast { c in it }).sumBy { travel[it - 1] }
        }

19.11.2023

1887. Reduction Operations to Make the Array Elements Equal medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/410

Problem TLDR

Number of operations to decrease all elements to the next smallest

Intuition

The algorithm pretty much in a problem definition, just implement it.

Approach

  • iterate from the second position, to simplify the initial conditions

Complexity

  • Time complexity: \(O(nlog())\)

  • Space complexity: \(O(n)\)

Code


    fun reductionOperations(nums: IntArray): Int {
      nums.sort()
      return (nums.size - 2 downTo 0).sumBy {
        if (nums[it] < nums[it + 1]) nums.size - 1 - it else 0
      }
    }

18.11.2023

1838. Frequency of the Most Frequent Element medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/408

Problem TLDR

Max count of equal numbers if increment arr[i] k times

Intuition

Let’s sort the array and scan numbers from small to hi. As we’re doing only the increment operations, only the left part of the current position matters. Let’s see how much items we can make equal to the current arr[i]:

    // 1 4 8 13  inc
    // 4 4 8 13  3
    //   ^
    // 8 8 ^     3 + 2 * (8 - 4) = 8 + 3 = 12
    // 1 8 ^     12 - (8 - 1) = 4

When taking a new element 8, our total increment operations inc grows by the difference between two previous 4 4 and the current 8. If inc becomes bigger than k, we can move the from position, returning nums[i] - nums[from] operations back.

Approach

  • use inclusive from and to
  • always compute the max
  • make initial conditions from the 0 element position, and iterate from 1 to avoid overthinking

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


    fun maxFrequency(nums: IntArray, k: Int): Int {
      nums.sort()
      var from = 0
      var inc = 0
      var max = 1
      for (to in 1..<nums.size) {
        inc += (to - from) * (nums[to] - nums[to - 1])
        while (from <= to && inc > k)
          inc -= nums[to] - nums[from++]
        max = max(max, to - from + 1)
      }
      return max
    }

17.11.2023

1877. Minimize Maximum Pair Sum in Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/407

Problem TLDR

Minimum possible max of array pairs sums

Intuition

The optimal construction way is to pair smallest to largest.

Approach

We can use two pointers and iteration, let’s write non-optimal one-liner however

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\), this solution takes O(n), but can be rewritten

Code


    fun minPairSum(nums: IntArray): Int = 
      nums.sorted().run {
          zip(asReversed()).maxOf { it.first + it.second }
      }

16.11.2023

1980. Find Unique Binary String medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/406

Problem TLDR

First absent number in a binary string array

Intuition

The naive solution would be searching in all the numbers 0..2^n. However, if we convert strings to ints and sort them, we can do a linear scan to detect first absent.

Approach

  • use padStart to convert back

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun findDifferentBinaryString(nums: Array<String>): String {
      var next = 0
      for (x in nums.sorted()) {
        if (x.toInt(2) > next) break
        next++
      }
      return next.toString(2).padStart(nums[0].length, '0')
    }

15.11.2023

1846. Maximum Element After Decreasing and Rearranging medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/405

Problem TLDR

Max number from converting array to non decreasing

Intuition

First, sort the array. Now, for every missing number, 1 3 5 -> 2 we can take one of the numbers from the highest, 1 2 3. We can use a counter and a Priority Queue. For example:

array:   1 5 100 100 100
counter: 1 2 3   4   5

Approach

Let’s use some Kotlin’s sugar:

  • with
  • asList

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


  fun maximumElementAfterDecrementingAndRearranging(arr: IntArray): Int =
    with(PriorityQueue<Int>().apply { addAll(arr.asList()) }) {
      var max = 0
      while (isNotEmpty()) if (poll() > max) max++
      max
  }

Shorter version: image.png

14.11.2023

1930. Unique Length-3 Palindromic Subsequences medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/403

Problem TLDR

Count of unique palindrome substrings of length 3

Intuition

We can count how many other characters between group of the current

Approach

Let’s use Kotlin API:

  • groupBy
  • filterValues
  • indexOf
  • lastIndexOf

Complexity

  • Time complexity: \(O(n)\), we can also use withIndex to avoid searching indexOf and lastIndexOf.

  • Space complexity: \(O(1)\), if we store frequencies in an IntArray

Code


    fun countPalindromicSubsequence(s: String): Int {
      val freq = s.groupBy { it }.filterValues { it.size > 1 }
      var count = 0
      for ((l, f) in freq) {
        if (f.size > 2) count++
        val visited = HashSet<Char>()
        for (i in s.indexOf(l)..s.lastIndexOf(l)) 
          if (s[i] != l && visited.add(s[i])) count++
      }
      return count
    }

13.11.2023

2785. Sort Vowels in a String medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/402

Problem TLDR

Sort vowels in a string

Intuition

The sorted result will only depend of the vowels frequencies.

Approach

Let’s use Kotlin API:

  • groupBy
  • mapValues
  • buildString

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun sortVowels(s: String): String {
      val freq = s.groupBy { it }.mapValues({ it.value.size }).toMutableMap()
      val vl = mutableListOf('A', 'E', 'I', 'O', 'U', 'a', 'e', 'i', 'o', 'u')
      val vs = vl.toSet()
      return buildString {
        for (c in s)
          if (c in vs) {
            while (freq[vl.first()].let { it == null || it <= 0 }) vl.removeFirst()
            freq[vl.first()] = freq[vl.first()]!! - 1
            append(vl.first())
          } else append(c)
      }
    }

12.11.2023

815. Bus Routes hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/401

Problem TLDR

Minimum buses to travel by given routes

Intuition

The Breadth-First Search in a routes graph would work. Build stop to route association to know which of the routes are next.

Approach

Some optimizations:

  • eliminate the trivial case source == target
  • remove a visited stop from stopToRoute graph
  • there is at most routes.size buses needed
  • remember the visited stop

Complexity

  • Time complexity: \(O(RS)\)

  • Space complexity: \(O(RS)\)

Code


    fun numBusesToDestination(routes: Array<IntArray>, source: Int, target: Int): Int {
      if (source == target) return 0
      val stopToRoute = mutableMapOf<Int, MutableList<Int>>()
      for (i in routes.indices)
        for (stop in routes[i])
          stopToRoute.getOrPut(stop) { mutableListOf() } += i
      return with(ArrayDeque<Int>()) {
        add(source)
        val visited = mutableSetOf<Int>()
        for (bus in 1..routes.size)
          repeat(size) {
            for (route in stopToRoute.remove(removeFirst()) ?: emptyList())
              if (visited.add(route)) for (s in routes[route])
                if (s == target) return@with bus else add(s)
          }
        -1
      }
    }

11.11.2023

2642. Design Graph With Shortest Path Calculator hard blog post substack image.png

Problem TLDR

Implement graph with shortest path searching

Intuition

There is no special knowledge here, just a simple Dijkstra, that is BFS in a space of the shortest-so-far paths

Approach

  • the visited set will improve the speed

Complexity

  • Time complexity: \(O(Vlog(E))\)

  • Space complexity: \(O(E)\)

Code


class Graph(n: Int, edges: Array<IntArray>) :
  HashMap<Int, MutableList<IntArray>>() {
  init { for (e in edges) addEdge(e) }

  fun addEdge(edge: IntArray) {
    getOrPut(edge[0]) { mutableListOf() } += edge
  }

  fun shortestPath(node1: Int, node2: Int): Int =
    with(PriorityQueue<Pair<Int, Int>>(compareBy({ it.second }))) {
      add(node1 to 0)
      val visited = HashSet<Int>()
      while (isNotEmpty()) {
        val (n, wp) = poll()
        if (n == node2) return@with wp
        if (visited.add(n)) 
          get(n)?.onEach { (_, s, w) -> add(s to (w + wp))}
      }
      -1
    }
}

10.11.2023

1743. Restore the Array From Adjacent Pairs medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/399

Problem TLDR

Restore an array from adjacent pairs

Intuition

We can form an undirected graph and do a Depth-First Search

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun restoreArray(adjacentPairs: Array<IntArray>): IntArray {
      val fromTo = mutableMapOf<Int, MutableList<Int>>()
      for ((from, to) in adjacentPairs) {
        fromTo.getOrPut(from) { mutableListOf() } += to
        fromTo.getOrPut(to) { mutableListOf() } += from
      }
      val visited = HashSet<Int>()
      with(ArrayDeque<Int>()) {
        add(fromTo.keys.first { fromTo[it]!!.size == 1 }!!)
        return IntArray(adjacentPairs.size + 1) {
          while (first() in visited) removeFirst()
          removeFirst().also {
            visited.add(it)
            fromTo[it]?.onEach { add(it) }
          }
        }
      }
    }

09.11.2023

1759. Count Number of Homogenous Substrings medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/398

Problem TLDR

Count of substrings of same chars

Intuition

Just count current len and add to total

  // abbcccaa   c t
  // a          1 1
  //  b         1 2
  //   b        2 4
  //    c       1 5
  //     c      2 7
  //      c     3 10
  //       a    1 11
  //        a   2 13

Approach

  • don’t forget to update prev

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun countHomogenous(s: String): Int {
    var total = 0
    var count = 0
    var prev = '.'
    for (c in s) {
      if (c == prev) count++
      else count = 1
      total = (total + count) % 1_000_000_007
      prev = c
    }
    return total
  }

08.11.2023

2849. Determine if a Cell Is Reachable at a Given Time medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/397

Problem TLDR

Is path possible on grid sx, sy -> fx, fy

Intuition

Given the problem size, we can’t use DP, as it will take O(n^2). However, we must notice, that if the shortest path is reachable, than any other path can be formed to travel at any time.

Approach

The shortest path will consist of only the difference between coordinates.

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\)

Code


    fun isReachableAtTime(sx: Int, sy: Int, fx: Int, fy: Int, t: Int): Boolean {
      var dx = Math.abs(fx - sx)
      var dy = Math.abs(fy - sy)
      var both = min(dx, dy)
      var other = max(dx, dy) - both
      var total = both + other
      return total <= t && (total > 0 || t != 1)
    }

07.11.2023

1921. Eliminate Maximum Number of Monsters medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/396

Problem TLDR

Count possible 1-minute kills in a game of dist[] targets falling with speed[]

Intuition

Each target has it’s own arrival time_i = dist[i] / speed[i]. We must prioritize targets by it.

Approach

Let’s use Kotlin API:

  • indices
  • sortedBy
  • withIndex
  • takeWhile
  • time becomes just a target index

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun eliminateMaximum(dist: IntArray, speed: IntArray): Int =
      dist.indices.sortedBy { dist[it].toDouble() / speed[it] }
      .withIndex()
      .takeWhile { (time, ind) -> speed[ind] * time < dist[ind] }
      .count()

06.11.2023

1845. Seat Reservation Manager medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/395

Problem TLDR

Design reservation number system

Intuition

The naive approach is to just use PriorityQueue as is:

class SeatManager(n: Int): PriorityQueue<Int>() {
  init { for (x in 1..n) add(x) }
  fun reserve() = poll()
  fun unreserve(seatNumber: Int) = add(seatNumber)
}

However, we can improve the memory cost by noticing, that we can shrink the heap when max is returned.

Approach

  • we can save some lines of code by using extending the class (prefer a field instead in a production code to not exprose the heap directly)

Complexity

  • Time complexity: \(O(log(n))\) for operations

  • Space complexity: \(O(n)\)

Code


class SeatManager(n: Int): PriorityQueue<Int>() {
  var max = 0
  fun reserve() = if (isEmpty()) ++max else poll()
  fun unreserve(seatNumber: Int) {
    if (seatNumber == max) max--
    else add(seatNumber)
  }
}

05.11.2023

1535. Find the Winner of an Array Game medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/394

Problem TLDR

Find maximum of the k nearest in array

Intuition

Looking at some examples:

  // 0 1 2 3 4 5
  // 1 3 2 5 4 10            3
  //   3 2 5 4 10 1          3
  //   3   5 4 10 1 2        5
  //       5 4 10 1 2 3      5
  //       5   10 1 2 3 4    10
  //           10 1 2 3 4 5  10 ...

we can deduce that the problem is trivial when k >= arr.size - it is just a maximum. Now, when k < arr.size we can just simulate the given algorithm and stop on the first k-winner.

Approach

  • we can iterate over 1..arr.lastIndex or use a clever initialization wins = -1

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


  fun getWinner(arr: IntArray, k: Int): Int {
    var wins = -1
    var max = arr[0]
    for (x in arr) {
      if (x > max) {
        wins = 1
        max = x
      } else wins++
      if (wins == k) break
    }
    return max
  }

04.11.2023

1503. Last Moment Before All Ants Fall Out of a Plank medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/392

Problem TLDR

Max time ants on a line when goint left and right

Intuition

Use the hint: ants can pass through

Approach

The problem becomes trivial

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun getLastMoment(n: Int, left: IntArray, right: IntArray): Int =
       max(left.maxOrNull() ?: 0, n - (right.minOrNull() ?: n)) 

03.11.2023

767. Reorganize String medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/391

Problem TLDR

Non-repeating consequent chars string from another string

Intuition

The naive brute force solution is to do Depth-First Search and memoization by given current char, previous one and used chars set. It gives TLE, as it takes O(n^3).

Next, use hint.

To take chars one by one from the two most frequent we will use a PriorityQueue

Approach

  • if previous is equal to the current and there is no other chars - we can’t make a result
  • consider appending in a single point of code to simplify the solution
  • use Kotlin’s API: buildString, compareByDescending, onEach

Complexity

  • Time complexity: \(O(n)\), assume constant 128log(128) for a Heap sorting

  • Space complexity: \(O(n)\)

Code


    fun reorganizeString(s: String): String = buildString {
      val freq = IntArray(128)
      s.onEach { freq[it.toInt()]++ }
      val pq = PriorityQueue<Char>(compareByDescending { freq[it.toInt()] })
      for (c in 'a'..'z') if (freq[c.toInt()] > 0) pq.add(c)
      while (pq.isNotEmpty()) {
        var c = pq.poll()
        if (isNotEmpty() && last() == c) {
          if (pq.isEmpty()) return ""
          c = pq.poll()
          pq.add(last())
        }
        append(c)
        if (--freq[c.toInt()] > 0) pq.add(c)
      }
    }

02.11.2023

2265. Count Nodes Equal to Average of Subtree medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/390

Problem TLDR

Number of nodes in a tree where val == sum / count of a subtree

Intuition

Just do a Depth First Search and return sum and count of a subtree.

Approach

  • avoid nulls when traversing the tree

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\) for the recursion depth

Code


    fun averageOfSubtree(root: TreeNode?): Int {
      var res = 0
      fun dfs(n: TreeNode): Pair<Int, Int> {
        val (ls, lc) = n.left?.let { dfs(it) } ?: 0 to 0
        val (rs, rc) = n.right?.let { dfs(it) } ?: 0 to 0
        val sum = n.`val` + ls + rs
        val count = 1 + lc + rc
        if (n.`val` == sum / count) res++
        return sum to count
      }
      root?.let { dfs(it) }
      return res
    }

01.11.2023

501. Find Mode in Binary Search Tree easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/389

Problem TLDR

Most frequent elements in a Binary Search Tree

Intuition

A simple solution is to use a frequency map. Another way is the linear scan of the increasing sequence. For example, 1 1 1 2 2 2 3 3 4 4 4: we can use one counter and drop the previous result if counter is more than the previous max.

Approach

To convert the Binary Search Tree into an increasing sequence, we can do an in-order traversal.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), result can be n if numbers are unique

Code

    fun findMode(root: TreeNode?): IntArray {
      val res = mutableListOf<Int>()
      var maxCount = 0
      var count = 0
      var prev = Int.MAX_VALUE
      fun dfs(n: TreeNode) {
        n.left?.let { dfs(it) }
        if (prev == n.`val`) {
          count++
        } else {
          count = 1
          prev = n.`val`
        }
        if (count == maxCount) {
          res += n.`val`
        } else if (count > maxCount) {
          maxCount = count
          res.clear()
          res += n.`val`
        }
        n.right?.let { dfs(it) }
      }
      root?.let { dfs(it) }
      return res.toIntArray()
    }

31.10.2023

2433. Find The Original Array of Prefix Xor medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/387

Problem TLDR

Reverse xor operation

Intuition

Let’s observe how xor works:

    // 010 2
    // 101 5
    // 111 7
    // 5 xor 7 = 2
    // 101 xor 111 = 010
    // 5 xor 2 = 101 xor 010 = 111

We can reverse the xor operation by applying it again: a ^ b = c, then a ^ c = b

Approach

There are several ways to write this:

  1. by using mapIndexed
  2. by in-place iteration
  3. by creating a new array

Let’s use Kotlin’s array constructor lambda and getOrElse.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun findArray(pref: IntArray) = IntArray(pref.size) {
      pref[it] xor pref.getOrElse(it - 1) { 0 }
    }

30.10.2023

1356. Sort Integers by The Number of 1 Bits easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/386

Problem TLDR

Sort an array comparing by bit count and value

Intuition

Let’s use some Kotlin API

Approach

  • countOneBits
  • sortedWith
  • compareBy

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun sortByBits(arr: IntArray): IntArray = arr
      .sortedWith(compareBy({ it.countOneBits() }, { it }))
      .toIntArray()

29.10.2023

458. Poor Pigs hard blog post substack

image.png image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/385

Problem TLDR

Minimum pigs to find a poison in buckets in k rounds

Intuition

The first idea is, with the number of pigs increasing, the possibility to successfully test in the given time grows from impossible to possible. This gives us the idea to use a Binary Search.

However, now we must solve another problem: given the pigs and rounds, how many buckets we can test?

One simple insight is: let’s assign unique pigs pattern to each of the bucket.

We can brute-force this problem and use memorization. Consider each pig, it can avoid participation, and can participate in all the rounds:

    val dp = mutableMapOf<Int, Int>()
    fun numPatterns(pigs: Int): Int {
      fun dfs(curr: Int): Int = if (curr == 0) 1 else dp.getOrPut(curr) {
        val take = dfs(curr - 1)
        if (take >= buckets) take else take + take * minutesToTest / minutesToDie
      }
      return dfs(pigs)
    }

This number grows quickly, so we trim it by the buckets number maximum.

Another way to solve this, is to observe those unique patterns.

If we have just one round, 3 pigs, there are total 8 patterns:

    // pigs = 3 rounds = 1
    //   123
    // 0 000 no pig drinks
    // 1 001 pig #3 drinks
    // 2 010 pig #2 drinks
    // 3 011 pigs #2 and #3 drinks
    // 4 100 pig #1 drinks
    // 5 101 pigs #1 and #3 drinks
    // 6 110 pigs #1 and #2 drinks
    // 7 111 all pig drinks

or,

    //
    // 0 1 2 3 4 5 6 7
    //         1 1 1 1 <-- pig #1
    //     2 2     2 2 <-- pig #2
    //   3   3   3   3 <-- pig #3

Now, if one bucket is a poison, we immediately know which one of those 8 buckets by its unique pattern.

Ok, so 3 pigs for 1 round enables to test 8 or 2^3 buckets. It is evident, that for 1 round the number of possible buckets is 2^pigs

How this changes with the growth of rounds? Let’s observe another example, 3 pigs, 2 rounds:

    //
    // 3 pigs, 2 rounds:
    //   123
    // 0 000
    // 1 001
    // 2 002
    // 3 010
    // 4 011
    // 5 012
    // 6 020
    // 7 021
    // 8 022
    // 9 100
    //10 101
    //11 102
    //12 110
    //13 111
    //14 112
    //15 120
    //16 121
    //17 122
    //18 200
    //19 201
    //20 202
    //21 210
    //22 211
    //23 212
    //24 220
    //25 221
    //26 222

or,

    // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
    // - round 1 -
    //                   1  1  1  1  1  1  1  1  1
    //       2 2 2                2  2  2                    2  2  2
    //   3     3     3      3        3        3        3        3        3
    // - round 2 -
    //                                              1  1  1  1  1  1  1  1  1
    //             2 2 2                   2  2  2                    2  2  2
    //     3     3     3       3         3       3        3        3        3

Each pigs pattern consists of the 3 pigs, and each pig defined as round 1 or round 2.

This results in 27 unique patterns, or buckets being able to test, or 3^3. Let’s extrapolate this formula: buckets = (1 + rounds) ^ pigs

Approach

For better Binary Search, use:

  • inclusive lo and hi
  • check the last condition lo == hi
  • always move lo or hi
  • always compute the result independently min = min(min, mid)

Complexity

  • Time complexity: \(O(log^2(buckets))\), one log for the Binary Search, another is for canTest function

  • Space complexity: \(O(1)\)

Code

DFS + memo

  fun poorPigs(buckets: Int, minutesToDie: Int, minutesToTest: Int): Int {
    val dp = mutableMapOf<Int, Int>()
    fun numPatterns(pigs: Int): Int {
      fun dfs(curr: Int): Int = if (curr == 0) 1 else dp.getOrPut(curr) {
        val take = dfs(curr - 1)
        if (take >= buckets) take else take + take * minutesToTest / minutesToDie
      }
      return dfs(pigs)
    }
    var lo = 0
    var hi = buckets
    var min = hi
    while (lo <= hi) {
      val mid = lo + (hi - lo) / 2
      if (numPatterns(mid) >= buckets) {
        min = min(min, mid)
        hi = mid - 1
      } else lo = mid + 1
    }
    return min
  }

The more clever version:


    fun poorPigs(buckets: Int, minutesToDie: Int, minutesToTest: Int): Int {
      fun canTest(pigs: Int): Boolean {
        var p = 0
        var bs = 1
        while (p++ < pigs) {
          bs *= 1 + minutesToTest / minutesToDie
          if (bs >= buckets) return true
        }
        return bs >= buckets
      }
      var lo = 0
      var hi = buckets
      var min = hi
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (canTest(mid)) {
          min = min(min, mid)
          hi = mid - 1
        } else lo = mid + 1
      }
      return min
    }

28.10.2023

1220. Count Vowels Permutation hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/384

Problem TLDR

Count of n lengths paths according to graph rules a->e, e->(a, i), etc

Intuition

This is a straghtforward DFS + memoization dynamic programming problem. Given the current position and the previous character, we know the suffix answer. It is independent of any other factors, so can be cached.

Approach

Let’s write DFS + memo

  • use Kotlin’s sumOf API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun countVowelPermutation(n: Int): Int {
      val vs = mapOf('a' to arrayOf('e'),
                     'e' to arrayOf('a', 'i'),
                     'i' to arrayOf('a', 'e', 'o', 'u'),
                     'o' to arrayOf('i', 'u'),
                     'u' to arrayOf('a'),
                     '.' to arrayOf('a', 'e', 'i', 'o', 'u'))
      val dp = mutableMapOf<Pair<Int, Char>, Long>()
      fun dfs(i: Int, c: Char): Long = if (i == n) 1L else 
        dp.getOrPut(i to c) { vs[c]!!.sumOf { dfs(i + 1, it) } } %
        1_000_000_007L
      return dfs(0, '.').toInt()
    }

Iterative version image.png Another one-liner image.png

27.10.2023

5. Longest Palindromic Substring medium blog post substack image.png Golf version image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/383

Problem TLDR

Longest palindrome substring

Intuition

If dp[from][to] answering whether substring s(from, to) is a palindrome, then dp[from][to] = s[from] == s[to] && dp[from + 1][to - 1]

Approach

  • We can cleverly initialize the dp array to avoid some corner cases checks.
  • It is better to store just two indices. For simplicity, let’s just do substring each time.

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun longestPalindrome(s: String): String {
      val dp = Array(s.length) { i -> BooleanArray(s.length) { i >= it } }
      var res = s.take(1)
      for (to in s.indices) for (from in to - 1 downTo 0) {
        dp[from][to] = s[from] == s[to] && dp[from + 1][to - 1]
        if (dp[from][to] && to - from + 1 > res.length) 
          res = s.substring(from, to + 1)
      }
      return res
    }

26.10.2023

823. Binary Trees With Factors medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/382

Problem TLDR

Number of trees from arr where each k node has i and j leafs arr[k]=arr[j]*arr[i]

Intuition

By naive intuition we can walk array in n^2 manner and collect all the matching multiplications. However, there is a nested depth, and we need a law how to add this to the result.

Let’s observe the pattern:

    // 12 3 4 6 2
    // 2x3=6  a
    // 3x2=6  b
    // 3x4=12 c
    // 4x3=12 d
    // 2x6=12 e
    // 6x2=12 f
    // 2x2=4  g
    // 5 + [a b c d e f g] + [ca] + [da] + [ea eb] + [fa fb] = 18

If we start from node e we must include both a and b. The equation becomes: f(k)=SUM(f(i)*f(j)). For node e: k=12, i=2, j=6, f(12)=f(2)*f(6), f(6)=f(3)*f(2) + f(2)*f(3)=2

If we sort the array, we will make sure, lower values are calculated.

We can think about this like a graph: 2x3->6->12

Approach

Calculate each array values individually using DFS + memo, then sum.

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun numFactoredBinaryTrees(arr: IntArray): Int {
      var set = arr.toSet()
      arr.sort()
      val dp = mutableMapOf<Int, Long>()
      fun dfs(a: Int): Long = dp.getOrPut(a) {
        1L + arr.sumOf {
          if (a % it == 0 && set.contains(a / it))
            dfs(it) * dfs(a / it) else 0L
        }
      }
      return (arr.sumOf { dfs(it) } % 1_000_000_007L).toInt()
    }

25.10.2023

779. K-th Symbol in Grammar medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/381

Problem TLDR

Binary Tree 0 -> 01, 1 -> 10 at [n][k] position

Intuition

Let’s draw the example and see the pattern:

  //1                                    [0]
  //2                  [0]                                          1
  //3        [0]                    1                      1                   0 
  //4     0       [1]          1         0            1         0          0         1 
  //5  0    1    1   [0]     1    0    0    1       1    0    0    1     0    1    1    0 
  //6 0 1  1 0  1 0 [0]1    1 0  0 1  0 1  1 0     1 0  0 1  0 1  1 0   0 1  1 0  1 0  0 1 
  //  1 2  3 4  5 6  7 8    9
  //                 ^ 

Some observations:

  • Every 0 starts its own tree, and every 1 start its own pattern of a tree.
  • We can know the position in the previous row: (k + 1) / 2
  • If previous value is 0, current pair is 01, otherwise 10

Approach

  • we don’t need to memorize the recursion, as it goes straightforward up
  • we can use and 1 bit operation instead of % 2

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun kthGrammar(n: Int, k: Int): Int = if (n == 1) 0 else 
    (if (kthGrammar(n - 1, (k + 1) / 2) == 0) k.inv() else k) and 1

24.10.2023

515. Find Largest Value in Each Tree Row medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/380

Problem TLDR

Binary Tree’s maxes of the levels

Intuition

Just use Breadth-First Search

Approach

Let’s use some Kotlin’s API:

  • generateSequence
  • maxOf

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun largestValues(root: TreeNode?): List<Int> = 
    with(ArrayDeque<TreeNode>()) {
      root?.let { add(it) }
      generateSequence { if (isEmpty()) null else 
        (1..size).maxOf {
          with(removeFirst()) {
            left?.let { add(it) }
            right?.let { add(it) }
            `val`
          }
        }
      }.toList()
    }

23.10.2023

342. Power of Four easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/379

Problem TLDR

Is n == x^4?

Intuition

There are several ways to solve this. We need to look at the bit representation of some examples, there are an even number of trailing zeros and always just a single 1 bit:

    // 4  100
    // 16 10000
    // 64 1000000

Approach

if (n == 1) true else if (n == 0) false 
else n % 4 == 0 && isPowerOfFour(n / 4)

Bit shift approach:

       var x = n
       var count = 0
       while (x > 0 && x and 1 == 0) {
          x = x shr 1
          count++
       }
       return x == 1 && count % 2 == 0

Bit mask approach:

n > 0 && (n and (n - 1)) == 0 && (n and 0b0101_0101_0101_0101__0101_0101_0101_0101 != 0)

Use Kotlin countTrailingZeroBits. Or do a Binary Search if you write that algorithm by hand:

unsigned int c = 32; // c will be the number of zero bits on the right
v &= -signed(v);
if (v) c--;
if (v & 0x0000FFFF) c -= 16;
if (v & 0x00FF00FF) c -= 8;
if (v & 0x0F0F0F0F) c -= 4;
if (v & 0x33333333) c -= 2;
if (v & 0x55555555) c -= 1;

Complexity

  • Time complexity: \(O(1)\), for bit mask solution

  • Space complexity: \(O(1)\)

Code


    fun isPowerOfFour(n: Int): Boolean = n > 0 &&
      (n and (n - 1)) == 0 && n.countTrailingZeroBits() % 2 == 0

22.10.2023

1793. Maximum Score of a Good Subarray hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/378

Problem TLDR

Max of window_min * (window_size) for window having nums[k]

Intuition

This is not a problem where you need to find a minimum of a sliding window.

By description, we must always include nums[k]. Let’s start from here and try to optimally add numbers to the left and to the right of it.

Approach

  • in an interview, it is safer to write 3 separate loops: move both pointers, then move two others separately:
      while (i > 0 && j < nums.lastIndex) ...
      while (i > 0) ...
      while (j < nums.lastIndex) ...

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun maximumScore(nums: IntArray, k: Int): Int {
      var i = k
      var j = k
      var min = nums[k]
      return generateSequence { when {
        i == 0 && j == nums.lastIndex -> null
        i > 0 && j < nums.lastIndex -> if (nums[i - 1] > nums[j + 1]) --i else ++j
        i > 0 -> --i else -> ++j
      } }.maxOfOrNull {
        min = min(min, nums[it])
        min * (j - i + 1)
      } ?: nums[k]
    }

21.10.2023

1425. Constrained Subsequence Sum hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/377

Problem TLDR

Max sum of subsequence i - j <= k

Intuition

The naive DP approach is to do DFS and memoization:

    fun constrainedSubsetSum(nums: IntArray, k: Int): Int {
      val dp = mutableMapOf<Int, Int>()
      fun dfs(i: Int): Int = if (i >= nums.size) 0 else dp.getOrPut(i) {
        var max = nums[i]
        for (j in 1..k) max = max(max, nums[i] + dfs(i + j))
        max
      }
      return (0..<nums.size).maxOf { dfs(it) }
    }

This solution takes O(nk) time and gives TLE.

Let’s rewrite it to the iterative version to think about further optimization:

    fun constrainedSubsetSum(nums: IntArray, k: Int): Int {
      val dp = mutableMapOf<Int, Int>()
      for (i in nums.indices)
        dp[i] = nums[i] + (i - k..i).maxOf { dp[it] ?: 0 }
      return dp.values.max()
    }

Next, read a hint :) It will suggest to use a Heap. Indeed, looking at this code, we’re just choosing a maximum value from the last k values:

    fun constrainedSubsetSum(nums: IntArray, k: Int): Int =
    with (PriorityQueue<Int>(reverseOrder())) {
      val dp = mutableMapOf<Int, Int>()
      for (i in nums.indices) {
        if (i - k > 0) remove(dp[i - k - 1])
        dp[i] = nums[i] + max(0, peek() ?: 0)
        add(dp[i])
      }
      dp.values.max()
    }

This solution takes O(nlog(k)) time and still gives TLE.

Let’s look at other’s people solutions, just take a hint: decreasing queue. This technique must be remembered, as it is a common trick to find a maximum in a sliding window with O(1) time.

Approach

Decreasing queue flushes all the values that smaller than the current.

  • we’ll store the indices to remove them later if out of k
  • careful with indices

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(k)\)

Code


    fun constrainedSubsetSum(nums: IntArray, k: Int): Int =
    with (ArrayDeque<Int>()) {
      for (i in nums.indices) {
        if (isNotEmpty() && first() < i - k) removeFirst()
        if (isNotEmpty()) nums[i] += max(0, nums[first()])
        while (isNotEmpty() && nums[last()] < nums[i]) removeLast()
        addLast(i)
      }
      nums.max()
    }

20.10.2023

341. Flatten Nested List Iterator medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/376

Problem TLDR

Implement graph iterator

Intuition

We need to save all the deep levels positions, so let’s use a Stack.

Approach

  • we can store nextInt integer in a separate variable, or just leave it in a Stack and do pop on next()
  • it is better to advance after each next() call to know if there is a next position
  • careful with the order of elements when expanding

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


class NestedIterator(nestedList: List<NestedInteger>) : Stack<NestedInteger>() {
    init { 
      addAll(nestedList.reversed())
      advance()
    }
    fun advance() {
      while (isNotEmpty() && !peek().isInteger()) { 
        addAll(pop().list.reversed())
      }
    }
    fun next(): Int = pop().integer.also { advance() }
    fun hasNext(): Boolean = isNotEmpty()
}

19.10.2023

844. Backspace String Compare medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/375

Problem TDLR

Are typing with backspace sequences equal

Intuition

We can use a Stack to evaluate the resulting strings. However, scanning from the end and counting backspaces would work better.

Approach

Remove all of the backspaced chars before comparing

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun backspaceCompare(s: String, t: String): Boolean {
      var si = s.lastIndex
      var ti = t.lastIndex
      while (si >= 0 || ti >= 0) {
        var bs = 0
        while (si >= 0 && (s[si] == '#' || bs > 0))
          if (s[si--] == '#') bs++ else bs--
        bs = 0
        while (ti >= 0 && (t[ti] == '#' || bs > 0))
          if (t[ti--] == '#') bs++ else bs--
        if (si < 0 != ti < 0) return false
        if (si >= 0 && s[si--] != t[ti--]) return false
      }
      return true
    }

18.10.2023

2050. Parallel Courses III hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/374

Problem TLDR

Shortest time to visit all nodes in relations=[from, to] graph

Intuition

We can start from nodes without out siblings - leafs and do Depth-First Search from them, calculating time for each sibling in parallel and choosing the maximum. That is an optimal way to visit all the nodes. For each node, a solution can be cached.

Approach

Let’s use some Kotlin’s API:

  • calculate leafs by subtracting all from nodes from all the nodes 1..n
  • form a graph Map<Int, List<Int>> by using groupBy
  • choose the maximum and return it with maxOf
  • get and put to map with getOrPut

Complexity

  • Time complexity: \(O(nr)\), will visit each node only once, r - average siblings count for each node

  • Space complexity: \(O(n)\)

Code


    fun minimumTime(n: Int, relations: Array<IntArray>, time: IntArray): Int {
      val lastNodes = (1..n) - relations.map { it[0] }
      val fromTo = relations.groupBy({ it[1] }, { it[0] })
      val cache = mutableMapOf<Int, Int>()
      fun dfs(curr: Int): Int = cache.getOrPut(curr) {
        time[curr - 1] + (fromTo[curr]?.maxOf { dfs(it) } ?: 0)
      }
      return lastNodes.maxOf { dfs(it) }
    }

P.S.: we can also just choose the maximum, as it will be the longest path:

    fun minimumTime(n: Int, relations: Array<IntArray>, time: IntArray): Int {
      val fromTo = relations.groupBy({ it[1] }, { it[0] })
      val cache = mutableMapOf<Int, Int>()
      fun dfs(curr: Int): Int = cache.getOrPut(curr) {
        time[curr - 1] + (fromTo[curr]?.maxOf { dfs(it) } ?: 0)
      }
      return (1..n).maxOf { dfs(it) }
    }

17.10.2023

1361. Validate Binary Tree Nodes medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/373

Problem TLDR

Is Binary Tree of leftChild[] & rightChild[] valid

Intuition

There are some examples: image.png

Tree is valid if:

  • all the leafs are connected
  • there is no leaf with more than one in nodes

Approach

For connections check let’s use Union-Find. We also must count in nodes.

Complexity

  • Time complexity: \(O(an)\), a - is for Union-Find search, it is less than 10 for Int.MAX_VALUE nodes, if we implement ranks in Union-Find

  • Space complexity: \(O(n)\)

Code


    fun validateBinaryTreeNodes(n: Int, leftChild: IntArray, rightChild: IntArray): Boolean {
      val uf = IntArray(n) { it }
      val indeg = IntArray(n)
      fun root(x: Int): Int = if (x == uf[x]) x else root(uf[x])
      fun connect(a: Int, b: Int): Boolean {
        if (b < 0) return true
        if (indeg[b]++ > 0) return false
        val rootA = root(a)
        val rootB = root(b)
        uf[rootA] = rootB
        return rootA != rootB
      }
      return (0..<n).all {
        connect(it, leftChild[it]) && connect(it, rightChild[it])
      } && (0..<n).all { root(0) == root(it) }
    }

16.10.2023

119. Pascal’s Triangle II easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/372

Problem TLDR

Pascal’s Triangle

Intuition

One way is to generate sequence:

    fun getRow(rowIndex: Int): List<Int> =
      generateSequence(listOf(1)) {
        listOf(1) + it.windowed(2) { it.sum() } + 1
      }.elementAtOrElse(rowIndex) { listOf() }

Another way is to use fold

Approach

  • notice, we can add a simple 1 to collection by +
  • use sum and windowed

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n)\)

Code


    fun getRow(rowIndex: Int): List<Int> =
      (1..rowIndex).fold(listOf(1)) { r, _ ->
        listOf(1) + r.windowed(2) { it.sum() } + 1
      }

15.10.2023

1269. Number of Ways to Stay in the Same Place After Some Steps hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/371

Problem TLDR

Number of ways to return to 0 after moving left, right or stay steps time

Intuition

We can do a brute force Depth First Search, each time moving position to the left, right or stay, adjusting steps left. After all the steps used, we count the way if it is at 0 position. The result will only depend on the inputs, so can be cached.

Approach

  • one optimization can be to use only half of the array, as it is symmetrical
  • use when instead of if - else, because you can forget else:
if (some) 0L
if (other) 1L // must be `else if`

Complexity

  • Time complexity: \(O(s^2)\), max index can be no more than number of steps, as we move by 1 at a time

  • Space complexity: \(O(s^2)\)

Code


    fun numWays(steps: Int, arrLen: Int): Int {
      val m = 1_000_000_007L
      val dp = mutableMapOf<Pair<Int, Int>, Long>()
      fun dfs(i: Int, s: Int): Long = dp.getOrPut(i to s) { when {
        s == steps && i == 0 -> 1L
        i < 0 || i >= arrLen || s >= steps -> 0L
        else -> {
          val leftRight = (dfs(i - 1, s + 1) + dfs(i + 1, s + 1)) % m
          val stay = dfs(i, s + 1)
          (leftRight + stay) % m
        }
      } }
      return dfs(0, 0).toInt()
    }

14.10.2023

2742. Painting the Walls hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/369

Problem TLDR

Min cost to complete all tasks using one paid cost[] & time[] and one free 0 & 1 workers

Intuition

Let’s use a Depth First Search and try each wall by free and by paid workers.

After all the walls taken, we see if it is a valid combination: if paid worker time less than free worker time, then free worker dares to take task before paid worker, so it is invalid. We will track the time, keeping it around zero: if free worker takes a task, time flies back, otherwise time goes forward by paid worker request. The valid combination is t >= 0.

      fun dfs(i: Int, t: Int): Int = dp.getOrPut(i to t) {
        if (i == cost.size) { if (t < 0) 1_000_000_000 else 0 }
        else {
          val takePaid = cost[i] + dfs(i + 1, t + time[i])
          val takeFree = dfs(i + 1, t - 1)
          min(takePaid, takeFree)
        }
      }

This solution almost works, however gives TLE, so we need another trick min(cost.size, t + time[i]):

  • Pay attention that free worker takes exactly 1 point of time that is, can paint all the walls by n points of time.
  • So, after time passes n points it’s over, we can use free worker, or basically we’re done.
  • An example of that is times: 7 6 5 4 3 2 1. If paid worker takes task with time 7, all the other tasks will be left for free worker, because he is doing them by 1 points of time.

Approach

  • store two Int’s in one by bits shifting
  • or use an Array for the cache, but code becomes complex

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun paintWalls(cost: IntArray, time: IntArray): Int {
      val dp = mutableMapOf<Int, Int>()
      fun dfs(i: Int, t: Int): Int = dp.getOrPut((i shl 16) + t) {
        if (i == cost.size) { if (t < 0) 1_000_000_000 else 0 }
        else {
          val takePaid = cost[i] + dfs(i + 1, min(cost.size, t + time[i]))
          val takeFree = dfs(i + 1, t - 1)
          min(takePaid, takeFree)
        }
      }
      return dfs(0, 0)
    }

13.10.2023

746. Min Cost Climbing Stairs easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/368

Problem TLDR

Classic DP: climbing stairs

Intuition

Start with brute force approach: consider every position and choose one of a two - use current stair or use next. Given that, the result will only depend on the input position, so can be cached. This will give a simple DFS + memo DP code:

    fun minCostClimbingStairs(cost: IntArray): Int {
      val dp = mutableMapOf<Int, Int>()
      fun dfs(curr: Int): Int = dp.getOrPut(curr) {
        if (curr >= cost.lastIndex) 0
        else min(
          cost[curr] + dfs(curr + 1),
          cost[curr + 1] + dfs(curr + 2)
        )
      }
      return dfs(0)
    }

This is accepted, but can be better if rewritten to bottom up and optimized.

Approach

After rewriting the recursive solution to iterative bottom up, we can notice, that only two of the previous values are always used. Convert dp array into two variables.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun minCostClimbingStairs(cost: IntArray): Int {
      var curr = 0
      var prev = 0
      for (i in 0..<cost.lastIndex) 
        curr = min(cost[i + 1] + curr, cost[i] + prev)
              .also { prev = curr }
      return curr
    }

12.10.2023

1095. Find in Mountain Array hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/367

Problem TLDR

Binary Search in a mountain

Intuition

First, find the top of the slope. Next, do two Binary Searches on the left and on the right slopes

Approach

  • to find a top search for where the increasing slope ends

For better Binary Search code

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always update the result top = max(top, mid)
  • always move the borders lo = mid + 1, hi = mid - 1
  • move border that cuts off the irrelevant part of the array

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(1)\)

Code

 
    fun findInMountainArray(target: Int, mountainArr: MountainArray): Int {
      var lo = 1
      var hi = mountainArr.length() - 1
      var top = -1
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (mountainArr.get(mid - 1) < mountainArr.get(mid)) {
          top = max(top, mid)
          lo = mid + 1
        } else hi = mid - 1
      }
      lo = 0
      hi = top
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val m = mountainArr.get(mid)
        if (m == target) return mid
        if (m < target) lo = mid + 1 else hi = mid - 1
      }
      lo = top
      hi = mountainArr.length() - 1
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val m = mountainArr.get(mid)
        if (m == target) return mid
        if (m < target) hi = mid - 1 else lo = mid + 1
      }
      return -1
    }
 

11.10.2023

2251. Number of Flowers in Full Bloom hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/366

Problem TLDR

Array of counts of segments in intersection

Intuition

We need to quickly count how many segments are for any particular time. If we sort segments by from position, we can use line sweep, and we also need to track times when count decreases. To find out how many people in a time range we can sort them and use Binary Search.

Approach

  • to track count changes let’s store time deltas in timeToDelta HashMap
  • careful with storing decreases, they are starting in to + 1
  • instead of sorting the segments we can use a TreeMap
  • we need to preserve peoples order, so use separate sorted indices collection

For better Binary Search code:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always save a good result peopleIndBefore = max(.., mid)
  • always move the borders lo = mid + 1, hi = mid - 1
  • if mid is less than target drop everything on the left side: lo = mid + 1

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun fullBloomFlowers(flowers: Array<IntArray>, people: IntArray): IntArray {
      val peopleInds = people.indices.sortedBy { people[it] }
      val timeToDelta = TreeMap<Int, Int>()
      for ((from, to) in flowers) {
        timeToDelta[from] = 1 + (timeToDelta[from] ?: 0)
        timeToDelta[to + 1] = -1 + (timeToDelta[to + 1] ?: 0)
      }
      val res = IntArray(people.size)
      var count = 0
      var lastPeopleInd = -1
      timeToDelta.onEach { (time, delta) ->
        var lo = max(0, lastPeopleInd - 1)
        var hi = peopleInds.lastIndex
        var peopleIndBefore = -1
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          if (people[peopleInds[mid]] < time) {
            peopleIndBefore = max(peopleIndBefore, mid)
            lo = mid + 1
          } else hi = mid - 1
        }
        for (i in max(0, lastPeopleInd)..peopleIndBefore) res[peopleInds[i]] = count
        count += delta
        lastPeopleInd = peopleIndBefore + 1
      }
      return res
    }

10.10.2023

2009. Minimum Number of Operations to Make Array Continuous hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/365

Problem TLDR

Min replacements to make array continuous a[i] = a[i - 1] + 1

Intuition

Use hint. There are some ideas to solve this:

  • if we choose any particular number from the array, we know how the result array must look like - 1 3 4 -> 1 2 3 or 3 4 5 or 4 5 6
  • we can sort the array and discard all numbers left to the current and right to the last of the result. For example, 1 3 4, if current number is 1 we drop all numbers bigger than 3 as 1 2 3 is a result.
  • to find the position of the right border, we can use a Binary Search
  • now we have a range of numbers that almost good, but there can be duplicates. To count how many duplicates in range in O(1) we can precompute a prefix counter of the unique numbers.

Approach

Look at someone else’s solution. For better Binary Search code:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always move the border lo = mid + 1, hi = mid - 1
  • always update the result toPos = min(toPos, mid)
  • choose which border to move by discarding not relevant mid position: if nums[mid] is less than target, we can drop all numbers to the left, so move lo

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


    fun minOperations(nums: IntArray): Int {
      nums.sort()
      val uniqPrefix = IntArray(nums.size) { 1 }
      for (i in 1..<nums.size) {
        uniqPrefix[i] = uniqPrefix[i - 1]
        if (nums[i] != nums[i - 1]) uniqPrefix[i]++
      }
      var minOps = nums.size - 1
      for (i in nums.indices) {
        val from = nums[i]
        val to = from + nums.size - 1
        var lo = i
        var hi = nums.size - 1
        var toPos = nums.size
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          if (nums[mid] > to) {
            toPos = min(toPos, mid)
            hi = mid - 1
          } else lo = mid + 1
        }
        val uniqCount = max(0, uniqPrefix[toPos - 1] - uniqPrefix[i]) + 1
        minOps = min(minOps, nums.size - uniqCount)
      }
      return minOps
    }

9.10.2023

34. Find First and Last Position of Element in Sorted Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/364

Problem TLDR

Binary Search range

Intuition

Just write a Binary Search

Approach

For simpler code:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always move the borders lo = mid + 1, hi = mid - 1
  • always write the found result if (nums[mid] == target)
  • to understand which border to move, consider this thought: if this position is definitely less than target, we can drop it and all that less than it

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(1)\)

Code


    fun searchRange(nums: IntArray, target: Int): IntArray {
      var from = -1
      var lo = 0
      var hi = nums.lastIndex
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (nums[mid] == target) from = min(max(from, nums.size), mid)
        if (nums[mid] < target) lo = mid + 1
        else hi = mid - 1
      }
      var to = from
      lo = maxOf(0, from)
      hi = nums.lastIndex
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (nums[mid] == target) to = max(min(-1, to), mid)
        if (nums[mid] <= target) lo = mid + 1
        else hi = mid - 1
      }
      return intArrayOf(from, to)
    }

8.10.2023

1458. Max Dot Product of Two Subsequences hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/363

Problem TLDR

Max product of two subsequences

Intuition

We can search in all possible subsequences in O(n^2) by choosing between: take element and stop, take and continue, skip first, skip second.

Approach

The top-down aproach is trivial, let’s modify it into bottom up.

  • use sentry dp size to avoid writing ifs

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun maxDotProduct(nums1: IntArray, nums2: IntArray): Int {
      val dp = Array(nums1.size + 1) { Array(nums2.size + 1) { -1000000 } }
      for (j in nums2.lastIndex downTo 0)
        for (i in nums1.lastIndex downTo 0)
          dp[i][j] = maxOf(
              nums1[i] * nums2[j],
              nums1[i] * nums2[j] + dp[i + 1][j + 1],
              dp[i][j + 1],
              dp[i + 1][j])
      return dp[0][0]
    }

7.10.2023

1420. Build Array Where You Can Find The Maximum Exactly K Comparisons hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/362

Problem TLDR

Count possible arrays of n 1..m values increasing k times

Intuition

First, try to write down some examples of arrays. There are some laws of how the number of arrays grows.

Next, use hint :)

Then just write Depth First Search of all possible numbers for each position and count how many times numbers grows. Stop search when it is bigger than k times. The result can be cached.

Approach

  • use Long to avoid overflows

Complexity

  • Time complexity: \(O(nkm^2)\), nkm - is a search depth, and another m for internal loop

  • Space complexity: \(O(nkm)\)

Code


    fun numOfArrays(n: Int, m: Int, k: Int): Int {
      val mod = 1_000_000_007L
      val dp = Array(n) { Array(m + 1) { Array(k + 1) { -1L } } }
      fun dfs(i: Int, max: Int, c: Int): Long = 
        if (c > k) 0L
        else if (i == n) { if (c == k) 1L else 0L }
        else dp[i][max][c].takeIf { it >= 0 } ?: {
          var sum = (max * dfs(i + 1, max, c)) % mod
          for (x in (max + 1)..m) 
            sum = (sum + dfs(i + 1, x, c + 1)) % mod
          sum
        }().also { dp[i][max][c] = it }
      return dfs(0, 0, 0).toInt()
    }

6.10.2023

343. Integer Break medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/361

Problem TLDR

Max multiplication of the number split

Intuition

We can search from all possible splits. The result will only depend on the input n, so can be cached.

Approach

  • one corner case is the small numbers, like 2, 3, 4: ensure there is at least one split happen

Complexity

  • Time complexity: \(O(n^2)\), recursion depth is n and another n is in the loop. Without cache, it would be n^n

  • Space complexity: \(O(n)\)

Code


    val cache = mutableMapOf<Int, Int>()
    fun integerBreak(n: Int, canTake: Boolean = false): Int = 
      if (n == 0) 1 else cache.getOrPut(n) {
        (1..if (canTake) n else n - 1).map {
          it * integerBreak(n - it, true)
        }.max()
      }

5.10.2023

229. Majority Element II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/360

Problem TLDR

Elements with frequency > size / 3

Intuition

The naive solution, which is to count frequencies, can be this one-liner:

    fun majorityElement(nums: IntArray) = nums
      .groupBy { it }
      .filter { (k, v) -> v.size > nums.size / 3 }
      .map { (k, v) -> k }

However, to solve it in O(1) we need to read the hint: Moore algo. One idea is that there are at most only two such elements can coexist:

    // 111 123 333
    // 1111 1234 4444
    // 11111 12345 55555

The second idea is a clever counting of three buckets: first candidate, second candidate and others. We decrease candidates counters if x in the others bucket, and change candidate if it’s counter 0.

Approach

Steal someone’s else solution or ask ChatGPT about Moore algorithm to find majority element.

  • make sure you understand why the resulting elements are majority

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun majorityElement(nums: IntArray): List<Int> {
      var x1 = Int.MIN_VALUE
      var x2 = Int.MIN_VALUE
      var count1 = 0
      var count2 = 0
      for (x in nums) when {
        x != x2 && count1 == 0 -> x1 = x.also { count1 = 1 }
        x != x1 && count2 == 0 -> x2 = x.also { count2 = 1 }
        x == x1 -> count1++
        x == x2 -> count2++
        else -> {
          count1 = maxOf(0, count1 - 1)
          count2 = maxOf(0, count2 - 1)
        }
      }
      return buildList {
        if (nums.count { it == x1 } > nums.size / 3) add(x1)
        if (nums.count { it == x2 } > nums.size / 3) add(x2)
      }
    }

4.10.2023

706. Design HashMap easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/359

Problem TLDR

Design a HashMap

Intuition

The simple implementation consists of a growing array of buckets, where each bucket is a list of key-value pairs.

Approach

For better performance:

  • use LinkedList
  • start with smaller buckets size

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\), for all operations

Code


class MyHashMap() {
    var table = Array<MutableList<Pair<Int, Int>>>(16) { mutableListOf() }
    var count = 0

    fun bucket(key: Int) = table[key % table.size]

    fun rehash() = with(table.flatMap { it }) {
      table = Array(table.size * 2) { mutableListOf() }
      for ((key, value) in this) bucket(key) += key to value
    }

    fun put(key: Int, value: Int) = with(bucket(key)) {
      if (removeAll { it.first == key }) count++
      this += key to value
      if (count > table.size) rehash()
    }

    fun get(key: Int) = bucket(key)
      .firstOrNull { it.first == key }?.second ?: -1

    fun remove(key: Int) {
      if (bucket(key).removeAll { it.first == key }) count--
    }
}

3.10.2023

1512. Number of Good Pairs easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/358

Problem TLDR

Count equal pairs

Intuition

The naive N^2 solution will work. Another idea is to store the number frequency so far and add it to the current result.

Approach

Let’s use Kotlin’s API:

  • with
  • fold

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun numIdenticalPairs(nums: IntArray) = with(IntArray(101)) {
      nums.fold(0) { r, t -> r + this[t].also { this[t]++ } }
    }

2.10.2023

2038. Remove Colored Pieces if Both Neighbors are the Same Color medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/357

Problem TLDR

Is A wins in middle-removing AAA or BBB game

Intuition

We quickly observe, that removing A in BBAAABB doesn’t make B turn possible, so the outcome does not depend on how exactly positions are removed. A can win if it’s possible game turns are more than B. So, the problem is to find how many consequent A’s and B’s are.

Approach

We can count A and B in a single pass, however, let’s write a two-pass one-liner using window Kotlin method.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), can be O(1) if asSequence used

Code


    fun winnerOfGame(colors: String) = with(colors.windowed(3)) {
      count { it.all { it == 'A' } } > count { it.all { it == 'B' } } 
    }

1.10.2023

557. Reverse Words in a String III easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/356

Problem TLDR

Reverse words

Intuition

In an interview in-place solution expected. Maintain two pointers, and adjust one until end of word reached. This still takes O(N) space in JVM.

Approach

Let’s write a one-liner using Kotlin’s API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun reverseWords(s: String) = 
      s.reversed().split(" ").reversed().joinToString(" ")

30.09.2023

456. 132 Pattern medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/355

Problem TLDR

132 pattern in array

Intuition

If we slide the array from behind, we simplify the task to find the smallest element. When searching for largest decreasing subsequence we can use a monotonic Stack.

Approach

  • we must remember the popped element, as it is the second largest one

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun find132pattern(nums: IntArray): Boolean {
      val stack = Stack<Int>()
      var lo = Int.MIN_VALUE
      return (nums.lastIndex downTo 0).any { i ->
        while (stack.isNotEmpty() && stack.peek() < nums[i]) lo = stack.pop()
        stack.push(nums[i])
        nums[i] < lo
      }
    }

29.09.2023

896. Monotonic Array easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/354

Problem TLDR

Is array monotonic

Intuition

Let’s compute the diffs, then array is monotonic if all the diffs have the same sign.

Approach

Let’s use Kotlin’s API:

  • asSequence - to avoid creating a collection
  • map
  • filter
  • windowed - scans array by x sized sliding window

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun isMonotonic(nums: IntArray) =
      nums.asSequence().windowed(2)
      .map { it[0] - it[1] }
      .filter { it != 0 }
      .windowed(2)
      .all { it[0] > 0 == it[1] > 0 }
      

28.09.2023

905. Sort Array By Parity easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/353

Problem TLDR

Sort an array by even-odd

Intuition

There are built-in functions. However, in an interview manual partition is expected: maintain the sorted border l and adjust it after swapping.

Approach

Let’s write them all.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code

	// 1
	fun sortArrayByParity(nums: IntArray) = nums.also {
      var l = 0
      for (r in nums.indices) if (nums[r] % 2 == 0) 
        nums[r] = nums[l].also { nums[l++] = nums[r] }
    }
    
    // 2
    fun sortArrayByParity(nums: IntArray) = 
      nums.partition { it % 2 == 0 }.toList().flatten()
      
    // 3  
    fun sortArrayByParity(nums: IntArray) = nums.sortedBy { it % 2 }

27.09.2023

880. Decoded String at Index medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/352

Problem TLDR

k-th character in an encoded string like a3b2=aaabaaab

Intuition

We know the resulting length at every position of the encoded string. For example,

a3b2
1348

The next step, just walk from the end of the string and adjust k, by undoing repeating operation:

    // a2b2c2
    // 0 1 2 3 4 5 6 7 8 9 10 11 12 13
    // a a b a a b c a a b a  a  b  c
    // a2b2c2 = 2 x a2b2c = 2*(a2b2 + c) = 
    // 2*(2*(a2 + b) + c) = 2*(2*(2*a + b) + c)
    //  k=9         9%(len(a2b2c)/2)
    //
    // a3b2    k=7
    // 12345678
    // aaabaaab
    // aaab    k=7%4=3
    //
    // abcd2    k=6
    // 12345678
    // abcdabcd  k%4=2

Approach

  • use Long to avoid overflow
  • check digit with isDigit
  • Kotlin have a nice conversion function digitToInt
  • corner case is when search is become 0`, we must return first non-digit character

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code

    fun decodeAtIndex(s: String, k: Int): String {
      val lens = LongArray(s.length) { 1L }
      for (i in 1..s.lastIndex) lens[i] = if (s[i].isDigit()) 
          lens[i - 1] * s[i].digitToInt()
        else lens[i - 1] + 1 
      var search = k.toLong()
      for (i in s.lastIndex downTo 0) if (s[i].isDigit()) 
          search = search % (lens[i] / s[i].digitToInt().toLong())
        else if (lens[i] == search || search == 0L) return "" + s[i]
      throw error("not found")
    }

26.09.2023

316. Remove Duplicate Letters medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/351

Problem TLDR

Lexicographical smallest subsequence without duplicates

Intuition

The brute force way would be to just consider every position and do a DFS. To pass the test case, however, there is a greedy way: let’s take characters and pop them if new is smaller and the duplicate exists later in a string.

      // 01234
      //   234
      // bcabc
      // *      b    
      //  *     bc   
      //   *    a, pop c, pop b
      //    *   ab
      //     *  abc

Approach

We can use Kotlin’s buildString API instead of a Stack

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun removeDuplicateLetters(s: String) = buildString {
      var visited = mutableSetOf<Char>()
      val lastInds = mutableMapOf<Char, Int>()
      s.onEachIndexed { i, c -> lastInds[c] = i}
      s.onEachIndexed { i, c ->
        if (visited.add(c)) {
          while (isNotEmpty() && last() > c && i < lastInds[last()]!!) 
            visited.remove(last()).also { setLength(lastIndex) }
          append(c)
        }
      }
    }

25.09.2023

389. Find the Difference easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/350

Problem TLDR

Strings difference by a single char

Intuition

We can use frequency map. Or just calculate total sum by Char Int value.

Approach

Let’s use Kotlin’s API sumBy

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun findTheDifference(s: String, t: String) = 
      (t.sumBy { it.toInt() } - s.sumBy { it.toInt() }).toChar()

24.09.2023

799. Champagne Tower medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/349

Problem TLDR

Positional flow value in a Pascal’s Triangle

Intuition

Let’s treat every glass value as the total flow passed through it. Otherwise, it is a standard Pascal’s Triangle problem: reuse the previous row to compute the next.

Approach

  • if flow is less than 1.0 (full), it will contribute 0.0 to the next row. This can be written as max(0, x - 1)
  • careful with a champagne, it will beat you in a head

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n)\)

Code


    fun champagneTower(poured: Int, query_row: Int, query_glass: Int): Double {
      var flow = listOf(poured.toDouble())
      repeat(query_row) {
        val middle = flow.windowed(2).map { (a, b) -> 
          max(0.0, a - 1.0) / 2 + max(0.0, b - 1.0) / 2 
        }
        val edge = listOf(maxOf(0.0, flow.first() - 1.0) / 2)
        flow = edge + middle + edge
      }
      return minOf(flow[query_glass], 1.0)
    }

23.09.2023

1048. Longest String Chain medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/348

Problem TLDR

Longest chain of words with single character added

Intuition

We can build a graph, then use DFS to find a maximum depth. To detect predecessor, we can use two pointers.

Approach

Careful with two pointers: iterate over short string and adjust the second pointer for long, not vice versa.

Complexity

  • Time complexity: \(O(w*n^2)\), to build a graph

  • Space complexity: \(O(n^2)\), for graph

Code


    fun longestStrChain(words: Array<String>): Int {
      fun isPred(a: String, b: String): Boolean {
        if (a.length != b.length - 1) return false
        var i = -1
        return !a.any { 
          i++
          while (i < b.length && it != b[i]) i++
          i == b.length
        }
      }
      val fromTo = mutableMapOf<String, MutableSet<String>>()
      for (a in words) 
        for (b in words)
          if (isPred(a, b))
            fromTo.getOrPut(a) { mutableSetOf() } += b
      val cache = mutableMapOf<String, Int>()
      fun dfs(w: String): Int = cache.getOrPut(w) {
        1 + (fromTo[w]?.map { dfs(it) }?.max() ?: 0)
      }
      return words.map { dfs(it) }?.max() ?: 0
    }

22.09.2023

392. Is Subsequence easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/347

Problem TLDR

Is string a subsequence of another

Intuition

One possible way is to build a Trie, however this problem can be solved just with two pointers.

Approach

Iterate over one string and adjust pointer of another.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun isSubsequence(s: String, t: String): Boolean {
      var i = -1
      return !s.any { c ->
        i++
        while (i < t.length && t[i] != c) i++
        i == t.length
      }
    }

21.09.2023

4. Median of Two Sorted Arrays hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/346

Problem TLDR

Median in two concatenated sorted arrays

Intuition

We already know the target position of the median element in the concatenated array.

There is an approach with Binary Search, but it’s harder to come up with in an interview and write correctly.

Approach

We can maintain two pointers and increase them one by one until targetPos reached.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
      val targetPos = (nums1.size + nums2.size) / 2
      var i = 0
      var j = 0
      var prev = 0
      var curr = 0
      while (i + j <= targetPos) {
        prev = curr
        curr = when {
          i == nums1.size -> nums2[j++]
          j == nums2.size -> nums1[i++]
          nums1[i] <= nums2[j] -> nums1[i++]
          else -> nums2[j++]
        } 
      }
      return if ((nums1.size + nums2.size) % 2 == 0) 
        (prev + curr) / 2.0 
       else 
        curr.toDouble()
    }

20.09.2023

1658. Minimum Operations to Reduce X to Zero medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/345

Problem TLDR

Min suffix-prefix to make an x

Intuition

We can reverse the problem: find the middle of the array to make an arr_sum() - x. Now, this problem can be solved using a sliding window technique.

Approach

For more robust sliding window:

  • use safe array iteration for the right border
  • use explicit windowSize variable
  • check the result every time

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun minOperations(nums: IntArray, x: Int): Int {
      val targetSum = nums.sum() - x
      var windowSize = 0
      var currSum = 0
      var res = Int.MAX_VALUE
      nums.onEachIndexed { i, n ->
        currSum += n
        windowSize++
        while (currSum > targetSum && windowSize > 0)
          currSum -= nums[i - (windowSize--) + 1]
        if (currSum == targetSum) 
          res = minOf(res, nums.size - windowSize)
      }
      return res.takeIf { it < Int.MAX_VALUE } ?: -1
    }

19.09.2023

287. Find the Duplicate Number medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/344

Problem TLDR

Found duplicate in array, each value is in 1..<arr.size

Intuition

Hint: 4 2 2 2 2 ... 2 is also the case. What we can see, is that every value is in the 1..<arr.size range, so we can temporarly store the flag in here, then revert it back in the end.

    //   0 1 2 3 4  sz = 5
    //   3 1 3 4 2
    // 3       *  
    // 1   *  
    // 3       x
    //        

Approach

For a flag we can just add some big value to the number, or make it negative, for example.

Let’s write it using some Kotlin’s API:

  • first
  • also - notice how it doesn’t require brackets

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun findDuplicate(nums: IntArray) = nums.first { n ->
        nums[n % nums.size] >= nums.size
        .also { nums[n % nums.size] += nums.size }
      } % nums.size
      .also { for (j in nums.indices) nums[j] %= nums.size }

18.09.2023

1337. The K Weakest Rows in a Matrix easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/343

Problem TLDR

k indices with smallest row sum in a binary matrix

Intuition

We can precompute row sums, then use a Priority Queue to find k smallest. However, just sorting all will also work.

Approach

Let’s use Kotlin’s collections API

Complexity

  • Time complexity: \(O(n^2logn)\)

  • Space complexity: \(O(n^2)\)

Code


    fun kWeakestRows(mat: Array<IntArray>, k: Int) = mat
        .map { it.filter { it == 1 }.sum() ?: 0 }
        .withIndex()
        .sortedBy { it.value }
        .map { it.index }
        .take(k)
        .toIntArray()

16.09.2023

1631. Path With Minimum Effort medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/341

Problem TLDR

Minimum absolute difference in path top-left to right-bottom

Intuition

To find an optimal path using some condition, we can use A* algorithm:

  • add node to PriorityQueue
  • choose the “optimal” one
  • calculate a new heuristic for siblings and add to PQ

Approach

  • use directions sequence for more clean code

Complexity

  • Time complexity: \(O(nmlog(nm))\)

  • Space complexity: \(O(nm)\)

Code


    val dirs = sequenceOf(1 to 0, 0 to 1, 0 to -1, -1 to 0)
    fun minimumEffortPath(heights: Array<IntArray>): Int {
      val pq = PriorityQueue<Pair<Pair<Int, Int>, Int>>(compareBy { it.second })
      pq.add(0 to 0 to 0)
      val visited = HashSet<Pair<Int, Int>>()
      while (pq.isNotEmpty()) {
        val (xy, diff) = pq.poll()
        if (!visited.add(xy)) continue
        val (x, y) = xy
        if (x == heights[0].lastIndex && y == heights.lastIndex) return diff
        dirs.map { (dx, dy) -> x + dx to y + dy }
          .filter { (x1, y1) -> x1 in 0..<heights[0].size && y1 in 0..<heights.size }
          .forEach { (x1, y1) -> pq.add(x1 to y1 to maxOf(diff, abs(heights[y][x] - heights[y1][x1]))) }
      }
      return 0
    }

15.09.2023

1584. Min Cost to Connect All Points medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/340

Problem TLDR

Min manhatten distance connected graph

Intuition

We can start from any points, for example, 0. Next, we must iterate over all possible edges and find one with minimum distance.

Approach

  • use Priority Queue to sort all edges by distance
  • we can stop after all nodes are visited once
  • we can consider only the last edge distance in path

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun minCostConnectPoints(points: Array<IntArray>): Int {
      fun dist(from: Int, to: Int) = 
        abs(points[from][0] - points[to][0]) + abs(points[from][1] - points[to][1])
      val notVisited = points.indices.toMutableSet()
      val pq = PriorityQueue<Pair<Int, Int>>(compareBy({ it.second }))
      pq.add(0 to 0)
      var sum = 0
      while (notVisited.isNotEmpty()) {
        val curr = pq.poll()
        if (!notVisited.remove(curr.first)) continue
        sum += curr.second
        for (to in notVisited) pq.add(to to dist(curr.first, to))
      }
      return sum
    }

14.09.2023

332. Reconstruct Itinerary hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/339

Problem TLDR

Smallest lexical order path using all the tickets

Intuition

We can build a graph, then do DFS in a lexical order, backtracking. First path with all tickets used will be the answer.

Approach

  • graph has directed nodes
  • sort nodes lists by strings comparison
  • current node is always the last in the path

Complexity

  • Time complexity: \(O(x^n)\), where x - is an average edges count per node

  • Space complexity: \(O(n)\)

Code


    fun findItinerary(tickets: List<List<String>>): List<String> {
      val fromTo = mutableMapOf<String, MutableList<Pair<Int, String>>>()
      tickets.forEachIndexed { i, (from, to) ->
        fromTo.getOrPut(from) { mutableListOf() } += i to to
      }
      for (list in fromTo.values) list.sortWith(compareBy { it.second })
      val usedTickets = mutableSetOf<Int>()
      var path = mutableListOf("JFK")
      fun dfs(): List<String> = 
        if (usedTickets.size == tickets.size) path.toList()
        else fromTo[path.last()]?.asSequence()?.map { (ind, next) -> 
          if (usedTickets.add(ind)) {
            path.add(next)
            dfs().also {
              path.removeAt(path.lastIndex)
              usedTickets.remove(ind)
            }
          } else emptyList()
        }?.filter { it.isNotEmpty() }?.firstOrNull() ?: emptyList()
      return dfs()
    }

13.09.2023

135. Candy hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/338

Problem TLDR

Minimum candies count to satisfy condition: ratings[i] < ratings[i-1] must give more candies to i-1

Intuition

Let’s observe the example:

    // 0 1 2 3 4 5 6 7 8
    // 1 2 2 3 2 1 5 3 4
    // 1 1 1 1 1 1 1 1 1
    //   1   1 1   1   1
    //       1
    // 1 -> [0]
    // 3 -> [2, 4]
    // 6 -> [5, 7]
    // 8 -> [7]
    //
    // 1 2 3 4 5 6 7 8 9
    // 1 1 1 1 1 1 1 1 1
    //   1 1 1 1 1 1 1 1
    //     1 1 1 1 1 1 1
    //       1 1 1 1 1 1
    //         1 1 1 1 1
    //           1 1 1 1
    //             1 1 1
    //               1 1
    //                 1
    // 1 <- 2 <- 3 ...

We can look at this as a graph with nodes of siblings from higher rating to lower. Then the minimum number of candies is a maximum graph path length.

Approach

  • we can reuse depth value for each visited node

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun candy(ratings: IntArray): Int {
      val fromTo = mutableMapOf<Int, MutableList<Int>>()
      for (i in 1..<ratings.size)
        if (ratings[i] > ratings[i - 1]) 
          fromTo.getOrPut(i) { mutableListOf() } += i - 1
        else if (ratings[i] < ratings[i -1]) 
          fromTo.getOrPut(i - 1) { mutableListOf() } += i
      val depth = IntArray(ratings.size)
      fun maxDepth(curr: Int): Int =
        depth[curr].takeIf { it > 0 } ?:
        (1 + (fromTo[curr]?.map { maxDepth(it) }?.maxOrNull() ?: 0))
        .also { depth[curr] = it }
      return ratings.indices.sumBy { maxDepth(it) }
    }

12.09.2023

1647. Minimum Deletions to Make Character Frequencies Unique medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/337

Problem TLDR

Minimum removes duplicate frequency chars from string

Intuition

    // b b c e b a b
    // 1 1 1 4

Characters doesn’t matter, only frequencies. Let’s sort them and scan one-by-one from biggest to small and descriase max value.

Approach

Let’s use Kotlin collections API:

  • groupBy - converts string into groups by characters
  • sortedDescending - sorts by descending
  • sumBy - iterates over all values and sums the lambda result

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code



    fun minDeletions(s: String): Int {
      var prev = Int.MAX_VALUE
      return s.groupBy { it }.values
        .map { it.size }
        .sortedDescending()
        .sumBy {
          prev = maxOf(0, minOf(it, prev - 1))
          maxOf(0, it - prev)
        }
    }

11.09.2023

1282. Group the People Given the Group Size They Belong To medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/336

Problem TLDR

Groups from groups sizes array

Intuition

First, group by sizes, next, chunk by groups size each.

Approach

Let’s write it using Kotlin collections API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    // 2 1 3 3 3 2 1 1 1 2 2
    // 0 1 2 3 4 5 6 7 8 9 10
    // 2 -> 0 5 [9 10]
    // 1 -> 1 [6] [7] [8]
    // 3 -> 2 3 4
    fun groupThePeople(groupSizes: IntArray) =
      groupSizes
      .withIndex()
      .groupBy { it.value }
      .flatMap { (sz, nums) -> 
        nums.map { it.index }.chunked(sz) 
      }

10.09.2023

1359. Count All Valid Pickup and Delivery Options hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/335

Problem TLDR

Count permutations of the n pickup -> delivery orders

Intuition

Let’s look at how orders can be placed and draw the picture:

      // 1: p1 d1            variantsCount = 1
      // 2:                  length = 2
      // "___p1____d1_____": vacantPlaces = 3
      //              p2 d2
      //        p2       d2
      // p2              d2
      //        p2 d2
      // p2        d2
      // p2 d2
      //                                variantsCount = 6
      // 3:                             length = 4
      // "___p1____d1____p2____d2____": vacantPlaces = 5
      //                         p3 d3 
      //                    p3      d3
      //              p3            d3
      //        p3                  d3
      // p3                         d3
      //                    p3 d3
      //              p3       d3             x6
      //        p3             d3
      // p3                    d3
      //              p3 d3
      //        p3       d3
      // p3              d3
      //        p3 d3
      // p3        d3
      // p3 d3 

In this example, we can see the pattern:

  • the number of vacant places grows by 2 each round
  • inside each round there are repeating parts of arithmetic sum, that can be reused

Approach

  • use Long to avoid overflow

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun countOrders(n: Int): Int {
      var variantsCount = 1L
      var currSum = 1L
      var item = 1L
      val m = 1_000_000_007L
      repeat(n - 1) {
        item = (item + 1L) % m
        currSum = (currSum + item) % m
        item = (item + 1L) % m
        currSum = (currSum + item) % m
        variantsCount = (variantsCount * currSum) % m
      }
      return variantsCount.toInt()
    }

9.09.2023

377. Combination Sum IV medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/334

Problem TLDR

Number of ways to sum up array nums to target

Intuition

This is a canonical DP knapsack problem: choose one of the items and decrease the target by its value. If target is zero - we have a single way, if negative - no ways, otherwise keep taking items. The result will only depend on the target, so can be cached.

Approach

In this code:

  • trick to make conversion 0 -> 1, negative -> 0: 1 - (t ushr 31), it shifts the leftmost bit to the right treating sign bit as a value bit, converting any negative number to 1 and positive to 0
  • IntArray used instead of Map using takeIf Kotlin operator

Complexity

  • Time complexity: \(O(n^2)\), n for the recursion depth, and n for the inner iteration

  • Space complexity: \(O(n^2)\)

Code


    fun combinationSum4(nums: IntArray, target: Int): Int {
      val cache = IntArray(target + 1) { -1 }
      fun dfs(t: Int): Int = if (t <= 0) 1 - (t ushr 31) else 
        cache[t].takeIf { it >= 0 } ?:
        nums.sumBy { dfs(t - it) }.also { cache[t] = it }
      return dfs(target)
    }

8.09.2023

118. Pascal’s Triangle easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/333

Problem TLDR

Pascal Triangle

Intuition

Each row is a previous row sliding window sums concatenated with 1

Approach

Let’s write it using Kotlin API

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun generate(numRows: Int) = (2..numRows)
      .runningFold(listOf(1)) { r, _ ->
        listOf(1) + r.windowed(2).map { it.sum() } + listOf(1)
      }

7.09.2023

92. Reverse Linked List II medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/332

Problem TLDR

Reverse a part of Linked List

Intuition

We need to find a point where to start reversing after left steps, then do the reversing right - left steps and finally connect to tail.

Approach

  • use Dummy head technique to avoid reversed head corner case
  • better do debug right in the code

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code

    fun reverseBetween(head: ListNode?, left: Int, right: Int): ListNode? {
      val dummy = ListNode(0).apply { next = head }
      var prev: ListNode? = dummy
      var curr = prev             // d-1-2-3-4-5  2 4
      repeat(left) {              // pc
        prev = curr               // p c
        curr = curr?.next         //   p c
      }                                                
      val head = prev             // d-1-2-3-4-5  2 4
      val tail = curr             //   h t
      prev = curr                                    
      curr = curr?.next           //     p c
      repeat(right - left) {      //     p c n
        val next = curr?.next     //      <p c n
        curr?.next = prev         //     p<c n
        prev = curr               //      <p<c n
        curr = next               //     2<p c
      }                           //     2<3<p c
      head?.next = prev           // d-1-2-3-4-5  2 4
      tail?.next = curr           //   h t<3<p c
      return dummy.next
    }

6.09.2023

725. Split Linked List in Parts medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/331

Problem TLDR

Split Linked List into k almost equal lists

Intuition

First, precompute sizes, by adding to buckets one-by-one in a loop. Next, just move list pointer by sizes values.

Approach

Do not forget to disconnect nodes.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\) for the sizes array and for the result

Code


    fun splitListToParts(head: ListNode?, k: Int): Array<ListNode?> {
      val sizes = IntArray(k)
      var i = 0
      var curr = head
      while (curr != null) {
        sizes[i++ % k]++
        curr = curr.next
      }
      curr = head
      return sizes.map { sz ->
        curr.also {
          repeat(sz - 1) { curr = curr?.next }
          curr = curr?.next.also { curr?.next = null }
        }
      }.toTypedArray()
    }

5.09.2023

138. Copy List with Random Pointer medium blog post substack

image.png

Problem TLDR

Copy of a graph

Intuition

Simple way is just store mapping old -> new. The trick from hint is to store new nodes in between the old ones, then mapping became old -> new.next & new -> old.next.

Approach

One iteration to make new nodes, second to assign random field and final to split lists back.

Complexity

    • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


    fun copyRandomList(node: Node?): Node? {
      var curr = node
      while (curr != null) {
        val next = curr.next
        curr.next = Node(curr.`val`).apply { this.next = next }
        curr = next
      }
      curr = node
      while (curr != null) {
        curr.next?.random = curr.random?.next
        curr = curr.next?.next
      }
      curr = node
      val head = node?.next
      while (curr != null) {
        val currNew = curr.next
        val nextOrig = currNew?.next
        val nextNew = nextOrig?.next
        curr.next = nextOrig
        currNew?.next = nextNew
        curr = nextOrig
      }
      return head
    }

4.09.2023

141. Linked List Cycle easy blog post substack

image.png

Problem TLDR

Detect a cycle in a LinkedList

Intuition

Use tortoise and rabbit technique

Approach

Move one pointer one step at a time, another two steps at a time. If there is a cycle, they will meet.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(log(n))\) for recursion (iterative version is O(1))

Code


    fun hasCycle(slow: ListNode?, fast: ListNode? = slow?.next): Boolean = 
      fast != null && (slow == fast || hasCycle(slow?.next, fast?.next?.next))

3.09.2023

62. Unique Paths medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/328

Problem TLDR

Unique paths count, moving right-down from top-left to bottom-right

Intuition

On each cell, the number of paths is a sum of direct up number and direct left number.

Approach

Use single row array, as only previous up row is relevant

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(m)\)

Code


    fun uniquePaths(m: Int, n: Int): Int {
      val row = IntArray(n) { 1 }
      for (y in 1..<m)
        for (x in 1..<n)
          row[x] += row[x - 1]
      return row.last()
    }

2.09.2023

2707. Extra Characters in a String medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/327

Problem TLDR

Min count of leftovers after string split by the dictionary

Intuition

We can search all possible splits at every position when we find a word. To quickly find a word, let’s use a Trie. The result will only depend on the suffix of the string, so can be cached.

Approach

Do DFS, each time compare a skipped result with any take_word result, if found a word. We must continue to search, because some words can be prefixes to others: leet, leetcode -> leetcodes, taking leet is not optimal.

Complexity

  • Time complexity: \(O(n^2)\), DFS depth is n and another n for the inner iteration

  • Space complexity: \(O(n)\)

Code


    class Trie(var w: Boolean = false) : HashMap<Char, Trie>()
    fun minExtraChar(s: String, dictionary: Array<String>): Int {
      val trie = Trie()
      for (w in dictionary) {
        var t = trie
        for (c in w) t = t.getOrPut(c) { Trie() }
        t.w = true
      }
      val cache = mutableMapOf<Int, Int>()
      fun dfs(pos: Int): Int =  if (pos >= s.length) 0 else 
        cache.getOrPut(pos) {
          var min = 1 + dfs(pos + 1)
          var t = trie
          for (i in pos..<s.length) {
            t = t[s[i]] ?: break
            if (t.w) min = minOf(min, dfs(i + 1))
          }
          min
        } 
      return dfs(0)
    }

1.09.2023

338. Counting Bits easy blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/326

Problem TLDR

Array of bits count for numbers 0..n

Intuition

There is a tabulation technique used for caching bits count answer in O(1): for number xxxx0 bits count is count(xxxx) + 0, but for number xxxx1 bits count is count(xxxx) + 1. Now, to make a switch xxxx1 -> xxxx simple divide by 2. Result can be cached.

Approach

We can use DFS + memo, but bottom-up also simple. Result is a DP array itself: DP[number] = bits_count(number). The last bit can be checked by % operation, but and also works.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun countBits(n: Int) = IntArray(n + 1).apply {
        for (i in 0..n) 
          this[i] = this[i / 2] + (i and 1)
      }

31.08.2023

1326. Minimum Number of Taps to Open to Water a Garden hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/325

Problem TLDR

Fill all space between 0..n using minimum intervals

Intuition

We need to fill space between points, so skip all zero intervals. Next, sort intervals and scan them greedily. Consider space between lo..hi as filled. If from > lo we must open another water source. However, there are possible good candidates before, if their to > hi.

      //     0 1 2 3 4 5 6 7 8 9
      //     0 5 0 3 3 3 1 4 0 4
      // 1 5 *************
      //     ^           ^
      //     lo          hi
      // 3 3 *************
      // 4 3   *************
      //       ^         . ^
      //       from      . to
      //                 *** opened++  
      //                 ^ ^
      //                lo hi
      // 5 3     *************
      //                     ^ hi
      // 7 4       *************
      //                       ^ hi finish
      // 6 1           *****
      // 9 4           *********

Approach

Look at others solutions and steal the implementation

Complexity

  • Time complexity: \(O(nlog(n))\), for sorting

  • Space complexity: \(O(n)\), to store the intervals

Code


    fun minTaps(n: Int, ranges: IntArray): Int {
      var opened = 0
      var lo = -1
      var hi = 0
      ranges.mapIndexed { i, v -> maxOf(0, i - v) to minOf(i + v, n) }
        .filter { it.first != it.second }
        .sortedBy { (from, _) -> from }
        .onEach { (from, to) ->
          if (from <= lo) hi = maxOf(hi, to)
          else if (from <= hi) {
            lo = hi
            hi = to
            opened++
          }
          if (hi == n) return opened
        }
      return -1
    }

30.08.2023

2366. Minimum Replacements to Sort the Array hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/324

Problem TLDR

Minimum number of number splits to make an array non-decreasing

Intuition

The first idea is, if we walk the array backwards, suffix is a maximum number. The second idea is how to split the current number optimally. Consider example:

        // 3  8   3
        // 3  53  3 +1 split
        // 3  233 3 +1 split
        // 12 233 3 +1 split

We shall not split 8 into numbers bigger than 3, so keep extracting them, until some remainder reached. However, this will not be the case for another example: 2 9 4, when we split 9 -> 5 + 4, we should not split 5 into 1 + 4, but 2 + 3, but optimal split is 3 + 3 + 3, as 3 < 4 and 3 > 2. Another strategy is to consider how many split operations we should do: 9 / 4 = 2, then we know the number of parts: 9 = (x split y split z) = 3 + 3 + 3. Each part is guaranteed to be less than 4 but the maximum possible to sum up to 9.

Approach

  • explicitly write the corner cases to simplify the thinking: ` x < prev, x == prev, prev == 1, x % prev == 0`
  • give a meaningful variable names and don’t prematurely simplify the math
  • try to find the good example to debug the code

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun minimumReplacement(nums: IntArray): Long {
        if (nums.isEmpty()) return 0L
        // 3  8   3
        // 3  53  3 +1 split
        // 3  233 3 +1 split
        // 12 233 3 +1 split
        var prev = nums.last()
        var count = 0L
        for (i in nums.lastIndex downTo 0) {
            if (nums[i] == prev) continue
            if (nums[i] < prev) prev = nums[i]
            else if (prev == 1) count += nums[i] - 1
            else if (nums[i] % prev == 0) count += (nums[i] / prev) - 1
            else {
                val splits = nums[i] / prev // 15 / 4 = 3
                count += splits
                val countParts = splits + 1 // 4 = (3 4 4 4)
                prev = nums[i] / countParts // 15 / 4 = 3
            }
        }
        return count
    }

29.08.2023

2483. Minimum Penalty for a Shop medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/323

Problem TLDR

First index of minimum penalty in array, penalty ‘Y’-> 1, ‘N’ -> -1

Intuition

Iterate from the end and compute the suffix penalty.

Approach

Suffix penalty is a difference between p_closed - p_opened.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun bestClosingTime(customers: String): Int {
      var p = 0
      var iMin = customers.length
      var pMin = 0
      for (i in customers.lastIndex downTo 0) {
        if (customers[i] == 'Y') p++ else p--
        if (p <= pMin) {
          iMin = i
          pMin = p
        }
      }
      return iMin
    }

28.08.2023

225. Implement Stack using Queues easy blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/322

Problem TLDR

Create a Stack using Queue’s push/pop methods.

Intuition

We can use a single Queue, and rotate it so that the newly inserted element will be on a first position:

1 push -> [1]
2 push -> [1 2] -> [2 1]
3 push -> [2 1 3] -> [1 3 2] -> [3 2 1] 

Approach

Kotlin has no methods pop, push and peek for ArrayDeque, use removeFirst, add and first.

Complexity

  • Time complexity: \(O(n)\) for insertions, others are O(1)

  • Space complexity: \(O(n)\) for internal Queue, and O(1) operations overhead

Code

class MyStack: Queue<Int> by LinkedList() {
    fun push(x: Int) {
      add(x)
      repeat(size - 1) { add(pop()) } 
    }
    fun pop() = remove()
    fun top() = first()
    fun empty() = isEmpty()
}

27.08.2023

403. Frog Jump hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/321

Problem TLDR

Can jump an array when each jump is k-1..k+1 of the previous

Intuition

The simple Depth-First Search works: iterate over next array items and check if jump to them is in range k-1..k+1. The result only depends on the array suffix and previous jump value k, so can be safely cached. This will take n^3 operations in the worst case.

There is an improvement, we can use Binary Search to quickly find the range for the next positions. Time will be improved to n^2log(n).

Approach

In the interview, it is better to write Binary Search by yourself if you’re unsure about how to adapt built-in binarySearch method to find bisectLeft or bisectRight borders.

  • use simple checks to convert insert position into a border: if (-i - 1 in 0..lastIndex) -i - 1 else i
  • same for from in 0..to, which also checks that from <= to, from >= 0 and to >= 0

Complexity

  • Time complexity: \(O(n^2log(n))\)

  • Space complexity: \(O(n^2)\)

Code


    fun canCross(stones: IntArray): Boolean {
      val dp = mutableMapOf<Pair<Int, Int>, Boolean>()
      fun dfs(i: Int, k: Int): Boolean = dp.getOrPut(i to k) {
        if (i == stones.lastIndex) return@getOrPut true
        var from = stones.binarySearch(stones[i] + maxOf(1, k - 1)).let {
          if (-it - 1 in 0..stones.lastIndex) -it - 1 else it
        }
        var to = stones.binarySearch(stones[i] + k + 1).let {
          if (-it - 2 in 0..stones.lastIndex) -it - 2 else it
        }
        return@getOrPut from in 0..to
          && (from..to).any { dfs(it, stones[it] - stones[i]) }
      }
      return dfs(0, 0)
    }

26.08.2023

646. Maximum Length of Pair Chain medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/320

Problem TLDR

Max count non-overlaping intervals

Intuition

The naive Dynamic Programming n^2 solution works, in a DFS choose between taking or skipping the pair, and cache by pos and prev.

Another solution, is just a line sweep algorithm: consider all ends of the intervals in increasing order, skipping the overlapping ones. It will be optimal, as there are no overlapping intervals past the end.

Approach

Sort and use the border variable, that changes when from > border.

Complexity

  • Time complexity: \(O(nlog(n))\), for sorting

  • Space complexity: \(O(n)\), for the sorted array

Code


    fun findLongestChain(pairs: Array<IntArray>): Int {
      var border = Int.MIN_VALUE
      return pairs.sortedWith(compareBy({ it[1] }))
      .count { (from, to) ->
        (from > border).also { if (it) border = to }
      }
    }

25.08.2023

97. Interleaving String medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/319

Problem TLDR

Can a string be a merge of two other strings

Intuition

Do DFS with two pointers, each time taking a char from the first or the second’s string, the third pointer will be p1 + p2. The result will depend only on the remaining suffixes, so can be safely cached.

Approach

  • calculate the key into a single Int p1 + p2 * 100
  • check that lengths are adding up

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun isInterleave(s1: String, s2: String, s3: String): Boolean {
      val cache = mutableMapOf<Int, Boolean>()
      fun dfs(p1: Int, p2: Int): Boolean = cache.getOrPut(p1 + p2 * 100) {
        p1 < s1.length && p2 < s2.length && (
          s1[p1] == s3[p1 + p2] && dfs(p1 + 1, p2)
          || s2[p2] == s3[p1 + p2] && dfs(p1, p2 + 1)
        )
        || p1 == s1.length && s2.substring(p2) == s3.substring(p1 + p2)
        || p2 == s2.length && s1.substring(p1) == s3.substring(p1 + p2)
      }
      return s1.length + s2.length == s3.length && dfs(0, 0)
    }

24.08.2023

68. Text Justification hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/318

Problem TLDR

Spread words to lines, evenly spacing left->right, and left-spacing the last line

Intuition

Scan word by word, checking maxWidth overflow.

Approach

Separate word letters count and count of spaces. To spread spaces left-evenly, iteratively add spaces one-by-one until maxWidth reached. Using Kotlin built-in functions helps to reduce boilerplate:

  • buildList
  • buildString
  • padEnd

Complexity

  • Time complexity: \(O(wn)\)

  • Space complexity: \(O(wn)\)

Code


    fun fullJustify(words: Array<String>, maxWidth: Int) = buildList<String> {
      val line = mutableListOf<String>()
      fun justifyLeft() = line.joinToString(" ").padEnd(maxWidth, ' ')
      var wLen = 0
      fun justifyFull() = buildString {
        val sp = IntArray(line.size - 1) { 1 }
        var i = 0
        var len = wLen + line.size - 1
        while (len++ < maxWidth && line.size > 1) sp[i++ % sp.size]++
        line.forEachIndexed { i, w ->
          append(w)
          if (i < sp.size) append(" ".repeat(sp[i]))
        }
      }
      words.forEachIndexed { i, w ->
        if (wLen + line.size + w.length > maxWidth) {
          add(if (line.size > 1) justifyFull() else justifyLeft())

          line.clear()
          wLen = 0  
        }
        line += w
        wLen += w.length
      }
      add(justifyLeft())
    }

23.08.2023

767. Reorganize String medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/317

Problem TLDR

Create non repeated subsequent chars string from string

Intuition

What will not work:

  • naive bubble sort like n^2 algorithm – give false negatives
  • n^3 dynamic programming DFS+memo – too slow for the problem

Now, use the hint.

If each time the most frequent char used greedily, solution magically works. (proving that is a homework)

Approach

Use Pri0rityQueue to store indices of the frequencies array. If the next char is repeated, and it is the only one left, we have no solution.

Complexity

  • Time complexity: \(O(nlog(n))\), each poll and insert is log(n) in PQ

  • Space complexity: \(O(n)\), for the result

Code


    fun reorganizeString(s: String): String = buildString {
      val freq = IntArray(128)
      s.forEach { freq[it.toInt()]++ }
      val pq = PriorityQueue<Int>(compareBy({ -freq[it] }))
      for (i in 0..127) if (freq[i] > 0) pq.add(i)
      while (pq.isNotEmpty()) {
        var ind = pq.poll()
        if (isNotEmpty() && get(0).toInt() == ind) {
          if (pq.isEmpty()) return ""
          ind = pq.poll().also { pq.add(ind) }
        }
        insert(0, ind.toChar())
        if (--freq[ind] > 0) pq.add(ind)
      }
    }

22.08.2023

168. Excel Sheet Column Title easy blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/316

Problem TLDR

Excel col number to letter-number 1 -> A, 28 -> AB

Intuition

Just arithmetic conversion of number to string with radix of 26 instead of 10. Remainder from division by 26 gives the last letter. Then the number must be divided by 26.

Approach

  • use a StringBuilder
  • number must be n-1

Complexity

  • Time complexity: \(O(log(n))\), logarithm by radix of 26

  • Space complexity: \(O(log(n))\)

Code


    fun convertToTitle(columnNumber: Int): String = buildString {
      var n = columnNumber
      while (n > 0) {
        insert(0, ((n - 1) % 26 + 'A'.toInt()).toChar())
        n = (n - 1) / 26
      }
    }

21.08.2023

459. Repeated Substring Pattern easy blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/315

Intuition

Consider example, abc abc abc. Doing shift left 3 times we get the same string:

abcabcabc - original
bcabcabca - shift left by 1
cabcabcab - shift left by 1
abcabcabc - shift left by 1

Now, there is a technique called Rolling Hash: let’s calculate the hash like this: hash = x + 31 * hash. After full string hash calculated, we start doing shifts:

    // abcd
    // a
    // 32^0 * b + 32^1 * a
    // 32^0 * c + 32^1 * b + 32^2 * a
    // 32^0 * d + 32^1 * c + 32^2 * b + 32^3 * a
    // bcda
    // 32^0 * a + 32^1 * d + 32^2 * c + 32^3 * b = 32*(abcd-32^3a) +a=32abcd-(32^4-1)a

Observing this math equation, next rolling hash is shiftHash = 31 * shiftHash - 31^len + c

Approach

  • careful to not shift by whole length

Complexity

  • Time complexity: \(O(n)\), at most 2 full scans, and hashing gives O(1) time

  • Space complexity: \(O(1)\)

Code


    fun repeatedSubstringPattern(s: String): Boolean {
      var hash = 0L
      for (c in s) hash = c.toInt() + 31L * hash
      var pow = 1L
      repeat(s.length) { pow *= 31L }
      pow--
      var shiftHash = hash
      return (0 until s.lastIndex).any { i ->
        shiftHash = 31L * shiftHash - pow * s[i].toInt()
        shiftHash == hash && 
          s == s.substring(0, i + 1).let { it.repeat(s.length / it.length) }
      }
    }

20.08.2023

1203. Sort Items by Groups Respecting Dependencies hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/314

Problem TLDR

Sort items by groups and in groups given dependencies.

Intuition

Use hint.

We can split items by groups and check groups dependencies. Next, do Topological Sort for groups and then do Topological Sort for items in each group.

Approach

Now, the tricks:

  • if we consider each -1 as a separate group, code will become cleaner
  • we don’t have to do separate Topological Sort for each group, just sort whole graph of items, then filter by each group
  • cycle detection can be done in a Topological Sort: if there is a cycle, there is no item with indegree == 0
  • Topological Sort function can be reused

Complexity

  • Time complexity: \(O(nm + E)\)

  • Space complexity: \(O(n + n + E)\)

Code


    class G(count: Int, val fromTo: MutableMap<Int, MutableSet<Int>> = mutableMapOf()) {
      operator fun get(k: Int) = fromTo.getOrPut(k) { mutableSetOf() }
      val order: List<Int> by lazy {
        val indegree = IntArray(count)
        fromTo.values.onEach { it.onEach { indegree[it]++ } }
        val queue = ArrayDeque<Int>(indegree.indices.filter { indegree[it] == 0 })
        generateSequence { queue.poll() }
            .onEach { fromTo[it]?.onEach { if (--indegree[it] == 0) queue += it } }
            .toList().takeIf { it.size == count } ?: listOf()
      }
    }
    fun sortItems(n: Int, m: Int, group: IntArray, beforeItems: List<List<Int>>): IntArray {
      var groupsCount = m
      for (i in 0 until n) if (group[i] == -1) group[i] = groupsCount++
      val items = G(n)
      val groups = G(groupsCount)
      for (to in beforeItems.indices)
        for (from in beforeItems[to])
          if (group[to] == group[from]) items[from] += to
          else groups[group[from]] += group[to]
      return groups.order.flatMap { g -> items.order.filter { group[it] == g } }.toIntArray()
    }

19.08.2023

1489. Find Critical and Pseudo-Critical Edges in Minimum Spanning Tree hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/313

Problem TLDR

List of list of must-have edges and list of optional edges for Minimum Weight Minimum Spanning Tree

Intuition

Use hints.

Minimum Spanning Tree can be obtained by sorting edges and adding not connected one-by-one using Union-Find

After we found target minimum weight, we can check how each node contributes: if removing the node increases the target, the node is a must-have. Also, if force using node in a spanning tree doesn’t change the target, node is optional.

Approach

  • careful with the sorted order of indices, returned positions must be in initial order
  • check if spanning tree is impossible to make, by checking if all nodes are connected

Complexity

  • Time complexity: \(O(E^2 + EV)\), sorting edges takes ElogE, then cycle E times algorithm of E+V

  • Space complexity: \(O(E + V)\), E for sorted edges, V for Union-Find array

Code


    fun findCriticalAndPseudoCriticalEdges(n: Int, edges: Array<IntArray>): List<List<Int>> {
      val sorted = edges.indices.sortedWith(compareBy({ edges[it][2] }))
      fun minSpanTreeW(included: Int = -1, excluded: Int = -1): Int {
        val uf = IntArray(n) { it }
        fun find(x: Int): Int = if (x == uf[x]) x else find(uf[x]).also { uf[x] = it }
        fun union(ind: Int): Int { 
          val (a, b, w) = edges[ind]
          return if (find(a) == find(b)) 0 else w.also { uf[find(b)] = find(a) }
        }
        return ((if (included < 0) 0 else union(included)) + sorted
          .filter { it != excluded }.map { union(it) }.sum()!!)
          .takeIf { (0 until n).all { find(0) == find(it) } } ?: Int.MAX_VALUE
      }
      val target = minSpanTreeW() 
      val critical = mutableListOf<Int>()
      val pseudo = mutableListOf<Int>()
      edges.indices.forEach {
        if (minSpanTreeW(excluded = it)  > target) critical += it
        else if (minSpanTreeW(included = it) == target) pseudo += it
      }
      return listOf(critical, pseudo)
    }

18.08.2023

1615. Maximal Network Rank medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/312

Problem TLDR

Max edges count for each pair of nodes

Intuition

We can just count edges for each node, then search for max in an n^2 for-loop.

Approach

  • use a HashSet to check contains in O(1)

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\), there are up to n^2 edges

Code


    fun maximalNetworkRank(n: Int, roads: Array<IntArray>): Int {
        val fromTo = mutableMapOf<Int, HashSet<Int>>()
        roads.forEach { (from, to) ->
          fromTo.getOrPut(from) { HashSet() } += to
          fromTo.getOrPut(to) { HashSet() } += from
        }
        var max = 0
        for (a in 0 until n) {
          for (b in a + 1 until n) {
            val countA = fromTo[a]?.size ?: 0
            val countB = fromTo[b]?.size ?: 0
            val direct = fromTo[a]?.contains(b) ?: false
            max = maxOf(max, countA + countB - (if (direct) 1 else 0))
          }
        }
        return max
    }

17.08.2023

542. 01 Matrix medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/311

Problem TLDR

Distances to 0 in an 0-1 matrix

Intuition

Depth-First search will not work, as the path to 0 must radiate to all directions.

We can start a Breadth-First Search waves from each 0. Each BFS step increases distance by 1.

Approach

  • use dir array for a simpler code
  • avoid rewriting the cells

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code

    fun updateMatrix(mat: Array<IntArray>): Array<IntArray> {
      val res = Array(mat.size) { IntArray(mat[0].size) { -1 } }
      val dir = listOf(-1 to 0, 0 to 1, 1 to 0, 0 to -1)
      with(ArrayDeque<Pair<Int, Int>>()) {
        for (y in 0..mat.lastIndex)
          for (x in 0..mat[0].lastIndex)
            if (mat[y][x] == 0) add(y to x)
      
        var dist = 0
        while (isNotEmpty()) {
          repeat(size) {
            val (y, x) = poll()
            if (res[y][x] == -1) {
              res[y][x] = dist
              for ((dx, dy) in dir) {
                val y1 = y + dy
                val x1 = x + dx
                if (y1 in 0..mat.lastIndex && x1 in 0..mat[0].lastIndex) 
                  add(y1 to x1)
              }
            }
          }
          dist++
        }
      }
      return res
    }

16.08.2023

239. Sliding Window Maximum medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/310

Problem TLDR

List of sliding window’s maximums

Intuition

To quickly find a maximum in a sliding window, consider example:

Input: nums = [1,3,-1,-3,5,3,6,7], k = 3
Window position                Max
---------------               -----
[#  3  -1]  _  _  _  _  _       3
 _ [3  -1  -3] _  _  _  _       3
 _  _ [ #   #  5] _  _  _       5
 _  _   _ [ #  5  3] _  _       5
 _  _   _   _ [#  #  6] _       6
 _  _   _   _  _ [#  #  7]      7

After each new maximum appends to the end of the window, they become the maximum until the window slides it out, so all lesser positions to the left of it are irrelevant.

Approach

We can use a decreasing Stack technique to remove all the smaller elements. However, to maintain a window size, we’ll need a Queue.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun maxSlidingWindow(nums: IntArray, k: Int): IntArray = with(ArrayDeque<Int>()) {
        val res = mutableListOf<Int>()
        nums.forEachIndexed { i, x ->
          while (isNotEmpty() && nums[peekLast()] < x) removeLast()
          add(i)
          while (isNotEmpty() && i - peekFirst() + 1 > k) removeFirst()
          if (i >= k - 1) res += nums[peekFirst()]
        }
        return res.toIntArray()
    }

15.08.2023

86. Partition List medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/309

Problem TLDR

Partition a Linked List by x value

Intuition

Keep two nodes for less and for more than x, and add to them, iterating over the list. Finally, concatenate more to less.

Approach

  • To avoid cycles, make sure to set each next to null
  • Use dummy head technique

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code



    fun partition(head: ListNode?, x: Int): ListNode? {
        val dummyLess = ListNode(0)
        val dummyMore = ListNode(0)
        var curr = head
        var less = dummyLess
        var more = dummyMore
        while (curr != null) {
          if (curr.`val` < x) {
            less.next = curr
            less = curr
          } else {
            more.next = curr
            more = curr
          }
          val next = curr.next
          curr.next = null
          curr = next
        }
        less.next = dummyMore.next
        return dummyLess.next
    }

14.08.2023

215. Kth Largest Element in an Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/308

Problem TLDR

Kth largest in an array

Intuition

There is a known Quckselect algorithm:

  • do a partition, get the pivot
  • if pivot is less than target, repeat on the left side
  • otherwise, repeat on the right side of the pivot

To do a partition:

  • make a growing buffer on the left
  • choose the pivot value which to compare all the elements
  • if nums[i] < pivot, put and grow the buffer
  • finally, put pivot to the end of the buffer
  • the buffer size now is a pivot position in a sorted array, as all elements to the left a less than it, and to the right are greater

Approach

For divide-and-conquer loop:

  • do the last check from == to
  • always move the border exclusive from = pi + 1, to = pi - 1

Complexity

  • Time complexity: \(O(n) -> O(n^2)\), the worst case is n^2

  • Space complexity: \((O(1))\), but array is modified

Code


    fun findKthLargest(nums: IntArray, k: Int): Int {
      var from = 0
      var to = nums.lastIndex
      fun swap(a: Int, b: Int) { nums[a] = nums[b].also { nums[b] = nums[a] } }
      val target = nums.size - k
      while (from <= to) {
        var pi = from
        var pivot = nums[to]
        for (i in from until to) if (nums[i] < pivot) swap(i, pi++)
        swap(to, pi)
        
        if (pi == target) return nums[pi]
        if (pi < target) from = pi + 1
        if (pi > target) to = pi - 1
      }
      return -1
    }

13.08.2023

2369. Check if There is a Valid Partition For The Array medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/307

Problem TLDR

Is it possible to partition an array of 2 or 3 equal nums or 3 increasing nums.

Intuition

Hint: don’t spend much time trying to write a greedy solution.

We can consider every suffix of an array and make it a subproblem. Given it depends on only of the starting position, it can be safely cached.

Approach

  • use Depth-First search and a HashMap for cache by position

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun validPartition(nums: IntArray): Boolean {
      val cache = mutableMapOf<Int, Boolean>()
      fun dfs(pos: Int): Boolean = cache.getOrPut(pos) {
        if (pos == nums.size) true
        else if (pos + 1 > nums.lastIndex) false
        else {
          val diff1 = nums[pos + 1] - nums[pos]
          if (diff1 == 0 && dfs(pos + 2)) true
          else if (pos + 2 > nums.lastIndex) false
          else {
            val diff2 = nums[pos + 2] - nums[pos + 1]
            (diff1 == 0 && diff2 == 0 || diff1 == 1 && diff2 == 1) && dfs(pos + 3)
          }
        }
      }
      return dfs(0)
    }

12.08.2023

63. Unique Paths II medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/306

Problem TLDR

Number of right-down ways tl->br in a matrix with obstacles

Intuition

Each time the robot moves in one direction gives a separate path. If two directions are possible, the number of paths gets added.

For example,

r r  #  0
r 2r 2r 2r
0 #  2r 4r

On the first row, the single path goes up to 1. On the second row, direct path down added to direct path right. On the third row, the same happens when top and left numbers of paths are not 0.

Approach

Use a separate row array to remember previous row paths counts.

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(nm)\)

Code



    fun uniquePathsWithObstacles(obstacleGrid: Array<IntArray>): Int {
      val row = IntArray(obstacleGrid[0].size)
      row[0] = 1
      for (r in obstacleGrid) 
        for (x in r.indices)
          if (r[x] != 0) row[x] = 0 
          else if (x > 0) row[x] += row[x - 1]
      return row.last()
    }

The Magical Rundown


In Emojia's forgotten 🌌 corner, where time doesn't merely flow—it waltzes 💃, 
spinning tales of lost yesterdays 🕰️ and unborn tomorrows ⌛, stands the 
whispered legend of the Time Labyrinth. Not merely walls and corridors, but 
a tapestry of fate's myriad choices, echoing distant memories and futures yet 
conceived.

Bolt, the lonely automaton 🤖, not born but dreamt into existence by starlight ✨ 
and cosmic whimsy, felt an inexplicable yearning towards the 🏁 - the Time Nexus. 
Ancient breezes 🍃 carried murmurs, not of it being an end, but a kaleidoscope 
🎨 gateway to every pulse and flutter ❤️ of chronology's capricious dance 🌊.

╔═══╤═══╤═══╤═══╗
║🤖 │ 0 │🚫 │ 0 ║
╟───┼───┼───┼───╢
║ 0 │ 0 │ 0 │ 0 ║
╟───┼───┼───┼───╢
║ 0 │🚫 │ 0 │🏁 ║
╚═══╧═══╧═══╧═══╝

With each step, the fabric of reality quivered. Shadows of histories 🎶, 
cosmic echoes 🌍, diverged and converged, painting and erasing moments of 
what was, is, and could be.

---

Standing before the 🚫, it wasn't a barrier for Bolt, but a silent riddle: 
"What song of the cosmos 🎵 shall you hum today, wanderer?"

╔═══╤═══╤═══╤═══╗
║🤖 │ ➡️ │🚫 │ 0 ║
╟───┼───┼───┼───╢
║ 0 │ 0 │ 0 │ 0 ║
╟───┼───┼───┼───╢
║ 0 │ 🚫 │ 0 │🏁 ║
╚═══╧═══╧═══╧═══╝

Dreamlike avenues 🛤️ unfurled, painting multitudes of futures in the vivid 
colors of a universe in spring. In this chronal dance, Bolt secretly hoped 
to outrace its own echoes, to be the first at the Nexus.

---

Junctions whispered with the delicate hum 🎵 of countless Bolts, each a tale, 
a fate, a fleeting note in the grand cosmic symphony.

╔═══╤═══╤═══╤═══╗
║🤖 │ ➡️ │🚫 │ 0 ║
╟───┼───┼───┼───╢
║⬇️ │ 2➡️│2➡️│2➡️║
╟───┼───┼───┼───╢
║ 0 │ 🚫 │ 0 │🏁 ║
╚═══╧═══╧═══╧═══╝

Yet, as the Time Nexus loomed, revealing its vast enigma, a sense of profound 
disquiet engulfed Bolt. Not only had another reflection reached before, but a 
sea of mirrored selves stared back.

╔═══╤═══╤═══╤═══╗
║🤖 │ ➡️ │🚫 │ 0 ║
╟───┼───┼───┼───╢
║⬇️ │ 2➡️│2➡️│2➡️║
╟───┼───┼───┼───╢
║⬇️ │ 🚫 │2⬇️│4🏁║
╚═══╧═══╧═══╧═══╝

In that echoing vastness, Bolt's singular hope was smothered. In the dance of 
time, amidst countless reflections, it whispered a silent, desperate question: 
Which tune, which cadence, which moment 🎶 was truly its own in this timeless 
waltz?

11.08.2023

518. Coin Change II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/305

Problem TLDR

Ways to make amount with array of coins

Intuition

This is a classical Dynamic Programming problem: the result is only depending on inputs – coins subarray and the amount, so can be cached.

In a Depth-First search manner, consider possibilities of taking a coin and skipping to the next.

Approach

  • HashMap gives TLE, but an Array cache will pass

Complexity

  • Time complexity: \(O(nm)\)

  • Space complexity: \(O(nm)\)

Code


    fun change(amount: Int, coins: IntArray): Int {
      val cache = Array(coins.size) { IntArray(amount + 1) { -1 } }
      fun dfs(curr: Int, left: Int): Int = if (left == 0) 1
        else if (left < 0 || curr == coins.size) 0
        else cache[curr][left].takeIf { it >= 0 } ?: {
          dfs(curr, left - coins[curr]) + dfs(curr + 1, left)
        }().also { cache[curr][left] = it }
      return dfs(0, amount)
    }

10.08.2023

81. Search in Rotated Sorted Array II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/304

Problem TLDR

Binary Search in a rotated array with duplicates

Intuition

There are several cases:

  • pivot on the left, right side can be checked
  • pivot on the right, left side can be checked
  • nums[lo] == nums[hi], do a linear scan

Approach

For more robust code:

  • inclusive lo and hi
  • last check lo == hi
  • check the result nums[mid] == target
  • move borders lo = mid + 1, hi = mid - 1
  • exclusive checks < & > are simpler to reason about than inclusive <=, =>

Complexity

  • Time complexity: \(O(n)\), the worst case is linear in a long array of duplicates

  • Space complexity: \(O(1)\)

Code


    fun search(nums: IntArray, target: Int): Boolean {
        var lo = 0
        var hi = nums.lastIndex
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          if (nums[mid] == target) return true
          if (nums[lo] < nums[hi]) { // normal case
            if (nums[mid] < target) lo = mid + 1 else hi = mid - 1
          } else if (nums[lo] > nums[hi]) { // pivot case
            if (nums[mid] > nums[hi]) {
              // pivot on the right
              // 5 6 7 8 9 1 2
              //   t   m   p
              if (target in nums[lo]..nums[mid]) hi = mid - 1 else lo = mid + 1
            } else {
              // pivot on the left
              //   9 1 2 3 4  
              //     p m t
              if (target in nums[mid]..nums[hi]) lo = mid + 1 else hi = mid - 1
            }
          } else hi-- // nums[lo] == nums[hi]
        }
        return false
    }

09.08.2023

2616. Minimize the Maximum Difference of Pairs medium blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/303

Problem TLDR

Minimum of maximums possible p diffs of distinct array positions

Intuition

The hint is misleading, given the problem size 10^5 DP approach will give TLE, as it is n^2.

The real hint is:

  • given the difference diff, how many pairs there are in an array, where pair_diff <= diff?
  • if we increase the picked diff will that number grow or shrink?

Using this hint, we can solve the problem with Binary Search, as with growth of diff, there is a flip of when we can take p numbers and when we can’t.

When counting the diffs, we use Greedy approach, and take the first possible, skipping its sibling. This will work, because we’re answering the questions of how many rather than maximum/minimum.

Approach

For more robust Binary Search, use:

  • inclusive lo, hi
  • last condition lo == hi
  • result: if (count >= p) res = minOf(res, mid)
  • move border lo = mid + 1, hi = mid - 1

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


    fun minimizeMax(nums: IntArray, p: Int): Int {
        nums.sort()
        var lo = 0
        var hi = nums.last() - nums.first()
        var res = hi
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          var i = 1
          var count = 0
          while (i < nums.size) if (nums[i] - nums[i - 1] <= mid) {
            i += 2
            count++
          } else i++
          if (count >= p) res = minOf(res, mid)
          if (count >= p) hi = mid - 1 else lo = mid + 1
        }
        return res
    }

08.08.2023

33. Search in Rotated Sorted Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/302

Problem TLDR

Binary Search in a shifted array

Intuition

The special case is when lo > hi, otherwise it is a Binary Search.

Then there are two cases:

  • if lo < mid - monotonic part is on the left
  • lo >= mid - monotonic part is on the right

Check the monotonic part immediately, otherwise go to the other part.

Approach

For more robust code:

  • inclusive lo and hi
  • check for target target == nums[mid]
  • move lo = mid + 1, hi = mid - 1
  • the last case lo == hi

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(log(n))\)

Code


    fun search(nums: IntArray, target: Int): Int {
      var lo = 0 
      var hi = nums.lastIndex
      while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (target == nums[mid]) return mid
        if (nums[lo] > nums[hi]) {
          if (nums[lo] > nums[mid]) {
            if (target < nums[mid] || target > nums[hi]) hi = mid - 1 else lo = mid + 1
          } else {
            if (target > nums[mid] || target < nums[lo]) lo = mid + 1 else hi = mid - 1
          }
        } else if (target < nums[mid]) hi = mid - 1 else lo = mid + 1
      }
      return -1
    }

07.08.2023

74. Search a 2D Matrix medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/301

Problem TLDR

2D Binary Search

Intuition

Just a Binary Search

Approach

For more robust code:

  • inclusive lo and hi
  • the last condition lo == hi
  • move borders lo = mid + 1, hi = mid - 1
  • check the result
  • use built-in functions

Complexity

  • Time complexity: \(O(log(n*m))\)

  • Space complexity: \(O(1)\)

Code



    fun searchMatrix(matrix: Array<IntArray>, target: Int): Boolean {
        var lo = 0
        var hi = matrix.lastIndex
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          val row = matrix[mid]
          if (target in row.first()..row.last()) 
            return row.binarySearch(target) >= 0
          if (target < row.first()) hi = mid - 1 else lo = mid + 1
        }
        return false
    }

06.08.2023

920. Number of Music Playlists hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/300

Problem TLDR

Playlists number playing n songs goal times, repeating each once in a k times

Intuition

We can search through the problem space, taking each new song with the given rules: song can be repeated only after another k song got played. When we have the goal songs, check if all distinct songs are played.

We can cache the solution by curr and used map, but that will give TLE.

The hard trick here is that the result only depends on how many distinct songs are played.

Approach

Use DFS and memo.

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun numMusicPlaylists(n: Int, goal: Int, k: Int): Int {
        val cache = mutableMapOf<Pair<Int, Int>, Long>()
        fun dfs(curr: Int, used: Map<Int, Int>): Long = cache.getOrPut(curr to used.size) {
          if (curr > goal) { 
            if ((1..n).all { used.contains(it) }) 1L else 0L 
          } else (1..n).asSequence().map { i -> 
              if (curr <= used[i] ?: 0) 0L else
                dfs(curr + 1, used.toMutableMap().apply { this[i] = curr + k })
            }.sum()!! % 1_000_000_007L
        }
        return dfs(1, mapOf()).toInt()
    }

05.08.2023

95. Unique Binary Search Trees II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/299

Problem TLDR

All possible Binary Search Trees for 1..n numbers

Intuition

One way to build all possible BST is to insert numbers in all possible ways. We can do this with a simple backtracking, given the small n <= 8. To remove duplicates, we can print the tree and use it as a hash key.

Approach

  • use a bit mask and a Stack for backtracking

Complexity

  • Time complexity:

\(O(n!* nlog(n))\), as the recursion depth is n, each time iterations go as n * (n - 1) * (n - 2) * … * 2 * 1, which is equal to n!. The final step of inserting elements is nlog(n), and building a hash is n, which is < nlogn, so not relevant.

  • Space complexity:

\(O(n!)\), is a number of permutations

Code



    fun insert(x: Int, t: TreeNode?): TreeNode = t?.apply {
        if (x > `val`) right = insert(x, right)
        else left = insert(x, left)
      } ?: TreeNode(x)
    fun print(t: TreeNode): String = 
      "[${t.`val`} ${t.left?.let { print(it) }} ${t.right?.let { print(it) }}]"
    fun generateTrees(n: Int): List<TreeNode?> {
      val stack = Stack<Int>()
      val lists = mutableListOf<TreeNode>()
      fun dfs(m: Int): Unit = if (m == 0) 
          lists += TreeNode(stack[0]).apply { for (i in 1 until n) insert(stack[i], this) }
        else for (i in 0 until n) if (m and (1 shl i) != 0) {
          stack.push(i + 1)
          dfs(m xor (1 shl i))
          stack.pop()
        }
      dfs((1 shl n) - 1)
      return lists.distinctBy { print(it) }
    }

Another divide-and-conquer solution, that I didn’t think of image.pngAnother divide-and-conquer solution, that I didn’t think of image.png

04.08.2023

139. Word Break medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/298

Problem TLDR

If a word is a wordDict concatenation

Intuition

To quickly find out if a sequence, we can use Trie. Then, we can search with DFS any possible split. As the result only depends on the argument, we can safely cache it.

Approach

Write a Trie and DFS, no tricks here.

Complexity

  • Time complexity: \(O(wn)\), w—is words count in s

  • Space complexity: \(O(w + 26^l)\), l—is the longest word in a dict

Code

    class Trie(var isWord: Boolean = false) { val next = mutableMapOf<Char, Trie>() }
    fun wordBreak(s: String, wordDict: List<String>): Boolean {
        val root = Trie()
        wordDict.forEach { 
          var t = root
          it.forEach { t = t.next.getOrPut(it) { Trie() } }
          t.isWord = true
        }
        val cache = mutableMapOf<Int, Boolean>()
        fun dfs(pos: Int): Boolean = pos == s.length || cache.getOrPut(pos) {
          var t: Trie? = root
          s.withIndex().asSequence().drop(pos).takeWhile { t != null }
          .any { (i, c) ->
            t = t?.next?.get(c)
            t?.isWord == true && dfs(i + 1)
          }
        }
        return dfs(0)
    }

03.08.2023

17. Letter Combinations of a Phone Number medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/297

Problem TLDR

Possible words from phone keyboard

Intuition

Just a naive DFS and Backtraking will solve the problem, as the number is short

Approach

  • pay attention to keys in keyboard, some have size of 4

Complexity

  • Time complexity: \(O(n4^n)\), recursion depth is n, each time we iterate over ‘3’ or ‘4’ letters, for example:
12 ->
abc def
a   d
a    e
a     f
 b  d
 b   e
 b    f
  c d
  c  e
  c   f

Each new number multiply previous count by 3 or 4. The final joinToString gives another n multiplier.

  • Space complexity: \(O(4^n)\)

Code


    fun letterCombinations(digits: String): List<String> = mutableListOf<String>().apply {
      val abc = arrayOf("abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz")
      val list = Stack<Char>()
      fun dfs(pos: Int) {
        if (list.size == digits.length) {
          if (list.isNotEmpty()) add(list.joinToString("")) 
        } else abc[digits[pos].toInt() - '2'.toInt()].forEach { 
          list.push(it)
          dfs(pos + 1)
          list.pop()
        }
      }
      dfs(0)
    }

02.08.2023

46. Permutations medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/296

Problem TLDR

List of all numbers permutations

Intuition

As the total count of number is small, we can just brute force the solution. We can use DFS and a backtracking technique: add number to the list pre-order then remove it post-order.

Approach

Iterate over all numbers and choose every number not in a bit mask

Complexity

  • Time complexity: \(O(n * n!)\), as we go n * (n - 1) * (n - 2) * .. * 2 * 1

  • Space complexity: \((n!)\)

Code


    fun permute(nums: IntArray): List<List<Int>> = mutableListOf<List<Int>>().apply {
      val list = mutableListOf<Int>()
      fun dfs(mask: Int): Unit = if (list.size == nums.size) this += list.toList()
        else nums.forEachIndexed { i, n ->
          if (mask and (1 shl i) == 0) {
            list += n
            dfs(mask or (1 shl i))
            list.removeAt(list.lastIndex)
          }
        }
      dfs(0)
    }

01.08.2023

77. Combinations medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/295

Problem TLDR

All combinations choosing k numbers from 1..n numbers

Intuition

As total number is 20, we can use bit mask to generate all possible 2^n bit masks, then choose only k 1-bits masks and generate lists.

Approach

Let’s write a Kotlin one-liner

Complexity

  • Time complexity: \(O(n2^n)\)

  • Space complexity: \(O(n2^n)\)

Code


    fun combine(n: Int, k: Int): List<List<Int>> = (0 until (1 shl n))
      .filter { Integer.bitCount(it) == k }
      .map { mask -> (1..n).filter { mask and (1 shl it - 1) != 0 } }

31.07.2023

712. Minimum ASCII Delete Sum for Two Strings medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/292

Problem TLDR

Minimum removed chars sum to make strings equal

Intuition

This is a known Dynamic Programming problem about the minimum edit distance. We can walk both strings and at each time choose what char to take and what to skip. The result is dependent only from the arguments, so can be cached.

Approach

Let’s use DFS and memo.

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun minimumDeleteSum(s1: String, s2: String): Int {
      val cache = mutableMapOf<Pair<Int, Int>, Int>()
      fun dfs(p1: Int, p2: Int): Int = cache.getOrPut(p1 to p2) { when {
        p1 == s1.length && p2 == s2.length -> 0
        p1 == s1.length -> s2.drop(p2).map { it.toInt() }.sum()!!
        p2 == s2.length -> s1.drop(p1).map { it.toInt() }.sum()!!
        s1[p1] == s2[p2] -> dfs(p1 + 1, p2 + 1)
        else -> minOf(s1[p1].toInt() + dfs(p1 + 1, p2), s2[p2].toInt() + dfs(p1, p2 + 1))
      } }
      return dfs(0, 0)
    }

30.07.2023

664. Strange Printer hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/291

Problem TLDR

Minimum continuous overrides by the same character to make a string

Intuition

The main idea comes to mind when you consider some palindromes as example:


abcccba

When we consider the next character ccc + b, we know, that the optimal number of repaints is Nc + 1. Or, bccc + b, the optimal is 1 + Nc.

However, the Dynamic Programming formula for finding a palindrome didn’t solve this case: ababa, as clearly, the middle a can be written in a single path aaaaa.

Another idea, is to split the string: ab + aba. Number for ab = 2, and for aba = 2. But, as first == last, we paint a only one time, so dp[from][to] = dp[from][a] + dp[a + 1][to].

As we didn’t know if our split is the optimal one, we must consider all of them.

Approach

  • let’s write bottom up DP

Complexity

  • Time complexity: \(O(n^3)\)

  • Space complexity: \(O(n^2)\)

Code



    fun strangePrinter(s: String): Int = with(Array(s.length) { IntArray(s.length) }) {
      s.mapIndexed { to, sto ->
        (to downTo 0).map { from -> when {
            to - from <= 1 -> if (s[from] == sto) 1 else 2
            s[from] == sto -> this[from + 1][to]
            else -> (from until to).map { this[from][it] + this[it + 1][to] }.min()!!
          }.also { this[from][to] = it }
        }.last()!!
      }.last()!!
    }

29.07.2023

808. Soup Servings medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/290

Problem TLDR

Probability of soup A drained first or both A and B with 0.5 multiplier.

Intuition

The formula in the examples gives us the correct way to calculate the probabilities: each time we make a choice with probability of 1/4. After we arrive with the final condition, we use multipliers 1.0 for A win, 0.5 for both A and B and 0.0 for B win.

This is a simple DFS + cache dynamic programming problem. However, this give TLE or OOM, as the N is too big.

At that point, the interview is over, and you safely can go home and see the answers in leetcode.com website.

To solve TLE & OOM, we must observe all the possible answers:

  val ans = doubleArrayOf(
    0.50000, 0.62500, 0.62500, 0.65625, 0.71875, 0.74219, 0.75781, 0.78516, 0.79688, 0.81787, 
    0.82764, 0.84485, 0.85217, 0.86670, 0.87256, 0.88483, 0.88963, 0.90008, 0.90406, 0.91301, 
    0.91634, 0.92405, 0.92687, 0.93353, 0.93593, 0.94170, 0.94376, 0.94878, 0.95056, 0.95493, 
    0.95646, 0.96029, 0.96162, 0.96497, 0.96612, 0.96906, 0.97007, 0.97265, 0.97353, 0.97580, 
    0.97657, 0.97857, 0.97924, 0.98100, 0.98160, 0.98315, 0.98367, 0.98505, 0.98551, 0.98672, 
    0.98713, 0.98820, 0.98856, 0.98951, 0.98983, 0.99067, 0.99095, 0.99170, 0.99195, 0.99261, 
    0.99283, 0.99342, 0.99362, 0.99414, 0.99431, 0.99478, 0.99493, 0.99535, 0.99548, 0.99585, 
    0.99597, 0.99630, 0.99640, 0.99670, 0.99679, 0.99705, 0.99714, 0.99737, 0.99744, 0.99765, 
    0.99772, 0.99790, 0.99796, 0.99812, 0.99818, 0.99832, 0.99837, 0.99850, 0.99854, 0.99866, 
    0.99870, 0.99880, 0.99884, 0.99893, 0.99896, 0.99904, 0.99907, 0.99914, 0.99917, 0.99923, 
    0.99925, 0.99931, 0.99933, 0.99939, 0.99940, 0.99945, 0.99947, 0.99951, 0.99952, 0.99956, 
    0.99957, 0.99961, 0.99962, 0.99965, 0.99966, 0.99968, 0.99969, 0.99972, 0.99972, 0.99975, 
    0.99975, 0.99977, 0.99978, 0.99980, 0.99980, 0.99982, 0.99982, 0.99984, 0.99984, 0.99985, 
    0.99986, 0.99987, 0.99987, 0.99988, 0.99989, 0.99989, 0.99990, 0.99991, 0.99991, 0.99991, 
    0.99992, 0.99992, 0.99993, 0.99993, 0.99993, 0.99994, 0.99994, 0.99994, 0.99995, 0.99995, 
    0.99995, 0.99996, 0.99996, 0.99996, 0.99996, 0.99996, 0.99997, 0.99997, 0.99997, 0.99997, 
    0.99997, 0.99997, 0.99997, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 
    0.99998, 0.99998, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 
    0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 
    0.99999, 0.99999, 0.99999, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000
  )

Basically, after a certain point, there is no new kind of answer.

Approach

As to solve this problem we must observe all the answers, a lookup table as a valid choice for the solution.

Complexity

  • Time complexity: \(O(1)\)

  • Space complexity: \(O(1)\)

Code


  val ans = doubleArrayOf(
    0.50000, 0.62500, 0.62500, 0.65625, 0.71875, 0.74219, 0.75781, 0.78516, 0.79688, 0.81787, 
    0.82764, 0.84485, 0.85217, 0.86670, 0.87256, 0.88483, 0.88963, 0.90008, 0.90406, 0.91301, 
    0.91634, 0.92405, 0.92687, 0.93353, 0.93593, 0.94170, 0.94376, 0.94878, 0.95056, 0.95493, 
    0.95646, 0.96029, 0.96162, 0.96497, 0.96612, 0.96906, 0.97007, 0.97265, 0.97353, 0.97580, 
    0.97657, 0.97857, 0.97924, 0.98100, 0.98160, 0.98315, 0.98367, 0.98505, 0.98551, 0.98672, 
    0.98713, 0.98820, 0.98856, 0.98951, 0.98983, 0.99067, 0.99095, 0.99170, 0.99195, 0.99261, 
    0.99283, 0.99342, 0.99362, 0.99414, 0.99431, 0.99478, 0.99493, 0.99535, 0.99548, 0.99585, 
    0.99597, 0.99630, 0.99640, 0.99670, 0.99679, 0.99705, 0.99714, 0.99737, 0.99744, 0.99765, 
    0.99772, 0.99790, 0.99796, 0.99812, 0.99818, 0.99832, 0.99837, 0.99850, 0.99854, 0.99866, 
    0.99870, 0.99880, 0.99884, 0.99893, 0.99896, 0.99904, 0.99907, 0.99914, 0.99917, 0.99923, 
    0.99925, 0.99931, 0.99933, 0.99939, 0.99940, 0.99945, 0.99947, 0.99951, 0.99952, 0.99956, 
    0.99957, 0.99961, 0.99962, 0.99965, 0.99966, 0.99968, 0.99969, 0.99972, 0.99972, 0.99975, 
    0.99975, 0.99977, 0.99978, 0.99980, 0.99980, 0.99982, 0.99982, 0.99984, 0.99984, 0.99985, 
    0.99986, 0.99987, 0.99987, 0.99988, 0.99989, 0.99989, 0.99990, 0.99991, 0.99991, 0.99991, 
    0.99992, 0.99992, 0.99993, 0.99993, 0.99993, 0.99994, 0.99994, 0.99994, 0.99995, 0.99995, 
    0.99995, 0.99996, 0.99996, 0.99996, 0.99996, 0.99996, 0.99997, 0.99997, 0.99997, 0.99997, 
    0.99997, 0.99997, 0.99997, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 0.99998, 
    0.99998, 0.99998, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 
    0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 0.99999, 
    0.99999, 0.99999, 0.99999, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000, 1.00000
  )
  fun soupServings(n: Int): Double = if (n >= 199 * 25) 1.0 else ans[Math.ceil(n / 25.0).toInt()]
  /*
    if (n > 4000) return 1.0
    val cache = mutableMapOf<Pair<Int, Int>, Double>()
    fun dfs(a: Int, b: Int): Double = cache.getOrPut(a to b) {
      if (a <= 0 && b <= 0) return 0.5
      if (a > 0 && b <= 0) return 0.0
      if (a <= 0 && b > 0) return 1.0
      (dfs(a - 100, b) +
      dfs(a - 75, b - 25) +
      dfs(a - 50, b - 50) +
      dfs(a - 25, b - 75)) * 0.25
    }
    return dfs(n, n)
  */

28.07.2023

486. Predict the Winner medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/289

Problem TLDR

Optimally taking numbers from an array's ends can one player win another

Intuition

The optimal strategy for the current player will be to search the maximum score of total sum - optimal another. The result can be cached as it only depends on the input array.

Approach

Write the DFS and cache by lo and hi.

  • use Long to avoid overflow

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    fun PredictTheWinner(nums: IntArray): Boolean {
      val cache = Array(nums.size) { LongArray(nums.size) { -1L } }
      fun dfs(lo: Int, hi: Int, currSum: Long): Long = cache[lo][hi].takeIf { it >= 0 } ?: {
        if (lo == hi) nums[lo].toLong()
        else if (lo > hi) 0L
        else currSum - minOf(
          dfs(lo + 1, hi, currSum - nums[lo]),
          dfs(lo, hi - 1, currSum - nums[hi]) 
        )
      }().also { cache[lo][hi] = it }
      val sum = nums.asSequence().map { it.toLong() }.sum()!!
      return dfs(0, nums.lastIndex, sum).let { it >= sum - it }
    }

27.07.2023

2141. Maximum Running Time of N Computers hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/288

Problem TLDR

Maximum time to use n batteries in parallel

Hint 1

Batteries 5 5 5 is equal to 1 2 3 4 5 to run 3 computers for 5 minutes.

Hint 2

Batteries are swapped instantly, so we can drain all 1 2 3 4 5 with just 3 computers, but if a pack is 1 2 3 4 100 we can only drain 5 from the last 100 battery. (or less)

Hint 3

Energy of 5 5 5 is 15 to run for 5 minutes. Energy in 1 2 3 4 100 is 1+2+3+4+5 when run for 5 minutes. Energy in 1 2 3 4 100 is 1+2+3+4+4 when run for 4 minutes. Energy in 1 2 3 4 100 is 1+2+3+3+3 when run for 3 minutes.

Intuition

The Binary Search idea is first to mind, as with growth of run time the function of canRun do the flip.

However, to detect if we canRun the given time is not so trivial.

We can use all batteries by swapping them every minute. To use 5 batteries in 3 computers, we can first use the max capacity and change others:


1 2 3 4 5
    1 1 1
    1 1 1
    1 1 1
  1   1 1
1 1     1

In this example, time = 5. Or we can have just 3 batteries with capacity of 5 each: 5 5 5. What if we add another battery:


1 2 3 4 5 9
      1 1 1
      1 1 1
      1 1 1
      1 1 1
    1   1 1
  1 1     1
  1 1     1

Time becomes 7, or we can have 7 7 7 battery pack with total energy = 3 * 7 = 21. And we don’t use 1 yet.

Let’s observe the energy for the time = 7:


1 2 3 4 5 9
* 1 1 1 1 1
  1 1 1 1 1
    1 1 1 1
      1 1 1
        1 1
          1
          1

We didn’t use 1, but had we another 1 the total energy will be 21 + 1 + 1 + 1(from 9) or 24, which is equal to 3 * 8, or time = 8. So, by this diagram, we can take at most time power units from each battery. So, our function canRun(time) is: energy(time) >= time * n. Energy is a sum of all batteries running at most time.

Approach

Binary Search:

  • inclusive lo & hi
  • last check lo == hi
  • compute result res = mid
  • boundaries lo = mid + 1, hi = mid - 1

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code




    fun maxRunTime(n: Int, batteries: IntArray): Long {
        // n=3       1 2 3 4 5 6 7 9
        // time = 4
        // we need 4 4 4, take 1 2 3 4 4 4 4 4
        // time = 5
        // we need 5 5 5, take 1 2 3 4 5 5 5 5


        // n=3         3 3 3 80
        // time = 1    1 1 1 1      vs    1 1 1
        // time = 2    2 2 2 2      vs    2 2 2
        // time = 3    3 3 3 3      vs    3 3 3
        // time = 4    3 3 3 4 (13) vs    4 4 4 (16)
        // time = 5    3 3 3 5 (14) vs    5 5 5 (15)
        // time = 6    3 3 3 6 (15) vs    6 6 6 (18)
        var lo = 0L
        var hi = batteries.asSequence().map { it.toLong() }.sum() ?: 0L
        var res = 0L
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2L
          val canRun = n * mid <= batteries.asSequence().map { minOf(it.toLong(), mid) }.sum()!!
          if (canRun) {
            res = mid
            lo = mid + 1L
          } else hi = mid - 1L
        }
        return res
    }



26.07.2023

1870. Minimum Speed to Arrive on Time medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/287

Problem TLDR

Max speed for all dist departing at round hours, be fit in hour

Intuition

Given the speed, we can calculate the travel time in O(n). With decreasing speed the time grows, so we can do the Binary Search

Approach

For more robust Binary Search code:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always move the borders lo = mid + 1, hi = mid - 1
  • always save the result res = mid

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


    fun minSpeedOnTime(dist: IntArray, hour: Double): Int {
        var lo = 1
        var hi = 1_000_000_000
        var res = -1
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          var dt = 0.0
          val time = dist.fold(0.0) { r, t -> 
            r + Math.ceil(dt).also { dt = t / mid.toDouble() } 
          } + dt
          if (hour >= time) {
            res = mid
            hi = mid - 1
          } else lo = mid + 1
        }
        return res
    }

25.07.2023

852. Peak Index in a Mountain Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/286

Problem TLDR

Mountain pattern index in the array in log time

Intuition

Do the Binary Search of the biggest growing index

Approach

For more robust Binary Search code:

  • use inclusive lo and hi
  • do the last check lo == hi
  • always write the result ind = mid if conditions are met
  • always move the borders lo = mid - 1, hi = mid + 1

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(1)\)

Code

    fun peakIndexInMountainArray(arr: IntArray): Int {
        var lo = 1
        var hi = arr.lastIndex
        var ind = -1
        while (lo <= hi) {
          val mid = lo + (hi - lo) / 2
          if (arr[mid] > arr[mid - 1]) {
            ind = mid
            lo = mid + 1
          } else hi = mid - 1
        }
        return ind
    }

Magical Rundown

🌄 "Look at that crimson blush, Alpha!" A radiant sunrise anoints the 
towering Everest, its snow-capped peaks aglow with the day's first 
light. An ethereal landscape, a symphony of shadows and silhouettes, 
lays the stage for an impending adventure. 🏔️

Team Alpha 🥾 chuckles, their voices swallowed by the wind, "Today's 
the day we've been dreaming of, Charlie!" Team Charlie 🦅, encased 
in their mountain gear, share their excitement. Their eyes, reflecting 
the sunlit peaks, are fixated on the summit – their celestial goal.

Base Camps (BC):   
0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20
Everest Heights:  
1K  2K  3K  4K  5K  6K  7K  8K  9K 10K 11K 12K 13K 14K 15K 16K 15K 14K 13K 12K 11K
    🥾(Team Alpha)                  🏔️(Mysterious Mid Point)                    🦅(Team Charlie)

🧭 "We're off to conquer the Everest!" Alpha's voice reverberates 
with a hopeful intensity. Their strategy, an intricate dance with 
numbers and ambition – Binary Search. The mountain, its snow-capped 
peaks reaching for the skies, hums ancient tales to their eager ears.

BC:        0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20
Heights:  1K  2K  3K  4K  5K  6K  7K  8K  9K 10K 11K 12K 13K 14K 15K 16K 15K 14K 13K 12K 11K
                                                 🥾🏁(Team Alpha's Milestone)            🦅(Team Charlie)

"10K! Feels like we've captured a bit of heaven," Team Alpha shares 
their awe, their voices a mere whisper against the grandeur of the 
landscape. 

🏞️ But the mountain, a grand enigma, hides her secrets well...

With a sudden, heart-stopping rumble, the mountain shivers, the 
seemingly innocuous snow beneath Team Charlie's feet giving way. A 
fierce avalanche sweeps down the slopes, sending Charlie scrambling 
for cover.

BC:        0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20
Heights:  1K  2K  3K  4K  5K  6K  7K  8K  9K 10K 11K 12K 13K 14K 15K 16K 15K 14K 13K 12K 11K
                                                 🥾🏁(Team Alpha's Resolve)              ❄️🦅(Team Charlie's Setback)

"Avalanche!" Charlie's voice, choked with frosty fear, crackles over 
the radio. Yet, Team Alpha, undeterred by the wrath of the mountain, 
pushes forward. "Hold tight, Charlie! We're stardust-bound!" They 
continue their daring ascent, breaching the cloudline to a dizzying 
16K.

🚩 "Charlie, we're among the stars!" Alpha's voice, filled with 
joyous triumph, echoes through the radio. The peak is conquered.

BC:   0   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20
Heights:  1K  2K  3K  4K  5K  6K  7K  8K  9K 10K 11K 12K 13K 14K 15K 16K 15K 14K 13K 12K 11K
                                                                🚩🎉(Victorious Summit at BC 15!)

As the sun bathes the snowy peaks in a golden hue, Team Alpha plants 
their triumphant flag at the top. They stand there, at the roof of 
the world, their hearts swelling with joy and pride. It's the journey, 
the shared aspirations, the dream of reaching for the stars, that truly 
defines their adventure. 🌠

24.07.2023

50. Pow(x, n) medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/285

Problem TLDR

x^n

Intuition

We can use tabulations technique: compute all powers of 2 and reuse them.

      // 2          1
      // 2*2 = 4    2
      // 4*4 = 16   4
      // 16*16=256  8
      // 2^8 * 2^8 = 2^16   16
      // 2^31 = 2^16 * 2^4 * 2

After computing the growing part, we need to find the optimal way to split the reminder. For example, x^31 = x^16 * x^5, then x^5 = x^4 * x^1. To find the closest power of 2, we can take the most significant bit, which is an x & -x bit operation.

        // 5 -> 4   101 -> 100
        // 7 -> 4   111 -> 100
        // 9 -> 8  1001 -> 1000

Approach

  • there is a corner case of the negative powers, just invert x -> 1/x
  • careful with Int.MIN_VALUE, as abs(MIN_VALUE) == abs(-MIN_VALUE)

Complexity

  • Time complexity: \(O(log(n))\)

  • Space complexity: \(O(1)\)

Code



    fun myPow(x: Double, n: Int): Double {
      if (n == 0) return 1.0
      val mul = if (n < 0) 1 / x else x
      val exp = if (n == Int.MIN_VALUE) Int.MAX_VALUE else Math.abs(n)

      val cache = DoubleArray(32)
      var k = mul
      var f = 1
      cache[0] = k
      while (f <= exp / 2) {
        k = k * k
        f = f * 2
        cache[Integer.numberOfTrailingZeros(f)] = k
      }
      while (f < exp) {
        val e = exp - f    

        val pow = e and -e
        k = k * cache[Integer.numberOfTrailingZeros(pow)]
        f = f + pow
      }
      if (n == Int.MIN_VALUE) k = k * mul
      return k
    }

23.07.2023

894. All Possible Full Binary Trees medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/284

Problem TLDR

All possible Full Binary Trees with n nodes, each have both children

Intuition

First, if count of nodes is even, BFT is not possible.

Let’s observe how the Trees are growing:

image.png

There are n / 2 rounds of adding a new pair of nodes to each leaf of each Tree in the latest generation.

Some duplicate trees occur, so we need to calculate a hash.

Approach

Let’s implement it in a BFS manner.

  • to avoid collision of the hash, add some symbols to indicate a level [...]

Complexity

  • Time complexity: \(O(n^4 2^n)\), n generations, queue size grows in 2^n manner, count of leafs grows by 1 each generation, so it’s x + (x + 1) + .. + (x + n), giving n^2, another n for collection leafs, and another for hash and clone

  • Space complexity: \(O(n^3 2^n)\)

Code


    fun clone(curr: TreeNode): TreeNode = TreeNode(0).apply {
      curr.left?.let { left = clone(it) }
      curr.right?.let { right = clone(it) }
    }
    fun hash(curr: TreeNode): String = 
      "[${curr.`val`} ${ curr.left?.let { hash(it) } } ${ curr.right?.let { hash(it) } }]"
    fun collectLeafs(curr: TreeNode): List<TreeNode> =
      if (curr.left == null && curr.right == null) listOf(curr)
      else collectLeafs(curr.left!!) + collectLeafs(curr.right!!) 
    fun allPossibleFBT(n: Int): List<TreeNode?> = if (n % 2 == 0) listOf() else
      with (ArrayDeque<TreeNode>().apply { add(TreeNode(0)) }) {
        val added = HashSet<String>()
        repeat (n / 2) { rep ->
          repeat(size) {
            val root = poll()
            collectLeafs(root).forEach {
              it.left = TreeNode(0)
              it.right = TreeNode(0)
              if (added.add(hash(root))) add(clone(root))
              it.left = null
              it.right = null
            }
          }
        }
        toList()
      }

image.png

effective solution. It can be described as “for every N generate every possible split of [0..i] [i+1..N]”. Subtrees are also made of all possible combinations.

22.07.2023

688. Knight Probability in Chessboard medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/283

Problem TLDR

Probability of making k steps on a chessboard without stepping outside

Intuition

The description example doesn’t give a clear picture of how the probability works.

image.png

  • individual probability is 1/8 each time we make a step.
    • One step is 1/8, two steps are 1/8 * 1/8 and so on.
    • So, the k-steps path will have probability of 1/8^k
  • we need to sum all the probabilities of individual k-steps paths, that will remain on a board
    • the brute force algorithm for this will be BFS:
      • for k rounds:
        • poll all the elements, and make possible steps on the board
        • resulting probability will be queue.size / 8^k, as queue will contain only the final possible ways after k steps

However, there are too many possible ways, we will quickly run out of memory.

It is noticeable, some ways are repeating, and after s steps the same cell [x, y] produces the same amount of possible ways dp[x, y][s]. We can cache this result for each cell.

However, the number of ways are still very big and do not fit into Long 64 bits. To solve this, we can cache not only the ways, but the probability, dividing each step by 8.

Approach

  • storing the directions in a sequence helps to reduce some LOC

Complexity

  • Time complexity: \(O(kn^2)\)

  • Space complexity: \(O(kn^2)\)

Code



    val dxdy = sequenceOf(-2 to 1, -2 to -1, 2 to 1, 2 to -1, -1 to 2, -1 to -2, 1 to 2, 1 to -2)
    fun knightProbability(n: Int, k: Int, row: Int, column: Int): Double {
      val cache = mutableMapOf<Pair<Pair<Int, Int>, Int>, Double>()
      fun count(x: Int, y: Int, k: Int): Double = if (k == 0) 1.0 else cache.getOrPut(x to y to k) {
          dxdy.map { (dx, dy) -> x + dx to y + dy }
          .map { (x, y) -> if (x in 0..n-1 && y in 0..n-1) count(x, y, k - 1) / 8.0 else 0.0 }
          .sum()
      }
      return count(column, row, k)
    }

The magical rundown

Step ₀ - The High Noon Duel 🤠🎵🌵:
🎶 The town clock strikes twelve, and the high noon chess duel commences. A 
lone knight 🐎 trots onto the scorching, sun-bleached chessboard, casting a long 
shadow on the sandy squares.

╔═══🌵═══🌵═══╗
║ 🐎 ║   ║    ║
╠═══🌵═══🌵═══╣
║   ║    ║   ║
╠═══🌵═══🌵═══╣
║   ║    ║   ║
╚═══🌵═══🌵═══╝

The Sheriff 🤠, ever the statistician, watches keenly. "For now, the odds are 
all in your favor, Knight," he says, unveiling the initial probability 𝓹₀ = 1.
┌─────💰────┬────💰────┬────💰────┐
│     1     │    0    │     0    │
├─────💰────┼────💰────┼────💰────┤
│     0     │    0    │     0    │
├─────💰────┼────💰────┼────💰────┤
│     0     │    0    │     0    │
└─────💰────┴────💰────┴────💰────┘

Step ₁ - The Dusty Trail 🌄🎵🐴:
🎶 The knight 🐎 leaps into action, stirring up a cloud of dust. He lands in two 
different squares, each with a calculated 1/8 chance. The Sheriff 🤠 nods 
approvingly. "Bold moves, Knight. The probability after this is 𝓹₁ = 1/8 + 1/8 = 1/4."
╔═══🌵═══🌵═══╗
║   ║    ║   ║
╠═══🌵═══🌵═══╣
║   ║    ║ 🐎 ║
╠═══🌵═══🌵═══╣
║   ║ 🐎 ║   ║
╚═══🌵═══🌵═══╝

He reveals the new odds:
┌─────💰────┬────💰────┬────💰────┐
│     0     │    0    │     0    │
├─────💰────┼────💰────┼────💰────┤
│     0     │    0    │    ¹/₈   │
├─────💰────┼────💰────┼────💰────┤
│     0     │   ¹/₈   │     0    │
└─────💰────┴────💰────┴────💰────┘

Step ₂ - The Sun-Baked Crossroads ☀️🎵🌪️:
🎶 The knight 🐎 continues his daring maneuvers, hopping onto a few critical 
spots. He lands on three squares, with probabilities of 1/64, 1/64, and 2/64. 
Adding these up, the Sheriff 🤠 declares, "The stakes have risen, Knight. The 
total is 𝓹₂ = 1/64 + 1/64 + 2/64 = 1/16."
╔═══🌵═══🌵═══╗
║🐎🐎║   ║ 🐎 ║
╠═══🌵═══🌵═══╣
║   ║    ║   ║
╠═══🌵═══🌵═══╣
║ 🐎 ║   ║   ║
╚═══🌵═══🌵═══╝

The updated odds take shape:
┌─────💰────┬────💰────┬────💰────┐
│    ²/₆₄   │    0    │   ¹/₆₄   │
├─────💰────┼────💰────┼────💰────┤
│     0     │    0    │     0    │
├─────💰────┼────💰────┼────💰────┤
│    ¹/₆₄   │    0    │     0    │
└─────💰────┴────💰────┴────💰────┘

Step ₃ - The Outlaw's Hideout 🏚️🎵🐍:
🎶 As the sun sets, the knight 🐎 lands in a few hidden spots with various 
probabilities. Each calculated leap adds to his total: 1/512 + 1/512 + 3/512 + 3/512. 
The Sheriff 🤠 raises an eyebrow. "Well played, Knight. Your total now is 𝓹₃ = 
1/512 + 1/512 + 3/512 + 3/512."

╔═══🌵═══🌵═══╗
║   ║ 🐎 ║    ║
╠═══🌵═══🌵═══╣
║ 🐎 ║   ║🐎🐎🐎║
╠═══🌵═══🌵═══╣
║   ║🐎🐎🐎║  ║
╚═══🌵═══🌵═══╝

Beneath the twinkling stars, the Sheriff 🤠 surveys the evolving game. "You're 
not an easy one to beat, Knight," he admits, revealing the updated stakes:
┌─────💰────┬────💰────┬────💰────┐
│     0     │  ¹/₅₁₂  │     0    │
├─────💰────┼────💰────┼────💰────┤
│   ¹/₅₁₂   │    0    │   ³/₅₁₂  │
├─────💰────┼────💰────┼────💰────┤
│     0     │  ³/₅₁₂  │     0    │
└─────💰────┴────💰────┴────💰────┘

🎶 So, under the twinkling stars and to the tune of the whistling wind, our 
knight's adventure continues into the night. The stakes are high, the moves 
unpredictable, but one thing's certain: this wild chess duel is far from over! 🌵🐎🌌🎵

21.07.2023

673. Number of Longest Increasing Subsequence medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/282

Proble TLDR

Count of LIS in an array

Intuition

To find Longest Increasing Subsequence, there is a known algorithm with \(O(nlog(n))\) time complexity. However, it can help with this case:


3 5 4 7

when we must track both 3 4 7 and 3 5 7 sequences. Given that, we can try to do full search with DFS, taking or skipping a number. To cache some results, we must make dfs depend on only the input arguments. Let’s define it to return both max length of LIS and count of them in one result, and arguments are the starting position in an array and previous number that we must start sequence from.

Approach

  • use an array cache, as Map gives TLE

Complexity

  • Time complexity: \(O(n^2)\)

  • Space complexity: \(O(n^2)\)

Code


    class R(val maxLen: Int, val cnt: Int)
    fun findNumberOfLIS(nums: IntArray): Int {
      val cache = Array(nums.size + 1) { Array<R>(nums.size + 2) { R(0, 0) } }
      fun dfs(pos: Int, prevPos: Int): R = if (pos == nums.size) R(0, 1) else 
        cache[pos][prevPos].takeIf { it.cnt != 0 }?: {
          val prev = if (prevPos == nums.size) Int.MIN_VALUE else nums[prevPos]
          var cnt = 0
          while (pos + cnt < nums.size && nums[pos + cnt] == nums[pos]) cnt++
          val skip = dfs(pos + cnt, prevPos)
          if (nums[pos] <= prev) skip else {
            val start = dfs(pos + cnt, pos).let { R(1 + it.maxLen, cnt * it.cnt ) }
            if (skip.maxLen == start.maxLen) R(skip.maxLen, start.cnt + skip.cnt)
            else if (skip.maxLen > start.maxLen) skip else start
          }
        }().also { cache[pos][prevPos] = it }
      return dfs(0, nums.size).cnt
    }

Magical rundown

🏰🔮🌌 The Astral Enigma of Eternity
In the boundless tapestry of time, an enigmatic labyrinth 🗝️ whispers
tales of forgotten epochs. Your fateful quest? To decipher the longest
increasing subsequences hidden within the celestial array 🧩 [3, 5, 4, 7].

🌄 The Aurora Gateway: dfs(0, nums.size)
    /                          \
🌳 The Verdant Passage (dfs(1,0)) / 🌑 The Nebulous Veil (dfs(1,nums.size))

Your odyssey commences at twilight's brink: will you tread the lush
🌳 Verdant Passage or dare to penetrate the enigmatic 🌑 Nebulous Veil?

🌄 The Aurora Gateway: dfs(0, nums.size)
   /   
🍃 The Glade of Whispers (Pos 1: num[1]=3, dfs(1,0))  
   /  
🌊 The Cascade of Echoes (Pos 2: num[2]=5, dfs(2,1))  
   / 
⛰️ The Bastion of Silence (Pos 3: num[3]=4, dfs(3,2)) 🚫🔒

The labyrinth’s heart pulsates with cryptic riddles. The ⛰️ Bastion of Silence
remains locked, overshadowed by the formidable 🌊 Cascade of Echoes.

🌄 The Aurora Gateway: dfs(0, nums.size)
   /   
🍃 The Glade of Whispers (Pos 1: num[1]=3, dfs(1,0))  
   \   
🌑 The Phantom of Riddles (Pos 2: num[2]=5, dfs(2,0)) 

Retracing your footsteps, echoes of untaken paths whisper secrets. Could
the ⛰️ Bastion of Silence hide beneath the enigma of the 🌑 Phantom of Riddles?

🌄 The Aurora Gateway: dfs(0, nums.size)
   /   
🍃 The Glade of Whispers (Pos 1: num[1]=3, dfs(1,0))  
   \   
💨 The Mist of Mystery (Pos 3: num[3]=4, dfs(3,0))
   \
🌩️ The Tempest of Triumph (Pos 4: num[4]=7, dfs(4,3)) 🏁🎉

At last, the tempest yields! Each twist and turn, each riddle spun and
secret learned, illuminates a longest increasing subsequence in the cosmic array.

Your enchanted grimoire 📜✨ (cache) now vibrates with the wisdom of ages:

prevPos\pos  0     1      2      3     4
       0     (0,0) (2,1) (2,1)  (3,2) (0,0)
       1     (0,0) (0,0) (2,1)  (3,2) (0,0)
       2     (0,0) (0,0) (0,0)  (2,1) (0,0)
       3     (0,0) (0,0) (0,0)  (0,0) (0,0)
       4     (0,0) (0,0) (0,0)  (0,0) (0,0)

Beneath the shimmering cosmic symphony, you cast the final incantation
🧙‍♂️ dfs(0, nums.size).cnt. The grimoire blazes with ethereal light, revealing
the total count of longest increasing subsequences.

You emerge from the labyrinth transformed: no longer merely an adventurer,
but the 🌟 Cosmic Guardian of Timeless Wisdom. 🗝️✨🌠

20.07.2023

735. Asteroid Collision medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/281

Problem TLDR

Result after asteroids collide left-right exploding by size: 15 5 -15 -5 5 -> -15 -5 5

Intuition

Let’s add positive asteroids to the Stack. When negative met, it can fly over all smaller positive added, and can explode if larger met.

Approach

Kotlin’s API helping reduce some LOC

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


    fun asteroidCollision(asteroids: IntArray): IntArray = with(Stack<Int>()) {
        asteroids.forEach { sz ->
          if (!generateSequence { if (sz > 0 || isEmpty() || peek() < 0) null else peek() }
            .any {
              if (it <= -sz) pop()
              it >= -sz
            }) add(sz)
        }
        toIntArray()
    }

19.07.2023

435. Non-overlapping Intervals medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/280

Problem TLDR

Minimum intervals to erase overlap

Intuition

First idea, is to sort the array by from. Next, we can greedily take intervals and remove overlapping ones. But, to remove the minimum number, we can start with removing the most long intervals.

Approach

  • walk the sweep line, counting how many intervals are non overlapping
  • only move the right border when there is a new non overlapping interval
  • minimize the border when it shrinks

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(1)\)

Code


    fun eraseOverlapIntervals(intervals: Array<IntArray>): Int {
        intervals.sortWith(compareBy({ it[0] }))
        var border = Int.MIN_VALUE
        return intervals.count { (from, to) -> 
          (border > from).also {
            if (border <= from || border > to) border = to
          }
        }
    }

18.07.2023

146. LRU Cache medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/279

Intuition

We can use Doubly-Linked List representing access time in its order.

Approach

  • use firstNode and lastNode

Complexity

  • Time complexity: \(O(1)\), for each call get or put

  • Space complexity: \(O(1)\), for each element

Code


class LRUCache(val capacity: Int) {
    class Node(val key: Int, var left: Node? = null, var right: Node? = null)
    var size = 0
    val map = mutableMapOf<Int, Int>()
    val firstNode = Node(-1)
    var lastNode = firstNode
    val keyToNode = mutableMapOf<Int, Node>()

    fun disconnect(node: Node) {
      val leftNode = node.left
      val rightNode = node.right
      node.left = null
      node.right = null
      leftNode?.right = rightNode
      rightNode?.left = leftNode
      if (node === lastNode) lastNode = leftNode!!
    }

    fun updateNode(key: Int) {
      val node = keyToNode[key]!!
      if (node === lastNode) return
      disconnect(node)
      lastNode.right = node
      node.left = lastNode
      lastNode = node
    }

    fun get(key: Int): Int = map[key]?.also { updateNode(key) } ?: -1

    fun put(key: Int, value: Int) {
      if (!map.contains(key)) {
        if (size == capacity) {
          firstNode.right?.let {
            map.remove(it.key)
            keyToNode.remove(it.key)
            disconnect(it)
          }
        } else size++
        keyToNode[key] = Node(key)
      }
      updateNode(key)
      map[key] = value
    }

}

17.07.2023

445. Add Two Numbers II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/278

Problem TLDR

Linked List of sum of two Linked Lists numbers, 9->9 + 1 = 1->0->0

Intuition

The hint is in the description: reverse lists, then just do arithmetic. Another way is to use stack.

Approach

  • don’t forget to undo the reverse

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


    fun addTwoNumbers(l1: ListNode?, l2: ListNode?, n: Int = 0): ListNode? {
      fun ListNode?.reverse(): ListNode? {
        var curr = this
        var prev: ListNode? = null
        while (curr != null) {
          val next = curr.next
          curr.next = prev
          prev = curr
          curr = next
        }
        return prev
      }
      var l1r = l1.reverse()
      var l2r = l2.reverse()
      var o = 0
      var prev: ListNode? = null
      while (l1r != null || l2r != null) {
        val v = o + (l1r?.`val` ?: 0) + (l2r?.`val` ?: 0)
        prev = ListNode(v % 10).apply { next = prev }
        o = v / 10
        l1r = l1r?.next
        l2r = l2r?.next
      }
      if (o > 0) prev = ListNode(o).apply { next = prev }
      l1r.reverse()
      l2r.reverse()
      return prev
    }

16.07.2023

1125. Smallest Sufficient Team hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/277

Problem TLDR

Smallest team from people with skills, having all required skills

Intuition

The skills set size is less than 32, so we can compute a bitmask for each of people and for the required skills. Next, our task is to choose a set from people that result skills mask will be equal to the required. We can do a full search, each time skipping or adding one mask from the people.
Observing the problem, we can see, that result is only depending on the current mask and all the remaining people. So, we can cache it.

Approach

  • we can use a HashMap to store skill to index, but given a small set of skills, just do indexOf in O(60 * 16)
  • add to the team in post order, as dfs must return only the result depending on the input arguments

Complexity

  • Time complexity: \(O(p2^s)\), as full mask bits are 2^s, s - skills, p - people

  • Space complexity: \(O(p2^s)\)

Code


    fun smallestSufficientTeam(skills: Array<String>, people: List<List<String>>): IntArray {
        val peoplesMask = people.map {  it.fold(0) { r, t -> r or (1 shl skills.indexOf(t)) } }
        val cache = mutableMapOf<Pair<Int, Int>, List<Int>>()
        fun dfs(curr: Int, mask: Int): List<Int> =
          if (mask == (1 shl skills.size) - 1) listOf()
          else if (curr == people.size) people.indices.toList()
          else cache.getOrPut(curr to mask) {
            val skip = dfs(curr + 1, mask)
            val take = dfs(curr + 1, mask or peoplesMask[curr]) + curr
            if (skip.size < take.size) skip else take
          }
        return dfs(0, 0).toIntArray()
    }

15.07.2023

1751. Maximum Number of Events That Can Be Attended II hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/276

Problem TLDR

Max sum of at most k values from non-intersecting array of (from, to, value) items

Intuition

Let’s observe example:

        // 0123456789011
        // [     4 ]
        // [1][2][3][2]
        //      [4][2]

If k=1 we choose [4] if k=2 we choose [4][2] if k=3 we choose [2][3][2]

What will not work:
  • sweep line algorithm, as it is greedy, but there is an only k items we must choose and we must do backtracking
  • adding to Priority Queue and popping the lowest values: same problem, we must backtrack
What will work:
  • asking for a hint: this is what I used
  • full search: at every index we can pick or skip the element
  • sorting: it will help to reduce irrelevant combinations by doing a Binary Search for the next non-intersecting element

We can observe, that at any given position the result only depends on the suffix array. That means we can safely cache the result by the current position.

Approach

For more robust Binary Search code:

  • use inclusive lo, hi
  • check the last condition lo == hi
  • always write the result next = mid
  • always move the borders lo = mid + 1, hi = mid - 1

Complexity

  • Time complexity: \(O(nklog(n))\)

  • Space complexity: \(O(nk)\)

Code


    fun maxValue(events: Array<IntArray>, k: Int): Int {
        // 0123456789011
        // [     4 ]
        // [1][2][3][2]
        //      [4][2]
        val inds = events.indices.sortedWith(compareBy({ events[it][0] }))
        // my ideas: 
        // sort - good
        // sweep line ? - wrong
        // priority queue ? - wrong
        // binary search ? 1..k - wrong
        // used hints:
        // hint: curr + next vs drop  dp?
        // hint: binary search next
        val cache = mutableMapOf<Pair<Int, Int>, Int>()
        fun dfs(curr: Int, canTake: Int): Int {
          return if (curr ==  inds.size || canTake == 0) 0
          else cache.getOrPut(curr to canTake) {
            val (_, to, value) = events[inds[curr]]
            var next = inds.size
            var lo = curr + 1
            var hi = inds.lastIndex
            while (lo <= hi) {
              val mid = lo + (hi - lo) / 2
              val (nextFrom, _, _) = events[inds[mid]]
              if (nextFrom > to) {
                next = mid
                hi = mid - 1
              } else lo = mid + 1
            }
            maxOf(value + dfs(next, canTake - 1), dfs(curr + 1, canTake))
          }
        }
        return dfs(0, k)
    }

14.07.2023

1218. Longest Arithmetic Subsequence of Given Difference medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/275

Problem TLDR

Longest arithmetic difference subsequence

Intuition

Store the next value and the length for it.

Approach

We can use a HashMap

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


fun longestSubsequence(arr: IntArray, difference: Int): Int = 
with(mutableMapOf<Int, Int>()) {
    arr.asSequence().map { x ->
        (1 + (this[x] ?: 0)).also { this[x + difference] = it } 
    }.max()!!
}

13.07.2023

207. Course Schedule medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/274

Problem TLDR

If none edges in a cycle

Intuition

To detect cycle, we can use DFS and two sets cycle and safe. Or use Topological Sort and check that all elements are visited.

Approach

Let’s use Topological Sort with Breadth-First Search.

  • build indegree - number of input nodes for each node
  • add to BFS only nodes with indegree[node] == 0
  • decrease indegree as it visited

Complexity

  • Time complexity: \(O(VE)\)

  • Space complexity: \(O(E + V)\)

Code


fun canFinish(numCourses: Int, prerequisites: Array<IntArray>): Boolean {
    val fromTo = mutableMapOf<Int, MutableSet<Int>>()
        val indegree = IntArray(numCourses)
        prerequisites.forEach { (to, from) ->
            fromTo.getOrPut(from) { mutableSetOf() } += to
            indegree[to]++
        }
        return with(ArrayDeque<Int>()) {
            addAll((0 until numCourses).filter { indegree[it] == 0 })
            generateSequence { if (isEmpty()) null else poll() }.map {
                fromTo[it]?.forEach {
                    if (--indegree[it] == 0) add(it)
                }
            }.count() == numCourses
        }
    }

12.07.2023

802. Find Eventual Safe States medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/273

Problem TLDR

List of nodes not in cycles

Intuition

Simple Depth-First Search will give optimal \(O(n)\) solution. When handling the visited set, we must separate those in cycle and safe.

Approach

  • we can remove from cycle set and add to safe set in a post-order traversal

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


fun eventualSafeNodes(graph: Array<IntArray>): List<Int> {
    val cycle = mutableSetOf<Int>()
        val safe = mutableSetOf<Int>()
            fun cycle(curr: Int): Boolean {
                return if (safe.contains(curr)) false else !cycle.add(curr)
                || graph[curr].any { cycle(it) }
                .also {
                    if (!it) {
                        cycle.remove(curr)
                        safe.add(curr)
                    }
                }
            }
            return graph.indices.filter { !cycle(it) }
        }

12.07.2023

863. All Nodes Distance K in Binary Tree medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/272

Problem TLDR

List of k distanced from target nodes in a Binary Tree

Intuition

There is a one-pass DFS solution, but it feels like too much of a corner cases and result handholding. A more robust way is to traverse with DFS and connect children nodes to parent, then send a wave from target at k steps.

Approach

Let’s build an undirected graph and do BFS.

  • don’t forget a visited HashSet

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


fun distanceK(root: TreeNode?, target: TreeNode?, k: Int): List<Int> {
    val fromTo = mutableMapOf<Int, MutableList<Int>>()
        fun dfs(node: TreeNode?, parent: TreeNode?) {
            node?.run {
                parent?.let {
                    fromTo.getOrPut(`val`) { mutableListOf() } += it.`val`
                    fromTo.getOrPut(it.`val`) { mutableListOf() } += `val`
                }
                dfs(left, this)
                dfs(right, this)
            }
        }
        dfs(root, null)
        return LinkedList<Int>().apply {
            val visited = HashSet<Int>()
                target?.run {
                    add(`val`)
                    visited.add(`val`)
                }
                repeat(k) {
                    repeat(size) {
                        fromTo.remove(poll())?.forEach { if (visited.add(it)) add(it) }
                    }
                }
            }
        }

11.07.2023

111. Minimum Depth of Binary Tree easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/271

Problem TLDR

Count nodes in the shortest path from root to leaf

Intuition

  • remember to count nodes, not edges
  • leaf is a node without children
  • use BFS or DFS

Approach

Let’s use BFS

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


fun minDepth(root: TreeNode?): Int = with(ArrayDeque<TreeNode>()) {
    root?.let { add(it) }
    generateSequence(1) { (it + 1).takeIf { isNotEmpty() } }
    .firstOrNull {
        (1..size).any {
            with(poll()) {
                left?.let { add(it) }
                right?.let { add(it) }
                left == null && right == null
            }
        }
    } ?: 0
}

10.07.2023

2272. Substring With Largest Variance hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/270

Problem TLDR

Max diff between count s[i] and count s[j] in all substrings of s

Intuition

The first idea is to simplify the task by considering only two chars, iterating over all alphabet combinations. Second idea is how to solve this problem for binary string in \(O(n)\): abaabbbabbb. We split this problem: find the largest subarray for a with the smallest count of b, and reverse the problem – largest b with smallest a. For this issue, there is a Kadane’s algorithm for maximizing sum: take values greedily and reset count when sum < 0. Important customization is to always consider countB at least 1 as it must be present in a subarray.

Approach

  • we can use Set of only the chars in s
  • iterate in ab and ba pairs
  • Kotlin API helps save some LOC

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), or O(1) if asSequence used

Code


fun largestVariance(s: String): Int = s.toSet()
.let { ss -> ss.map { a -> ss.filter { it != a }.map { a to it } }.flatten() }
.map { (a, b) ->
    var countA = 0
    var countB = 0
    s.filter { it == a || it == b }
    .map { c ->
        if (c == a) countA++ else countB++
        if (countA < countB) {
            countA = 0
            countB = 0
        }
        countA - maxOf(1, countB)
    }.max() ?: 0
}.max() ?: 0

9.07.2023

2551. Put Marbles in Bags hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/269

Problem TLDR

abs(max - min), where max and min are the sum of k interval borders

Intuition

Let’s observe some examples:


// 1 3 2 3 5 4 5 7 6
// *   * *
// 1+3 2+2 3+6 = 4+4+9 = 17
// * * *
// 1+1 3+3 2+6 = 2+6+8 = 16
// *             * * = 1+5 7+7 6+6
// 1 9 1 9 1 9 1 9 1    k = 3
// *   *           *    s = 1+9+1+9+1+1
// * *   *              s = 1+1+9+1+9+1
// 1 1 9 9 1 1 9 9 1    k = 3
// * *       *          s = 1+1+1+1+1+1
// *     *       *      s = 1+9+9+9+9+1
// 1 1 1 9 1 9 9 9 1    k = 3
// * * *                s = 1+1+1+1+1+1
// *         . * *      s = 1+9+9+9+9+1
// 1 4 2 5 2            k = 3
// . * . *              1+1+4+2+5+2
//   . * *              1+4+2+2+5+2
// . *   . *            1+1+4+5+2+2

One thing to note, we must choose k-1 border pairs i-1, i with min or max sum.

Approach

Let’s use PriorityQueue.

Complexity

  • Time complexity: \(O(nlog(k))\)

  • Space complexity: \(O(k)\)

Code


fun putMarbles(weights: IntArray, k: Int): Long {

    val pqMax = PriorityQueue<Int>(compareBy( { weights[it].toLong() + weights[it - 1].toLong() } ))
        val pqMin = PriorityQueue<Int>(compareByDescending( { weights[it].toLong() + weights[it - 1].toLong() } ))
            for (i in 1..weights.lastIndex) {
                pqMax.add(i)
                if (pqMax.size > k - 1) pqMax.poll()
                pqMin.add(i)
                if (pqMin.size > k - 1) pqMin.poll()
            }
            return Math.abs(pqMax.map { weights[it].toLong() + weights[it - 1].toLong() }.sum()!! -
            pqMin.map { weights[it].toLong() + weights[it - 1].toLong() }.sum()!!)
        }

7.07.2023

2024. Maximize the Confusion of an Exam medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/268

Problem TLDR

Max same letter subarray replacing k letters

Intuition

An important example is ftftftft k=3: we must fill all the intervals. It also tells, after each filling up we must decrease k. Let’s count T and F. Sliding window is valid when tt <= k || ff <= k.

Approach

We can save some lines using Kotlin collections API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\), or \(O(1)\) using asSequence

Code


fun maxConsecutiveAnswers(answerKey: String, k: Int): Int {
    var tt = 0
    var ff = 0
    var lo = 0
    return answerKey.mapIndexed { i, c ->
        if (c == 'T') tt++ else ff++
        while (tt > k && ff > k && lo < i)
        if (answerKey[lo++] == 'T') tt-- else ff--
        i - lo + 1
    }.max() ?: 0
}

6.07.2023

209. Minimum Size Subarray Sum medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/267

Problem TLDR

Min length subarray with sum >= target

Intuition

Use two pointers: one adding to sum and another subtracting. As all numbers are positive, then sum will always be increasing with adding a number and deceasing when subtracting.

Approach

Let’s use Kotlin Sequence API

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


fun minSubArrayLen(target: Int, nums: IntArray): Int {
    var lo = 0
    var sum = 0
    return nums.asSequence().mapIndexed { hi, n ->
        sum += n
        while (sum - nums[lo] >= target) sum -= nums[lo++]
        (hi - lo + 1).takeIf { sum >= target }
    }
    .filterNotNull()
    .min() ?: 0
}

5.07.2023

1493. Longest Subarray of 1’s After Deleting One Element medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/266

Problem TLDR

Largest 1..1 subarray after removing one item

Intuition

Let’s maintain two pointers for a start and a nextStart positions, and a third pointer for the right border.

  • move start to the nextStart when right == 0
  • move nextStart to start of 1’s

Approach

  • corner case is when all array is 1’s, as we must remove 1 then anyway

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\) add asSequence for it to become \(O(1)\)

Code


fun longestSubarray(nums: IntArray): Int {
    var start = -1
    var nextStart = -1
    return if (nums.sum() == nums.size) nums.size - 1
    else nums.mapIndexed { i, n ->
        if (n == 0) {
            start = nextStart
            nextStart = -1
            0
        } else {
            if (nextStart == -1) nextStart = i
            if (start == -1) start = nextStart
            i - start + (if (start == nextStart) 1 else 0)
        }
    }.max() ?:0
}

4.07.2023

137. Single Number II medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/265

Proble TLDR

Single number in an array of tripples

Intuition

One simple approach it to count bits at each position. Result will have a 1 when count % 3 != 0.

Approach

Let’s use fold.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

Code


fun singleNumber(nums: IntArray): Int =
//110
//110
//110
//001
//001
//001
//010
//010
//010
//100
//463
(0..31).fold(0) { res, bit ->
    res or ((nums.count { 0 != it and (1 shl bit) } % 3) shl bit)
}

3.07.2023

859. Buddy Strings easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/264

Problem TLDR

Is it just one swap s[i]<>s[j] to string s == string goal

Intuition

Compare two strings for each position. There are must be only two not equal positions and they must be mirrored pairs.

Approach

Let’s write it in Kotlin collections API style.

Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(n)\)

Code


fun buddyStrings(s: String, goal: String): Boolean = s.length == goal.length && (
s == goal && s.groupBy { it }.any { it.value.size > 1 } ||
s.zip(goal)
.filter { (a, b) -> a != b }
.windowed(2)
.map { (ab, cd) -> listOf(ab, cd.second to cd.first) }
.let { it.size == 1 && it[0][0] == it[0][1] }
)

2.07.2023

1601. Maximum Number of Achievable Transfer Requests hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/263

Problem TLDR

Max edges to make all counts in == out edges in graph

Intuition

Let’s observe some examples: image.png

All requests are valid if count of incoming edges are equal to outcoming. One possible solution is to just check each combination of edges.

Approach

Let’s use bitmask to traverse all combinations, as total number 16 can fit in Int

Complexity

  • Time complexity: \(O(n2^r)\)

  • Space complexity: \(O(n2^r)\)

Code


fun maximumRequests(n: Int, requests: Array<IntArray>): Int =
    (0..((1 shl requests.size) - 1)).filter { mask ->
        val fromTo = IntArray(n)
        requests.indices.filter { ((1 shl it) and mask) != 0 }.forEach {
            val (from, to) = requests[it]
            fromTo[from] -= 1
            fromTo[to] += 1
        }
        fromTo.all { it == 0 }
    }.map { Integer.bitCount(it) }.max()!!

1.07.2023

2305. Fair Distribution of Cookies medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/262

Problem TLDR

Min of the max distributing n cookies to k children

Intuition

Search all possible ways to give current cookie to one of the children. Backtrack sums and calculate the result.

Approach

Just DFS

Complexity

  • Time complexity: \(O(k^n)\)

  • Space complexity: \(O(2^n)\)

Code


fun distributeCookies(cookies: IntArray, k: Int): Int {
    fun dfs(pos: Int, children: IntArray): Int {
        if (pos == cookies.size) return if (children.contains(0)) -1 else children.max()!!
        var min = -1
        for (i in 0 until k) {
            children[i] += cookies[pos]
            val res = dfs(pos + 1, children)
            if (res != -1) min = if (min == -1) res else minOf(min, res)
            children[i] -= cookies[pos]
        }
        return min
    }
    return dfs(0, IntArray(k))
}

30.06.2023

1970. Last Day Where You Can Still Cross hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/261

Problem TLDR

Last day matrix connected top-bottom when flooded each day at cells[day]

Intuition

One possible solution is to do a Binary Search in a days space, however it gives TLE. Let’s invert the problem: find the first day from the end where there is a connection top-bottom. image.png Now, cells[day] is a new ground. We can use Union-Find to connect ground cells.

Approach

  • use sentinel cells for top and bottom
  • use path compressing uf[n] = x

Complexity

  • Time complexity: \(O(an)\), where a is a reverse Ackerman function

  • Space complexity: \(O(n)\)

Code


val uf = HashMap<Int, Int>()
fun root(x: Int): Int = if (uf[x] == null || uf[x] == x) x else root(uf[x]!!)
.also { uf[x] = it }
fun latestDayToCross(row: Int, col: Int, cells: Array<IntArray>) =
    cells.size - 1 - cells.reversed().indexOfFirst { (y, x) ->
        uf[y * col + x] = root(if (y == 1) 0 else if (y == row) 1 else y * col + x)
        sequenceOf(y to x - 1, y to x + 1, y - 1 to x, y + 1 to x)
        .filter { (y, x) -> y in 1..row && x in 1..col }
        .map { (y, x) -> y * col + x }
        .forEach { if (uf[it] != null) uf[root(y * col + x)] = root(it) }
        root(0) == root(1)
    }

29.06.2023

864. Shortest Path to Get All Keys hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/260

Problem TLDR

Min steps to collect all lowercase keys in matrix. # and uppercase locks are blockers.

Intuition

What will not work:

  • dynamic programming – gives TLE
  • DFS – as we can visit cells several times

For the shortest path, we can make a Breadth-First Search wave in a space of the current position and collected keys set.

Approach

  • Let’s use bit mask for collected keys set
  • all bits set are (1 << countKeys) - 1

Complexity

  • Time complexity: \(O(nm2^k)\)

  • Space complexity: \(O(nm2^k)\)

Code


val dir = arrayOf(0, 1, 0, -1)
data class Step(val y: Int, val x: Int, val keys: Int)
fun shortestPathAllKeys(grid: Array<String>): Int {
    val w = grid[0].length
    val s = (0..grid.size * w).first { '@' == grid[it / w][it % w] }
    val bit: (Char) -> Int = { 1 shl (it.toLowerCase().toInt() - 'a'.toInt()) }
    val visited = HashSet<Step>()
        val allKeys = (1 shl (grid.map { it.count { it.isLowerCase() } }.sum()!!)) - 1
        var steps = -1
        return with(ArrayDeque<Step>()) {
            add(Step(s / w, s % w, 0))
            while (isNotEmpty() && steps++ < grid.size * w) {
                repeat(size) {
                    val step = poll()
                    val (y, x, keys) = step
                    if (keys == allKeys) return steps - 1
                    if (x in 0 until w && y in 0..grid.lastIndex && visited.add(step)) {
                        val cell = grid[y][x]
                        if (cell != '#' && !(cell.isUpperCase() && 0 == (keys and bit(cell)))) {
                            val newKeys = if (cell.isLowerCase()) (keys or bit(cell)) else keys
                            var dx = -1
                            dir.forEach { dy ->  add(Step(y + dy, x + dx, newKeys)).also { dx = dy } }
                        }
                    }
                }
            }
            -1
        }
    }

28.06.2023

1514. Path with Maximum Probability medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/259

Problem TLDR

Max probability path from start to end in a probability edges graph

Intuition

What didn’t work:

  • naive BFS, DFS with visited set - will not work, as we need to visit some nodes several times
  • Floyd-Warshall - will solve this problem for every pair of nodes, but takes \(O(n^3)\) and gives TLE What will work: Dijkstra

    Approach

  • store probabilities from start to every node in an array
  • the stop condition will be when there is no any better path

Complexity

  • Time complexity: \(O(EV)\)

  • Space complexity: \(O(EV)\)

Code


fun maxProbability(n: Int, edges: Array<IntArray>, succProb: DoubleArray, start: Int, end: Int): Double {
    val pstart = Array(n) { 0.0 }
    val adj = mutableMapOf<Int, MutableList<Pair<Int, Double>>>()
    edges.forEachIndexed { i, (from, to) ->
        adj.getOrPut(from) { mutableListOf() } += to to succProb[i]
        adj.getOrPut(to) { mutableListOf() } += from to succProb[i]
    }
    with(ArrayDeque<Pair<Int, Double>>()) {
        add(start to 1.0)
        while(isNotEmpty()) {
            val (curr, p) = poll()
            if (p <= pstart[curr]) continue
            pstart[curr] = p
            adj[curr]?.forEach { (next, pnext) -> add(next to p * pnext) }
        }
    }

    return pstart[end]
}

27.06.2023

373. Find K Pairs with Smallest Sums medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/258

Problem TLDR

List of increasing sum pairs a[i], b[j] from two sorted lists a, b

Intuition

Naive solution with two pointers didn’t work, as we must backtrack to the previous pointers sometimes:


1 1 2
1 2 3

1+1 1+1 2+1 2+2(?) vs 1+2

The trick is to think of the pairs i,j as graph nodes, where the adjacent list is i+1,j and i, j+1. Each next node sum is strictly greater than the previous: image.png Now we can walk this graph in exactly k steps with Dijkstra algorithm using PriorityQueue to find the next smallest node.

Approach

  • use visited set
  • careful with Int overflow
  • let’s use Kotlin’s generateSequence

Complexity

  • Time complexity: \(O(klogk)\), there are k steps to peek from heap of size k

  • Space complexity: \(O(k)\)

Code


fun kSmallestPairs(nums1: IntArray, nums2: IntArray, k: Int): List<List<Int>> =
    with(PriorityQueue<List<Int>>(compareBy({ nums1[it[0]].toLong() + nums2[it[1]].toLong() }))) {
        add(listOf(0, 0))
        val visited = HashSet<Pair<Int, Int>>()
        visited.add(0 to 0)

        generateSequence {
            val (i, j) = poll()
            if (i < nums1.lastIndex && visited.add(i + 1 to j)) add(listOf(i + 1, j))
            if (j < nums2.lastIndex && visited.add(i to j + 1)) add(listOf(i, j + 1))
            listOf(nums1[i], nums2[j])
        }
        .take(minOf(k.toLong(), nums1.size.toLong() * nums2.size.toLong()).toInt())
        .toList()
    }

26.06.2023

2462. Total Cost to Hire K Workers medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/257

Problem TLDR

The sum of the smallest cost from suffix and prefix of a costs size of candidates in k iterations

Intuition

Description of the problem is rather ambiguous: we actually need to consider candidates count of items from the head and from the tail of the costs array. Then we can use PriorityQueue to choose the minimum and adjust two pointers lo and hi.

Approach

  • use separate condition, when 2 * candidates >= costs.size
  • careful with indexes, check yourself by doing dry run
  • we can use separate variable takenL and takenR or just use queue’s sizes to minify the code

Complexity

  • Time complexity: \(O(nlog(n))\)

  • Space complexity: \(O(n)\)

Code


        fun totalCost(costs: IntArray, k: Int, candidates: Int): Long {
            val pqL = PriorityQueue<Int>()
            val pqR = PriorityQueue<Int>()
            var lo = 0
            var hi = costs.lastIndex
            var sum = 0L
            var count = 0
            if (2 * candidates >= costs.size) while (lo <= hi) pqL.add(costs[lo++])
            while (pqL.size < candidates && lo <= hi) pqL.add(costs[lo++])
            while (pqR.size < candidates && lo < hi) pqR.add(costs[hi--])
            while (lo <= hi && count++ < k) {
                if (pqR.peek() < pqL.peek()) {
                    sum += pqR.poll()
                    pqR.add(costs[hi--])
                } else {
                    sum += pqL.poll()
                    pqL.add(costs[lo++])
                }
            }
            while (pqR.isNotEmpty()) pqL.add(pqR.poll())
            while (count++ < k && pqL.isNotEmpty()) sum += pqL.poll()
            return sum
        }

25.06.2023

1575. Count All Possible Routes hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/256

Problem TLDR

Count paths from start to finish using |locations[i]-locations[j] of the fuel

Intuition

Let’s observe the example:


//  0 1 2 3 4
//  2 3 6 8 4
//    *   *
//
//  2 3 4 6 8
//    *     *
//
//  3-2(4)-3(3)-6(0)
//  3-6(2)-8(0)
//  3-8(5)
//  3-8(5)-6(3)-8(1)
//  3-4(4)-6(2)-8(0)

At each position curr given the amount of fuel f there is a certain number of ways to finish. It is independent of all the other factors, so can be safely cached.

Approach

  • as there are also paths from finish to finish, modify the code to search other paths when finish is reached

Complexity

  • Time complexity: \(O(nf)\), f - is a max fuel

  • Space complexity: \(O(nf)\)

Code


fun countRoutes(locations: IntArray, start: Int, finish: Int, fuel: Int): Int {
    //  0 1 2 3 4
    //  2 3 6 8 4
    //    *   *
    //
    //  2 3 4 6 8
    //    *     *
    //
    //  3-2(4)-3(3)-6(0)
    //  3-6(2)-8(0)
    //  3-8(5)
    //  3-8(5)-6(3)-8(1)
    //  3-4(4)-6(2)-8(0)

    val cache = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(curr: Int, f: Int): Int {
        if (f < 0) return 0
        return cache.getOrPut(curr to f) {
            var sum = if (curr == finish) 1 else 0
            locations.forEachIndexed { i, n ->
                if (i != curr) {
                    sum = (sum + dfs(i, f - Math.abs(n - locations[curr]))) % 1_000_000_007
                }
            }
            return@getOrPut sum
        }
    }
    return dfs(start, fuel)
}

24.06.2023

956. Tallest Billboard hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/255

Problem TLDR

Max sum of disjoint set in array

Intuition

Naive Dynamic Programming solution is to do a full search, adding to the first and to the second sums. That will give Out of Memory for this problem constraints.


dp[i][firstSum][secondSum] -> Out of Memory

The trick to make it work and consume less memory, is to cache only the difference firstSum - secondSum. It will slightly modify the code, but the principle is the same: try to add to the first, then to the second, otherwise skip.

Approach

  • we can compute the first sum, as when diff == 0 then sum1 == sum2

Complexity

  • Time complexity: \(O(nm)\), m is a max difference

  • Space complexity: \(O(nm)\)

Code


fun tallestBillboard(rods: IntArray): Int {
    val cache = Array(rods.size + 1) { Array(10000) { -1 } }
    fun dfs(curr: Int, sumDiff: Int): Int {
        if (curr == rods.size) return if (sumDiff == 0) 0 else Int.MIN_VALUE / 2

        return cache[curr][sumDiff + 5000].takeIf { it != -1 } ?: {
            val take1 = rods[curr] + dfs(curr + 1, sumDiff + rods[curr])
            val take2 = dfs(curr + 1, sumDiff - rods[curr])
            val notTake = dfs(curr + 1, sumDiff)
            maxOf(take1, take2, notTake)
        }().also { cache[curr][sumDiff + 5000] = it }
    }
    return dfs(0, 0)
}

23.06.2023

1027. Longest Arithmetic Subsequence medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/254

Problem TLDR

Max arithmetic subsequence length in array

Intuition

This was a hard problem for me :) Naive Dynamic Programming solution with recursion and cache will give TLE. Let’s observe the result, adding numbers one-by-one:


// 20 1 15 3 10 5 8
// 20
// 20 1
//  1
// 20 20  1 15
//  1 15 15
//
// 20 20 20  1 1 15 3
// 1  15  3 15 3 3
//
// 20 20 20 20  1 1  1 15 15 10
//  1 15  3 10 15 3 10  3 10
//    10
//
// 20 20 20 20 20  1 1  1 1 15 15 15 10 5
//  1 15  3 10  5 15 3 10 5  3 10  5  5
//    10                        5
//     5
//
// 20 20 20 20 20 20  1 1  1 1 1 15 15 15 15 10 10 5 8
//  1 15  3 10  5  8 15 3 10 5 8  3 10  5  8  5  8 8
//    10                             5

For each pair from-to there is a sequence. When adding another number, we know what next numbers are expected.

Approach

We can put those sequences in a HashMap by next number key.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

Code


data class R(var next: Int, val d: Int, var size: Int)
fun longestArithSeqLength(nums: IntArray): Int {
    // 20 1 15 3 10 5 8
    // 20
    // 20 1
    //  1
    // 20 20  1 15
    //  1 15 15
    //
    // 20 20 20  1 1 15 3
    // 1  15  3 15 3 3
    //
    // 20 20 20 20  1 1  1 15 15 10
    //  1 15  3 10 15 3 10  3 10
    //    10
    //
    // 20 20 20 20 20  1 1  1 1 15 15 15 10 5
    //  1 15  3 10  5 15 3 10 5  3 10  5  5
    //    10                        5
    //     5
    //
    // 20 20 20 20 20 20  1 1  1 1 1 15 15 15 15 10 10 5 8
    //  1 15  3 10  5  8 15 3 10 5 8  3 10  5  8  5  8 8
    //    10                             5

    val nextToR = mutableMapOf<Int, MutableList<R>>()
        var max = 2
        nums.forEachIndexed { to, num ->
            nextToR.remove(num)?.forEach { r ->
                r.next = num + r.d
                max = maxOf(max, ++r.size)
                nextToR.getOrPut(r.next) { mutableListOf() } += r
            }
            for (from in 0..to - 1) {
                val d = num - nums[from]
                val next = num + d
                nextToR.getOrPut(next) { mutableListOf() } += R(next, d, 2)
            }
        }
        return max
    }

22.06.2023

714. Best Time to Buy and Sell Stock with Transaction Fee medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/253

Problem TLDR

Max profit from buying stocks and selling them with fee for prices[day]

Intuition

Naive recursive or iterative Dynamic Programming solution will take \(O(n^2)\) time if we iterate over all days for buying and for selling. The trick here is to consider the money balances you have each day. We can track two separate money balances: for when we’re buying the stock balanceBuy and for when we’re selling balanceSell. Then, it is simple to greedily track balances:

  • if we choose to buy, we subtract prices[day] from balanceBuy
  • if we choose to sell, we add prices[day] - fee to balanceSell
  • just greedily compare previous balances with choices and choose maximum balance.

Approach

  • balances are always following each other: buy-sell-buy-sell.., or we can rewrite this like currentBalance = maxOf(balanceSell, balanceBuy) and use it for addition and subtraction.
  • we can keep only the previous balances, saving space to \(O(1)\)

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun maxProfit(prices: IntArray, fee: Int) = prices
.fold(-prices[0] to 0) { (balanceBuy, balance), price ->
    maxOf(balanceBuy, balance - price) to maxOf(balance, balanceBuy + price - fee)
}.second

21.06.2023

2448. Minimum Cost to Make Array Equal hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/252

Problem TLDR

Min cost to make all arr[i] equal, where each change is cost[i]

Intuition

First idea is that at least one element can be unchanged. Assume, that we want to keep the most costly element unchanged, but this will break on example:


1 2 2 2    2 1 1 1
f(1) = 0 + 1 + 1 + 1 = 3
f(2) = 2 + 0 + 0 + 0 = 2 <-- more optimal

Let’s observe the resulting cost for each number:


//    1 2 3 2 1     2 1 1 1 1
//0:  2 2 3 2 1 = 10
//1:  0 1 2 1 0 = 4
//2:  2 0 1 0 1 = 4
//3:  4 1 0 1 2 = 8
//4:  6 2 1 2 3 = 14

We can see that f(x) have a minimum and is continuous. We can find it with Binary Search, comparing the slope = f(mid + 1) - f(mid - 1). If slope > 0, minimum is on the left.

Approach

For more robust Binary Search:

  • use inclusive lo, hi
  • always compute the result min
  • always move the borders lo = mid + 1 or hi = mid - 1
  • check the last case lo == hi

Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(1)\)

Code


fun minCost(nums: IntArray, cost: IntArray): Long {
    //    1 2 3 2 1     2 1 1 1 1
    //0:  2 2 3 2 1 = 10
    //1:  0 1 2 1 0 = 4
    //2:  2 0 1 0 1 = 4
    //3:  4 1 0 1 2 = 8
    //4:  6 2 1 2 3 = 14
    fun costTo(x: Long): Long {
        return nums.indices.map { Math.abs(nums[it].toLong() - x) * cost[it].toLong() }.sum()
    }
    var lo = nums.min()?.toLong() ?: 0L
    var hi = nums.max()?.toLong() ?: 0L
    var min = costTo(lo)
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val costMid1 = costTo(mid - 1)
        val costMid2 = costTo(mid + 1)
        min = minOf(min, costMid1, costMid2)
        if (costMid1 < costMid2) hi = mid - 1 else lo = mid + 1
    }
    return min
}

20.06.2023

2090. K Radius Subarray Averages medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/251

Problem TLDR

Array containing sliding window of size 2k+1 average or -1

Intuition

Just do what is asked

Approach

  • careful with Int overflow

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun getAverages(nums: IntArray, k: Int): IntArray {
    if (k == 0) return nums
    var sum = 0L
    val res = IntArray(nums.size) { -1 }
    for (i in 0 until nums.size) {
        sum += nums[i]
        if (i > 2 * k) sum -= nums[i - 2 * k - 1]
        if (i >= 2 * k) res[i - k] = (sum / (2 * k + 1)).toInt()
    }
    return res
}

19.06.2023

1732. Find the Highest Altitude easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/250

Problem TLDR

Max running sum

Intuition

Just sum all the values and compute the max

Approach

Let’s write Kotlin fold one-liner

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


fun largestAltitude(gain: IntArray): Int = gain
.fold(0 to 0) { (max, sum), t -> maxOf(max, sum + t) to (sum + t) }
.first

18.06.2023

2328. Number of Increasing Paths in a Grid hard blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/249

Problem TLDR

Count increasing paths in a matrix

Intuition

For every cell in a matrix, we can calculate how many increasing paths are starting from it. This result can be memorized. If we know the sibling’s result, then we add it to the current if curr > sibl.

Approach

  • use Depth-First search for the paths finding
  • use LongArray for the memo

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun countPaths(grid: Array<IntArray>): Int {
    val m = 1_000_000_007L
    val counts = Array(grid.size) { LongArray(grid[0].size) }
    fun dfs(y: Int, x: Int): Long {
        return counts[y][x].takeIf { it != 0L } ?: {
            val v = grid[y][x]
            var sum = 1L
            if (x > 0 && v > grid[y][x - 1]) sum = (sum + dfs(y, x - 1)) % m
            if (y > 0 && v > grid[y - 1][x]) sum = (sum + dfs(y - 1, x)) % m
            if (y < grid.size - 1 && v > grid[y + 1][x]) sum = (sum + dfs(y + 1, x)) % m
            if (x < grid[0].size - 1 && v > grid[y][x + 1]) sum = (sum + dfs(y, x + 1)) % m
            sum
        }().also { counts[y][x] = it }
    }
    return (0 until grid.size * grid[0].size)
    .fold(0L) { r, t -> (r + dfs(t / grid[0].size, t % grid[0].size)) % m }
    .toInt()
}

17.06.2023

1187. Make Array Strictly Increasing hard blog post substack

image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/248

Problem TLDR

Minimum replacements to make arr1 increasing using any numbers arr2

Intuition

For any current position in arr1 we can leave this number or replace it with any number from arr2[i] > curr. We can write Depth-First Search to check all possible replacements. To memorize, we must also consider the previous value. It can be used as-is, but more optimally, we just store a skipped boolean flag and restore the prev value: if it was skipped, then previous is from arr1 else from arr2.

Approach

  • sort and distinct the arr2
  • use Array for cache, as it will be faster than a HashMap
  • use explicit variable for the invalid result
  • for the stop condition, if all the arr1 passed, then result it good

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

Code


fun makeArrayIncreasing(arr1: IntArray, arr2: IntArray): Int {
    val list2 = arr2.distinct().sorted()
    val INV = -1
    val cache = Array(arr1.size + 1) { Array(list2.size + 1) { IntArray(2) { -2 } } }
    fun dfs(pos1: Int, pos2: Int, skipped: Int): Int {
        val prev = if (skipped == 1) arr1.getOrNull(pos1-1)?:-1 else list2.getOrNull(pos2-1)?:-1
        return if (pos1 == arr1.size) 0 else cache[pos1][pos2][skipped].takeIf { it != -2} ?:
        if (pos2 == list2.size) {
            if (arr1[pos1] > prev) dfs(pos1 + 1, pos2, 1) else INV
        } else if (list2[pos2] <= prev) {
            dfs(pos1, pos2 + 1, 1)
        } else {
            val replace = dfs(pos1 + 1, pos2 + 1, 0)
            val skip = if (arr1[pos1] > prev) dfs(pos1 + 1, pos2, 1) else INV
            if (skip != INV && replace != INV) minOf(skip, 1 + replace)
            else if (replace != INV) 1 + replace else skip
        }.also { cache[pos1][pos2][skipped] = it }
    }
    return dfs(0, 0, 1)
}

16.06.2023

1569. Number of Ways to Reorder Array to Get Same BST hard blog post substack image.png

Join me on Telegram Leetcode_daily

https://t.me/leetcode_daily_unstoppable/247

Problem TLDR

Count permutations of an array with identical Binary Search Tree

Intuition

First step is to build a Binary Search Tree by adding the elements one by one. Let’s observe what enables the permutations in [34512]: image.png Left child [12] don’t have permutations, as 1 must be followed by 2. Same for the right [45]. However, when we’re merging left and right, they can be merged in different positions. Let’s observe the pattern for merging ab x cde, ab x cd, ab x c, a x b: image.png And another, abc x def: image.png For each length of a left len1 and right len2 subtree, we can derive the equation for permutations p: \(p(len1, len2) = p(len1 - 1, len2) + p(len1, len2 - 1)\) Also, when left or right subtree have several permutations, like abc, acb, cab, and right def, dfe, the result will be multiplied 3 x 2.

Approach

Build the tree, then compute the p = left.p * right.p * p(left.len, right.len) in a DFS.

Complexity

  • Time complexity: \(O(n^2)\), n for tree walk, and n^2 for f
  • Space complexity: \(O(n^2)\)

Code


class Node(val v: Int, var left: Node? = null, var right: Node? = null)
data class R(val perms: Long, val len: Long)
fun numOfWays(nums: IntArray): Int {
    val mod = 1_000_000_007L
    var root: Node? = null
    fun insert(n: Node?, v: Int): Node {
        if (n == null) return Node(v)
        if (v > n.v) n.right = insert(n.right, v)
        else n.left = insert(n.left, v)
        return n
    }
    nums.forEach { root = insert(root, it) }
    val cache = mutableMapOf<Pair<Long, Long>, Long>()
    fun f(a: Long, b: Long): Long {
        return if (a < b) f(b, a) else if (a <= 0 || b <= 0) 1 else cache.getOrPut(a to b) {
            (f(a - 1, b) + f(a, b - 1)) % mod
        }
    }
    fun perms(a: R, b: R): Long {
        val perms = (a.perms * b.perms) % mod
        return (perms * f(a.len , b.len)) % mod
    }
    fun dfs(n: Node?): R {
        if (n == null) return R(1, 0)
        val left = dfs(n.left)
        val right = dfs(n.right)
        return R(perms(left, right), left.len + right.len + 1)
    }
    val res = dfs(root)?.perms?.dec() ?: 0
    return (if (res < 0) res + mod else res).toInt()
}

15.06.2023

1161. Maximum Level Sum of a Binary Tree medium blog post substack image.png

Join me on Telegram Leetcode_daily

https://t.me/leetcode_daily_unstoppable/246

Problem TLDR

Binary Tree level with max sum

Intuition

We can use Breadth-First Search to find a sum of each level.

Approach

Let’s try to write it in a Kotlin style

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun maxLevelSum(root: TreeNode?) = with(ArrayDeque<TreeNode>()) {
    root?.let { add(it) }
    generateSequence<Int> {
        if (isEmpty()) null else (1..size).map {
            with(poll()) {
                `val`.also {
                    left?.let { add(it) }
                    right?.let { add(it) }
                }
            }
        }.sum()
    }.withIndex().maxBy { it.value }?.index?.inc() ?: 0
}

14.06.2023

530. Minimum Absolute Difference in BST easy blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/245

Problem TLDR

Min difference in a BST

Intuition

In-order traversal in a BST gives a sorted order, we can compare curr - prev.

Approach

Let’s write a Morris traversal: make the current node a rightmost child of its left child.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


fun getMinimumDifference(root: TreeNode?): Int {
    if (root == null) return 0
    var minDiff = Int.MAX_VALUE
    var curr = root
    var prev = -1
    while (curr !=  null) {
        val left = curr.left
        if (left != null) {
            var leftRight = left
            while (leftRight.right != null) leftRight = leftRight.right
            leftRight.right = curr
            curr.left = null
            curr = left
        } else {
            if (prev >= 0) minDiff = minOf(minDiff, curr.`val` - prev)
            prev = curr.`val`
            curr = curr.right
        }
    }
    return minDiff
}

13.06.2023

2352. Equal Row and Column Pairs medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/244

Problem TLDR

Count of rowArray == colArray in an n x n matrix.

Intuition

Compute hash function for each row and each col, then compare them. If hash(row) == hash(col), then compare arrays. For hashing, we can use simple 31 * prev + curr, that encodes both value and position.

Approach

  • For this Leetcode data, tan hash works perfectly, we can skip comparing the arrays.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n)\)

Code


fun equalPairs(grid: Array<IntArray>): Int {
    val rowHashes = grid.map { it.fold(0.0) { r, t ->  Math.tan(r) + t } }
    val colHashes = (0..grid.lastIndex).map { x ->
        (0..grid.lastIndex).fold(0.0) { r, t -> Math.tan(r) + grid[t][x] } }
        return (0..grid.size * grid.size - 1).count {
            rowHashes[it / grid.size] == colHashes[it % grid.size]
        }
    }

12.06.2023

image.png 228. Summary Ranges easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/243

Problem TLDR

Fold continues ranges in a sorted array 1 2 3 5 -> 1->3, 5

Intuition

Scan from start to end, modify the last interval or add a new one.

Approach

Let’s write a Kotlin one-liner

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun summaryRanges(nums: IntArray): List<String> = nums
    .fold(mutableListOf<IntArray>()) { r, t ->
        if (r.isEmpty() || r.last()[1] + 1 < t) r += intArrayOf(t, t)
        else r.last()[1] = t
        r
    }
    .map { (f, t) -> if (f == t) "$f" else "$f->$t"}

11.06.2023

1146. Snapshot Array medium blog post substack image.png

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/242

Problem TLDR

Implement an array where all elements can be saved into a `snapshot’s.

Intuition

Consider example:


// 0 1 2 3 4 5 6 <-- snapshot id
// 1 . . 2 . . 3 <-- value

When get()(2) called, 1 must be returned. So, we need to keep all the previous values. We can put them into a list combining with the current snapshot id: (1,0), (2, 3), (3, 6). Then we can do a Binary Search and find the highest_id >= id.

Approach

For more robust Binary Search:

  • use inclusive lo, hi
  • check last condition lo == hi
  • always write the result ind = mid
Complexity
  • Time complexity: \(O(log(n))\) for get
  • Space complexity: \(O(n)\)

Code


class SnapshotArray(length: Int) {
    // 0 1 2 3 4 5 6
    // 1 . . 2 . . 3
    val arr = Array<MutableList<Pair<Int, Int>>>(length) { mutableListOf() }
    var currId = 0

    fun set(index: Int, v: Int) {
        val idVs = arr[index]
        if (idVs.isEmpty() || idVs.last().first != currId) idVs += currId to v
        else idVs[idVs.lastIndex] = currId to v
    }

    fun snap(): Int = currId.also { currId++ }

    fun get(index: Int, id: Int): Int {
        var lo = 0
        var hi = arr[index].lastIndex
        var ind = -1
        while (lo <= hi) {
            val mid = lo + (hi - lo) / 2
            if (arr[index][mid].first <= id) {
                ind = mid
                lo = mid + 1
            } else hi = mid - 1
        }
        return if (ind == -1) 0 else arr[index][ind].second
    }

}

10.06.2023

1802. Maximum Value at a Given Index in a Bounded Array medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/241

Problem TLDR

Max at index in an n sized array, where sum <= maxSum, nums[i] > 0 and maxDiff(i, i+1) < 2.

Intuition

Let’s write possible numbers, for example:


// n=6, i=1, m=10
// 10/6 = 1
// 0 1 2 3 4 5
// -----------
// 0 1 0 0 0 0 sum = 1
// 1 2 1 0 0 0 sum = 1 + (1 + 1 + 1) = 4
// 2 3 2 1 0 0 sum = 4 + (1 + 2 + 1) = 8
// 3 4 3 2 1 0 sum = 8 + (1 + 3 + 1) = 13 > 10  prev + (1 + left + right)
// 4 5 4 3 2 1 sum = 13 + (1 + 4 + 1) = 19      left = minOf(left, i)
// 5 6 5 4 3 2 sum = 19 + (1 + 4 + 1) = 24      right = minOf(right, size - i - 1)
// 6 7 6 5 4 3
// ...
//   5+x       sum = 19 + x * (1 + 4 +1)
// ...
// S(x-1) - S(x-1-i) + x + S(x-1) - S(x-1 - (size-i-1))
// x + 2 * S(x-1) - S(x-1-i) - S(x-size+i)
// S(y) = y * (y + 1) / 2

We should minimize the sum for it to be <= maxSum, so naturally, we place the maximum at index and do strictly lower the sibling numbers. Looking at the example, we see there is an arithmetic sum to the left and to the right of the index. \(S(n) = 1 + 2 + .. + (n-1) + n = n * (n+1) / 2\) We are also must subtract part of the sum, that out of the array: \(\sum = S(x-1) - S(x-1-i) + x + S(x-1) - S(x-1 - (size-i-1))\) Another catch, numbers can’t be 0, so we must start with an array filled of 1: 1 1 1 1 1 1. That will modify our algorithm, adding n to the sum and adding one more step to the max.

Given that we know sum for each max, and sum will grow with increasing of the max, we can do a binary search sum = f(max) for max.

Approach

For more robust binary search:

  • use inclusive borders lo and hi
  • check the last condition lo == hi
  • always compute the result max = mid
  • avoid the number overflow

    Complexity

  • Time complexity: \(O(log(n))\)
  • Space complexity: \(O(1)\)

Code


fun maxValue(n: Int, index: Int, maxSum: Int): Int {

    val s: (Int) -> Long = { if (it < 0L) 0L else it.toLong() * (it.toLong() + 1L) / 2L }
    var lo = 0
    var hi = maxSum
    var max = lo
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val sum = n + mid + 2L * s(mid - 1) - s(mid - 1 - index) - s(mid - n + index)
        if (sum <= maxSum) {
            max = mid
            lo = mid + 1
        } else hi = mid - 1
    }
    return max + 1
}

09.06.2023

744. Find Smallest Letter Greater Than Target easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/240

Problem TLDR

Lowest char greater than target.

Intuition

In a sorted array, we can use the Binary Search.

Approach

For more robust code:

  • use inclusive lo and hi
  • check the last condition lo == hi
  • always move lo or hi
  • always write a good result res = ...
  • safely compute mid

    Complexity

  • Time complexity: \(O(log(n))\)
  • Space complexity: \(O(1)\)

Code


fun nextGreatestLetter(letters: CharArray, target: Char): Char {
    var res = letters[0]
    var lo = 0
    var hi = letters.lastIndex
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (letters[mid] > target) {
            hi = mid - 1
            res = letters[mid]
        } else lo = mid + 1
    }
    return res
}

08.06.2023

1351. Count Negative Numbers in a Sorted Matrix easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/239

Problem TLDR

Count negatives in a sorted by row and by column matrix.

Intuition

Consider example:


4  3  2 -1
3  2  1 -1
1  1 -1 -2
^ we are here
-1 -1 -2 -3

If we set position x at the first negative number, it is guaranteed, that the next row[x] will also be negative. So we can skip already passed columns.

Approach

Let’s use Kotlin’s fold operator.

Complexity

  • Time complexity: \(O(n + m)\)
  • Space complexity: \(O(1)\)

Code


fun countNegatives(grid: Array<IntArray>): Int =
    grid.fold(0 to 0) { (total, prev), row ->
        var curr = prev
        while (curr < row.size && row[row.lastIndex - curr] < 0) curr++
        (total + curr) to curr
    }.first
}

07.06.2023

1318. Minimum Flips to Make a OR b Equal to c medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/238

Problem TLDR

Minimum a and b Int bit flips to make a or b == c.

Intuition

Naive implementation is to iterate over 32 bits and flip a or/and b bits to match c. If we didn’t consider the case where a = 1 and b = 1 and c = 0, the result would be (a or b) xor c, as a or b gives us the left side of the equation, and xor c gives only bits that are needed to flip. For the corner case a = b = 1, c = 0, we must do additional flip to make 0, and we must make any other combinations 0:


a b c     a and b   c.inv()   (a and b) and c.inv()

0 0 1     0         0         0
0 1 0     0         1         0
0 1 1     0         0         0
1 0 0     0         1         0
1 0 1     0         0         0
1 1 0     1         1         1
1 1 1     1         0         0

Approach

Use Integer.bitCount.

Complexity

  • Time complexity: \(O(1)\)
  • Space complexity: \(O(1)\)

Code


fun minFlips(a: Int, b: Int, c: Int): Int =
Integer.bitCount((a or b) xor c) + Integer.bitCount((a and b) and c.inv())

06.06.2023

1502. Can Make Arithmetic Progression From Sequence easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/237

Problem TLDR

Is IntArray can be arithmetic progression?

Intuition

Sort, then use sliding window.

Approach

Let’s write Kotlin one-liner.

Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(n)\)

Code


fun canMakeArithmeticProgression(arr: IntArray): Boolean =
arr.sorted().windowed(2).groupBy { it[1] - it[0] }.keys.size == 1

05.06.2023

1232. Check If It Is a Straight Line easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/236

Problem TLDR

Are all the x,y points in a line?

Intuition

We can compare \(tan_i = dy_i/dx_i = dy_0/dx_0\)

Approach

  • corner case is a vertical line

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


fun checkStraightLine(coordinates: Array<IntArray>): Boolean =
    with((coordinates[1][1] - coordinates[0][1])/
    (coordinates[1][0] - coordinates[0][0]).toDouble()) {
        coordinates.drop(2).all {
            val o = (it[1] - coordinates[0][1]) / (it[0] - coordinates[0][0]).toDouble()

            isInfinite() && o.isInfinite() || this == o
        }
    }

04.06.2023

547. Number of Provinces medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/235

Problem TLDR

Count connected groups in graph.

Intuition

Union-Find will perfectly fit to solve this problem.

Approach

For more optimal Union-Find:

  • use path compression in the root method: uf[it] = x
  • connect the smallest size subtree to the largest

    Complexity

  • Time complexity: \(O(a(n)n^2)\), a(n) - reverse Ackerman function f(x) = 2^2^2..^2, x times. a(Int.MAX_VALUE) = 2^32 = 2^2^5 == 3
  • Space complexity: \(O(n^2)\)

Code


fun findCircleNum(isConnected: Array<IntArray>): Int {
    val uf = IntArray(isConnected.size) { it }
    val sz = IntArray(isConnected.size) { 1 }
    var count = uf.size
    val root: (Int) -> Int = {
        var x = it
        while (uf[x] != x) x = uf[x]
        uf[it] = x
        x
    }
    val connect: (Int, Int) -> Unit = { a, b ->
        val rootA = root(a)
        val rootB = root(b)
        if (rootA != rootB) {
            count--
            if (sz[rootA] < sz[rootB]) {
                uf[rootB] = rootA
                sz[rootA] += sz[rootB]
                sz[rootB] = 0
            } else {
                uf[rootA] = rootB
                sz[rootB] += sz[rootA]
                sz[rootA] = 0
            }
        }
    }
    for (i in 0..sz.lastIndex)
    for (j in 0..sz.lastIndex)
    if (isConnected[i][j] == 1) connect(i, j)
    return count
}

03.06.2023

1376. Time Needed to Inform All Employees medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/234

Problem TLDR

Total time from headID to all nodes in graph.

Intuition

Total time will be the maximum time from the root of the graph to the lowest node. To find it out, we can use DFS.

Approach

Build the graph, then write the DFS.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun numOfMinutes(n: Int, headID: Int, manager: IntArray, informTime: IntArray): Int {
    val fromTo = mutableMapOf<Int, MutableList<Int>>()
        (0 until n).forEach { fromTo.getOrPut(manager[it]) { mutableListOf() } += it }
        fun dfs(curr: Int): Int {
            return informTime[curr] + (fromTo[curr]?.map { dfs(it) }?.max() ?: 0)
        }
        return dfs(headID)
    }

02.06.2023

2101. Detonate the Maximum Bombs medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/233

Problem TLDR

Count detonated bombs by chain within each radius.

Intuition

A bomb will only detonate if its center within the radius of another. image.png For example, A can detonate B, but not otherwise.

Let’s build a graph, who’s who can detonate.

Approach

Build a graph, the do DFS trying to start from each node.

Complexity

  • Time complexity: \(O(n^3)\), each of the n DFS will take \(n^2\)
  • Space complexity: \(O(n^2)\)

Code


fun maximumDetonation(bombs: Array<IntArray>): Int {
    val fromTo = mutableMapOf<Int, MutableList<Int>>()
        for (i in 0..bombs.lastIndex) {
            val bomb1 = bombs[i]
            val rr = bomb1[2] * bomb1[2].toLong()
            val edges = fromTo.getOrPut(i) { mutableListOf() }
            for (j in 0..bombs.lastIndex) {
                if (i == j) continue
                val bomb2 = bombs[j]
                val dx = (bomb1[0] - bomb2[0]).toLong()
                val dy = (bomb1[1] - bomb2[1]).toLong()
                if (dx * dx + dy * dy <= rr) edges += j
            }
        }
        fun dfs(curr: Int, visited: HashSet<Int> = HashSet()): Int {
            return if (visited.add(curr)) {
                1 + (fromTo[curr]?.sumBy { dfs(it, visited) } ?:0)
            } else 0
        }
        var max = 1
        for (i in 0..bombs.lastIndex) max = maxOf(max, dfs(i))
        return max
    }

01.06.2023

1091. Shortest Path in Binary Matrix medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/232

Problem TLDR

0 path length in a binary square matrix.

Intuition

Just do BFS.

Approach

Some tricks for cleaner code:

  • check for x, y in range
  • iterate over dirs. This is a sequence of x and y
  • modify the input array. But don’t do this in production code.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun shortestPathBinaryMatrix(grid: Array<IntArray>): Int =
    with(ArrayDeque<Pair<Int, Int>>()) {
        val range = 0..grid.lastIndex
        val dirs = arrayOf(0, 1, 0, -1, -1, 1, 1, -1)
        if (grid[0][0] == 0) add(0 to 0)
        grid[0][0] = -1
        var step = 0
        while (isNotEmpty()) {
            step++
            repeat(size) {
                val (x, y) = poll()
                if (x == grid.lastIndex && y == grid.lastIndex) return step
                var dx = -1
                for (dy in dirs) {
                    val nx = x + dx
                    val ny = y + dy
                    if (nx in range && ny in range && grid[ny][nx] == 0) {
                        grid[ny][nx] = -1
                        add(nx to ny)
                    }
                    dx = dy
                }
            }
        }
        -1
    }

31.05.2023

1396. Design Underground System medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/229

Problem TLDR

Average time from, to when different user IDs do checkIn(from, time1) and checkOut(to, time2)

Intuition

Just do what is asked, use HashMap to track user’s last station.

Approach

  • store sum time and count for every from, to station
  • use Pair as key for HashMap

    Complexity

  • Time complexity: \(O(1)\), for each call
  • Space complexity: \(O(n)\)

Code


class UndergroundSystem() {
    val fromToSumTime = mutableMapOf<Pair<String, String>, Long>()
    val fromToCount = mutableMapOf<Pair<String, String>, Int>()
    val idFromTime = mutableMapOf<Int, Pair<String, Int>>()
    fun Pair<String, String>.time() = fromToSumTime[this] ?: 0L
    fun Pair<String, String>.count() = fromToCount[this] ?: 0

    fun checkIn(id: Int, stationName: String, t: Int) {
        idFromTime[id] = stationName to t
    }

    fun checkOut(id: Int, stationName: String, t: Int) {
        val (from, tFrom) = idFromTime[id]!!
        val fromTo = from to stationName
        fromToSumTime[fromTo] = t - tFrom + fromTo.time()
        fromToCount[fromTo] = 1 + fromTo.count()
    }

    fun getAverageTime(startStation: String, endStation: String): Double =
    with(startStation to endStation) {
        time().toDouble() / count().toDouble()
    }

}

30.05.2023

705. Design HashSet easy blog post substack

Telegram

https://t.me/leetcode_daily_unstoppable/228

Problem TLDR

Write a HashSet.

Intuition

There are different hash functions. Interesting implementations is In Java HashMap https://github.com/openjdk/jdk/blob/master/src/java.base/share/classes/java/util/HashMap.java

Approach

Use key % size for the hash function, grow and rehash when needed.

Complexity

  • Time complexity: \(O(1)\)
  • Space complexity: \(O(n)\)

Code


class MyHashSet(val initialSz: Int = 16, val loadFactor: Double = 1.6) {
            var buckets = Array<LinkedList<Int>?>(initialSz) { null }
            var size = 0

            fun hash(key: Int): Int = key % buckets.size

            fun rehash() {
            if (size > buckets.size * loadFactor) {
                val oldBuckets = buckets
                buckets = Array<LinkedList<Int>?>(buckets.size * 2) { null }
                    oldBuckets.forEach { it?.forEach { add(it) } }
                }
            }

            fun bucket(key: Int): LinkedList<Int> {
                val hash = hash(key)
                if (buckets[hash] == null) buckets[hash] = LinkedList<Int>()
                    return buckets[hash]!!
                }

                fun add(key: Int) {
                    val list = bucket(key)
                    if (!list.contains(key)) {
                        list.add(key)
                        size++
                        rehash()
                    }
                }

                fun remove(key: Int) {
                    bucket(key).remove(key)
                }

                fun contains(key: Int): Boolean =
                   bucket(key).contains(key)
}

29.05.2023

1603. Design Parking System easy blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/227

Problem TLDR

Return if car of type 1, 2 or 3 can be added given sizes big, medium and small.

Intuition

Just write the code.

Approach

Let’s use an array to minimize the number of lines.

Complexity

  • Time complexity: \(O(1)\)
  • Space complexity: \(O(1)\)

Code


class ParkingSystem(big: Int, medium: Int, small: Int) {
    val types = arrayOf(big, medium, small)

    fun addCar(carType: Int): Boolean = types[carType - 1]-- > 0
}

28.05.2023

1547. Minimum Cost to Cut a Stick hard blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/226

Problem TLDR

Min cost of cuts c1,..,ci,..,cn of [0..n] where cut cost the length = to-from.

Intuition

We every stick from..to we can try all the cuts in that range. This result will be optimal and can be cached.

Approach

  • use DFS + memo
  • check for range

    Complexity

  • Time complexity: \(k^2\), as maximum depth of DFS is k, and we loop for k.
  • Space complexity: \(k^2\)

Code


fun minCost(n: Int, cuts: IntArray): Int {
    val cache = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(from: Int, to: Int): Int {
        return cache.getOrPut(from to to) {
            var min = 0
            cuts.forEach {
                if (it in from + 1..to - 1) {
                    val new = to - from + dfs(from, it) + dfs(it, to)
                    if (min == 0 || new < min) min = new
                }
            }

            min
        }
    }
    return dfs(0, n)
}

27.05.2023

1406. Stone Game III hard blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/225

Problem TLDR

Winner of “Alice”, “Bob” or “Tie” in game of taking 1, 2 or 3 stones by turn from stoneValue array.

Intuition

Let’s count the result for Alice, starting at i element: \(alice_i = \sum_{i=1,2,3}(stones_i) + suffix_i - alice_{i+1}\) The result can be safely cached. Bob’s points will be Alice’s points in the next turn.

Approach

Let’s write bottom up DP.

  • use increased sizes for cache and suffix arrays for simpler code
  • corner case is the negative number, so we must take at least one stone

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun stoneGameIII(stoneValue: IntArray): String {
    val suffix = IntArray(stoneValue.size + 1)
    for (i in stoneValue.lastIndex downTo 0) suffix[i] = stoneValue[i] + suffix[i + 1]
    val cache = IntArray(stoneValue.size + 1)
    var bob = 0

    for (curr in stoneValue.lastIndex downTo 0) {
        var sum = 0
        var first = true
        for (j in 0..2) {
            val ind = curr + j
            if (ind > stoneValue.lastIndex) break
            sum += stoneValue[ind]
            val nextAlice = cache[ind + 1]
            val next = suffix[ind + 1] - nextAlice
            if (first || sum + next > cache[curr]) {
                first = false
                cache[curr] = sum + next
                bob = nextAlice
            }
        }
    }
    return if (cache[0] == bob) "Tie" else if (cache[0] > bob) "Alice" else "Bob"
}

26.05.2023

1140. Stone Game II medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/224

Problem TLDR

While Alice and Bob optimally take 1..2*m numbers from piles find maximum for Alice.

Intuition

For each position, we can cache the result for Alice starting from it. Next round, Bob will become Alice and use that cached result, but Alice will use the remaining part: \(bob_i = suffix_i - alice_i\) and \(alice_i = \sum(piles_{1..x<2m}) + (suffix_i - alice_{i + 1})\)

Approach

  • compute prefix sums in IntArray
  • use HashMap for simpler code, or Array for faster

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

Code


fun stoneGameII(piles: IntArray): Int {
    // 2 7 9 4 4    M      A   B
    // A            1      1
    // A A          2      2,7
    // A B B        2          7,9
    // A A B B B    3          9,4,4
    val sums = IntArray(piles.size)
    sums[0] = piles[0]
    for (i in 1..piles.lastIndex)
    sums[i] = sums[i - 1] + piles[i]
    val total = sums[sums.lastIndex]
    val cache = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(m: Int, curr: Int): Int {
        return cache.getOrPut(m to curr) {
            var res = 0
            var pilesSum = 0
            for (x in 0 until 2*m) {
                if (curr + x > piles.lastIndex) break
                pilesSum += piles[curr + x]
                val nextOther = dfs(maxOf(m, x + 1), curr + x + 1)
                val nextMy = total - sums[curr + x] - nextOther
                res = maxOf(res, pilesSum + nextMy)
            }
            res
        }
    }
    return dfs(1, 0)
}

25.05.2023

837. New 21 Game medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/223

Problem TLDR

Probability sum of random numbers 1..maxPts sum be < n after it overflow k.

Intuition

For every event, we choose one number from numbers 1..maxPts. Probability of this event is p1 = 1/maxPts.

For example, n=6, k=1, maxpts=10: we can pick any numbers 1, 2, 3, 4, 5, 6 that are <=6. Numbers 7, 8, 9, 10 are excluded, because they are >6. After we pick one number with probability p1 = 1/10, the sum will be >=k so we stop. The final probability is the sum of individual valid choices p = sum(good_p1)

Another example, n=6, k=2, maxpts=10: our choices are


// n = 6, k = 2, maxpts = 10
// p_win1 1+1, 1+2, 1+3, 1+4, 1+5, 2,   3,  4,  5,  6
//        0.01 0.01 0.01 0.01 0.01 0.1 0.1 0.1 0.1 0.1 = 0.55

When we go to the second round in cases of 1+1, 1+2, 1+3, 1+4, 1+5, we multiply the probabilities, so p(1+1) = p1*p1.

Next, observe the pattern for other examples:


// n = 6, k = 3, maxpts = 10
// p_win  1+1+1, 1+1+2, 1+1+3, 1+1+4, 1+2, 1+3, 1+4, 1+5, 2+1, 2+2, 2+3, 2+4, 3,  4,  5,   6
//        0.001  0.001  0.001  0.001  0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.1 0.1 0.1 0.1
// sum=0.484

// n = 6, k = 4, maxpts = 10
// p_win  1+1+1+1 1+1+1+2 1+1+1+3 1+1+2 1+1+3 1+1+4 1+2+1 1+2+2 1+2+3 1+3  1+4  1+5  2+1+1 2+1+2 2+1+3 2+2  2+3  2+4  3+1  3+2  3+3  4   5   6
//         .0001   .0001   .0001   .001  .001  .001  .001  .001  .001  .01  .01  .01  .001  .001  .001  .01  .01  .01  .01  .01  .01  .1  .1  .1
//sum=0.3993

What we see is the sequence of 1+1+1+1, 1+1+1+2, 1+1+1+3, where we pick a number from 1..maxpts then calculate the sum and if the sum is still smaller than n we go deeper and make another choice from 1..maxpts. That can be written as Depth-First Search algorithm:


fun dfs(currSum: Int): Double {
    ...
    var sumP = 0.0
    for (x in 1..maxPts)
    sumP += dfs(currSum + x)
    res = sumP * p1
}

This will work and gives us correct answers, but gives TLE for big numbers, as its time complexity is \(O(n^2)\).

Let’s observe this algorithm’s recurrent equation: \(f(x) = (f(x+1) + f(x+2)+..+f(x + maxPts))*p1\) \(f(x + 1) = (f(x+2) + f(x+3) +...+f(x + maxPts)*p1 + f(x + 1 + maxPts))*p1\) subtract second sequence from the first: \(f(x) - f(x + 1) = f(x+1)*p1 - f(x+1+maxPts)*p1\) \(f(x) = f(x+1) + (f(x+1) - f(x+1+maxPts))*p1\) This removes one dimension of iteration 1..maxPts. However, it fails the first case where currSum == k - 1, because the equation didn’t consider that not all x+maxPts are smaller than n. For this case, we must return (n-k+1)*p1 as we choose last number from range k - 1..n.

Approach

This problem is next to impossible to solve in an interview, given how many conclusions you must derive on the fly. So, hope no one will give it to you.

  • use an array for the faster cache

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun new21Game(n: Int, k: Int, maxPts: Int): Double {
    // n = 6, k = 1, maxpts = 10
    // cards: 1 2 3 4 5 6 7 8 9 10
    // p_win1(6, 10) = count(1 2 3 4 5 6) / 10 = 0.6

    // n = 6, k = 2, maxpts = 10
    // p_win1 1+1, 1+2, 1+3, 1+4, 1+5, 2,   3,  4,  5,  6
    //        0.01 0.01 0.01 0.01 0.01 0.1 0.1 0.1 0.1 0.1 = 0.55

    // n = 6, k = 3, maxpts = 10
    // p_win  1+1+1, 1+1+2, 1+1+3, 1+1+4, 1+2, 1+3, 1+4, 1+5, 2+1, 2+2, 2+3, 2+4, 3,  4,  5,   6
    //        0.001  0.001  0.001  0.001  0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.01 0.1 0.1 0.1 0.1
    // sum=0.484

    // n = 6, k = 4, maxpts = 10
    // p_win  1+1+1+1 1+1+1+2 1+1+1+3 1+1+2 1+1+3 1+1+4 1+2+1 1+2+2 1+2+3 1+3  1+4  1+5  2+1+1 2+1+2 2+1+3 2+2  2+3  2+4  3+1  3+2  3+3  4   5   6
    //         .0001   .0001   .0001   .001  .001  .001  .001  .001  .001  .01  .01  .01  .001  .001  .001  .01  .01  .01  .01  .01  .01  .1  .1  .1
    //sum=0.3993
    val p1 = 1.0 / maxPts.toDouble()
    val cache = Array<Double>(n + 1) { -1.0 }
        // f(x) = (f(x+1) + f(x+2)+..+f(x + maxPts))*p1
        // f(x + 1) = (f(x+2) + f(x+3) +...+f(x + maxPts)*p1 + f(x + 1 + maxPts))*p1
        // f(x) - f(x + 1) = f(x+1)*p1 - f(x+1+maxPts)*p1
        // f(x) = f(x+1) + (f(x+1) - f(x+1+maxPts))*p1
    fun dfs(currSum: Int): Double {
        if (currSum == k - 1) return minOf(1.0, (n-k+1)*p1) // corner case
        if (currSum >= k) return if (currSum <= n) 1.0 else 0.0
        if (cache[currSum] != -1.0) return cache[currSum]
        //var sumP = 0.0
        //for (x in 1..minOf(maxPts, n - currSum)) {
             //    sumP += dfs(currSum + x)
        //}
        //val res = sumP * p1
        val res = dfs(currSum + 1) + (dfs(currSum + 1) - dfs(currSum + 1 + maxPts)) * p1
        cache[currSum] = res
        return res
    }
    return dfs(0)
}

24.05.2023

2542. Maximum Subsequence Score medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/222

Problem TLDR

Max score of k sum(subsequence(a)) * min(subsequence(b))

Intuition

First, the result is independent of the order, so we can sort. For maximum score, it better to start with maximum multiplier of min. Then, we iterate from biggest nums2 to smallest. Greedily add numbers until we reach k elements. After size > k, we must consider what element to extract. Given our min is always the current value, we can safely take any element without modifying the minimum, thus take out the smallest by nums1.

Approach

  • use PriorityQueue to dynamically take out the smallest
  • careful to update score only when size == k, as it may decrease with more elements

    Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(n)\)

Code


fun maxScore(nums1: IntArray, nums2: IntArray, k: Int): Long {
    // 14  2 1 12 100000000000  1000000000000 100000000000
    // 13 11 7 1  1             1             1
    val inds = nums1.indices.sortedWith(
    compareByDescending<Int> { nums2[it] }
        .thenByDescending { nums1[it] })
    var score = 0L
    var sum = 0L
    val pq = PriorityQueue<Int>(compareBy { nums1[it] })
    inds.forEach {
        sum += nums1[it].toLong()
        pq.add(it)
        if (pq.size > k) sum -= nums1[pq.poll()].toLong()
        if (pq.size == k) score = maxOf(score, sum * nums2[it].toLong())
    }
    return score
}

23.05.2023

703. Kth Largest Element in a Stream medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/221

Problem TLDR

Kth largest

Intuition

We need to keep all values smaller than current largest kth element and can safely drop all other elements.

Approach

Use PriorityQueue.

Complexity

  • Time complexity: \(O(nlogk)\)
  • Space complexity: \(O(k)\)

Code


class KthLargest(val k: Int, nums: IntArray) {
    val pq = PriorityQueue<Int>(nums.toList())

        fun add(v: Int): Int = with (pq) {
            add(v)
            while (size > k) poll()
            peek()
        }
    }

22.05.2023

347. Top K Frequent Elements medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/220

Problem TLDR

First k unique elements sorted by frequency.

Intuition

Group by frequency 1 1 1 5 5 -> 1:3, 5:2, then bucket sort frequencies 2:5, 3:1, then flatten and take first k.

Approach

Code


fun topKFrequent(nums: IntArray, k: Int): IntArray {
    val freq = nums.groupBy { it }.mapValues { it.value.size }
    val freqToNum = Array<MutableList<Int>>(nums.size + 1) { mutableListOf() }
    freq.forEach { (num, fr) -> freqToNum[nums.size + 1 - fr].add(num) }
    return freqToNum
        .filter { it.isNotEmpty() }
        .flatten()
        .take(k)
        .toIntArray()
}

21.05.2023

934. Shortest Bridge medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/219

Problem TLDR

Find the shortest path from one island of 1’s to another.

Intuition

Shortest path can be found with Breadth-First Search if we start it from every border cell of the one of the islands. To detect border cell, we have to make separate DFS.

Approach

  • modify grid to store visited set

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

Code


fun Array<IntArray>.inRange(xy: Pair<Int, Int>) = (0..lastIndex).let {
    xy.first in it && xy.second in it
}
fun Pair<Int, Int>.siblings() = arrayOf(
(first - 1) to second, first to (second - 1),
(first + 1) to second, first to (second + 1)
)
fun shortestBridge(grid: Array<IntArray>): Int {
    val queue = ArrayDeque<Pair<Int, Int>>()
    fun dfs(x: Int, y: Int) {
        if (grid[y][x] == 1) {
            grid[y][x] = 2
            (x to y).siblings().filter { grid.inRange(it) }.forEach { dfs(it.first, it.second) }
        } else if (grid[y][x] == 0) queue.add(x to y)
    }
    (0 until grid.size * grid.size)
    .map { it / grid.size to it % grid.size}
    .filter { (y, x) -> grid[y][x] == 1 }
    .first().let { (y, x) -> dfs(x, y)}
    return with (queue) {
        var steps = 1
        while (isNotEmpty()) {
            repeat(size) {
                val xy = poll()
                if (grid.inRange(xy)) {
                    val (x, y) = xy
                    if (grid[y][x] == 1) return@shortestBridge steps - 1
                    if (grid[y][x] == 0) {
                        grid[y][x] = 3
                        addAll(xy.siblings().filter { grid.inRange(it) })
                    }
                }
            }
            steps++
        }
        -1
    }
}

20.05.2023

399. Evaluate Division medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/218

Problem TLDR

Given values for a/b and b/c find answers for a/c.

Intuition

Let’s build a graph, a -> b with weights of values[a/b]. Then answer is a path from one node to the other. The shortest path can be found with a Breadth-First Search.

Approach

  • careful with corner case x/x, where x is not in a graph.

    Complexity

  • Time complexity: \(O(nEV)\)
  • Space complexity: \(O(n+E+V)\)

Code


fun calcEquation(equations: List<List<String>>, values: DoubleArray, queries: List<List<String>>): DoubleArray {
    val fromTo = mutableMapOf<String, MutableList<Pair<String, Double>>>()
    equations.forEachIndexed { i, (from, to) ->
        fromTo.getOrPut(from) { mutableListOf() } += to to values[i]
        fromTo.getOrPut(to) { mutableListOf() } += from to (1.0 / values[i])
    }
    // a/c = a/b * b/c
    return queries.map { (from, to) ->
        with(ArrayDeque<Pair<String, Double>>()) {
            val visited = HashSet<String>()
                visited.add(from)
                if (fromTo.containsKey(to)) add(from to 1.0)
                while (isNotEmpty()) {
                    repeat(size) {
                        val (point, value) = poll()
                        if (point == to) return@map value
                        fromTo[point]?.forEach { (next, nvalue) ->
                            if (visited.add(next)) add(next to value * nvalue)
                        }
                    }
                }
                -1.0
            }
        }.toDoubleArray()
    }

19.05.2023

785. Is Graph Bipartite? medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/217

Problem TLDR

Find if graph is bipartite

Intuition

image.png Mark edge Red or Blue and it’s nodes in the opposite.

Approach

  • there are disconnected nodes, so run DFS for all of them

    Complexity

  • Time complexity: \(O(VE)\), DFS once for all vertices and edges
  • Space complexity: \(O(V+E)\), for reds and visited set.

Code


fun isBipartite(graph: Array<IntArray>): Boolean {
    val reds = IntArray(graph.size)
    fun dfs(u: Int, isRed: Int): Boolean {
        if (reds[u] == 0) {
            reds[u] = if (isRed == 0) 1 else isRed
            return graph[u].all { dfs(it, -reds[u]) }
        } else return reds[u] == isRed
    }
    return graph.indices.all { dfs(it, reds[it]) }
}

18.05.2023

1557. Minimum Number of Vertices to Reach All Nodes medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/216

Problem TLDR

Find all starting nodes in graph.

Intuition

Count nodes that have no incoming connections.

Approach

  • we can use subtract operation in Kotlin

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


fun findSmallestSetOfVertices(n: Int, edges: List<List<Int>>): List<Int> =
    (0 until n) - edges.map { it[1] }

17.05.2023

2130. Maximum Twin Sum of a Linked List medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/215

Problem TLDR

Max sum of head-tail twin ListNodes: a-b-c-d -> max(a+d, b+c)

Intuition

Add first half to the Stack, then pop until end reached.

Approach

  • use fast and slow pointers to find the center.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code


        fun pairSum(head: ListNode?): Int {
            var fast = head
            var slow = head
            var sum = 0
            val stack = Stack<Int>()
                while (fast != null) {
                    stack.add(slow!!.`val`)
                    slow = slow.next
                    fast = fast.next?.next
                }
                while (slow != null) {
                    sum = maxOf(sum, stack.pop() + slow.`val`)
                    slow = slow.next
                }
                return sum
            }

16.05.2023

24. Swap Nodes in Pairs medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/214

Problem TLDR

Swap adjacent ListNodes a-b-c-d -> b-a-d-c.

Intuition

Those kinds of problems are easy, but your task is to write it bug free from the first go.

Approach

For more robust code:

  • use dummy head to track for a new head
  • use explicit variables for each node in the configuration
  • do debug code by writing down it values in the comments

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


fun swapPairs(head: ListNode?): ListNode? {
    val dummy = ListNode(0).apply { next = head }
    var curr: ListNode? = dummy
    while (curr?.next != null && curr?.next?.next != null) {
        // curr->one->two->next
        // curr->two->one->next
        var one = curr.next
        var two = one?.next
        val next = two?.next
        curr.next = two
        two?.next = one
        one?.next = next

        curr = one
    }
    return dummy.next
}

15.05.2023

1721. Swapping Nodes in a Linked List medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/213

Problem TLDR

Swap the values of the head-tail k’th ListNodes.

Intuition

As we aren’t asked to swap nodes, the problem is to find nodes.

Approach

Travel the fast pointer at k distance, then move both fast and two nodes until fast reaches the end.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

Code


fun swapNodes(head: ListNode?, k: Int): ListNode? {
    var fast = head
    for (i in 1..k - 1) fast = fast?.next
    val one = fast
    var two = head
    while (fast?.next != null) {
        two = two?.next
        fast = fast?.next
    }
    one?.`val` = two?.`val`.also { two?.`val` = one?.`val` }
    return head
}

14.05.2023

1799. Maximize Score After N Operations hard blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/212

Problem TLDR

Max indexed-gcd-pair sum from 2n array; [3,4,6,8] -> 11 (1gcd(3,6) + 2gcd(4,8))

Intuition

For each step and remaining items, the result is always the same, so is memorizable.

Approach

  • search all possible combinations with DFS
  • use bitmask to avoid double counting
  • use an array for cache

    Complexity

  • Time complexity: \(O(n^22^n)\)
  • Space complexity: \(O(n2^n)\)

Code


    fun gcd(a: Int, b: Int): Int = if (b % a == 0) a else gcd(b % a, a)
    fun maxScore(nums: IntArray): Int {
        val n = nums.size / 2
        val cache = Array(n + 1) { IntArray(1 shl nums.size) { -1 } }
        fun dfs(step: Int, mask: Int): Int {
            if (step > n) return 0
            if (cache[step][mask] != -1) return cache[step][mask]
            var max = 0
            for (i in 0..nums.lastIndex) {
                val ibit = 1 shl i
                if (mask and ibit != 0) continue
                for (j in (i + 1)..nums.lastIndex) {
                    val jbit = 1 shl j
                    if (mask and jbit != 0) continue
                    val curr = step * gcd(nums[i], nums[j])
                    val next = dfs(step + 1, mask or ibit or jbit)
                    max = maxOf(max, curr + next)
                }
            }
            cache[step][mask] = max
            return max
        }
        return dfs(1, 0)
    }

13.05.2023

2466. Count Ways To Build Good Strings medium blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/211

Problem

Count distinct strings, length low to high, appending ‘0’ zero or ‘1’ one times. Return count % 1,000,000,007.

Intuition

Let’s add zero’s or one’s one by one. For each current length, the resulting count is independent of all the previous additions. We can cache the result by the current size of the string.

Approach

Let’s write a DFS solution, adding zero or one and count the good strings. Then we can rewrite it to the iterative DP.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

Code

top-down:


fun countGoodStrings(low: Int, high: Int, zero: Int, one: Int): Int {
    val m = 1_000_000_007
    val cache = mutableMapOf<Int, Int>()
    fun dfs(currSize: Int): Int {
        if (currSize > high) return 0
        return cache.getOrPut(currSize) {
            val curr = if (currSize in low..high) 1 else 0
            val addZeros = if (zero > 0) dfs(currSize + zero) else 0
            val addOnes = if (one > 0) dfs(currSize + one) else 0
            (curr + addZeros + addOnes) % m
        }
    }
    return dfs(0)
}

bottom-up


fun countGoodStrings(low: Int, high: Int, zero: Int, one: Int): Int {
    val cache = mutableMapOf<Int, Int>()
    for (sz in high downTo 0)
    cache[sz] = ((if (sz >= low) 1 else 0)
    + (cache[sz + zero]?:0)
    + (cache[sz + one]?:0)) % 1_000_000_007
    return cache[0]!!
}

12.05.2023

2140. Solving Questions With Brainpower medium


fun mostPoints(questions: Array<IntArray>): Long {
    val dp = LongArray(questions.size)
    for (i in questions.lastIndex downTo 0) {
        val (points, skip) = questions[i]
        val tail = if (i + skip + 1 > questions.lastIndex) 0 else dp[i + skip + 1]
        val notTake = if (i + 1 > questions.lastIndex) 0 else dp[i + 1]
        dp[i] = maxOf(points + tail, notTake)
    }
    return dp[0]
}

or minified golf version


fun mostPoints(questions: Array<IntArray>): Long {
    val dp = HashMap<Int, Long>()
    for ((i, q) in questions.withIndex().reversed())
    dp[i] = maxOf(q[0] + (dp[i + q[1] + 1]?:0), dp[i + 1]?:0)
    return dp[0]?:0
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/210

Intuition

If we go from the tail, for each element we are interested only on what happens to the right from it. Prefix of the array is irrelevant, when we’re starting from the element i, because we sure know, that we are taking it and not skipping. Given that, dynamic programming equation is: \(dp_i = max(points_i + dp_{i+1+skip_i}, dp_{i+1})\), where dp is the mostPoints starting from position i.

Approach

Let’s implement a bottom-up solution.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

11.05.2023

1035. Uncrossed Lines medium


fun maxUncrossedLines(nums1: IntArray, nums2: IntArray): Int {
    val cache = Array(nums1.size) { Array(nums2.size) { -1 } }
    val intersect = nums1.toSet().intersect(nums2.toSet())

    fun dfs(i: Int, j: Int, x: Int): Int {
        if (i == nums1.size || j == nums2.size) return 0
        val cached = cache[i][j]
        if (cached != -1) return cached
        val n1 = nums1[i]
        val n2 = nums2[j]
        val drawLine = if (n1 == x && n2 == x || n1 == n2) 1 + dfs(i + 1, j + 1, n1) else 0
        val skipTop = dfs(i + 1, j, x)
        val skipBottom = dfs(i, j + 1, x)
        val skipBoth = dfs(i + 1, j + 1, x)
        val startTop = if (intersect.contains(n1)) dfs(i + 1, j, n1) else 0
        val startBottom = if (intersect.contains(n2)) dfs(i, j + 1, n2) else 0
        val res = maxOf(
        drawLine,
        maxOf(drawLine, skipTop, skipBottom),
        maxOf(skipBoth, startTop, startBottom)
        )
        cache[i][j] = res
        return res
    }
    return dfs(0, 0, 0)
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/209

Intuition

Consider the case:


2 5 1 2 5
2 2 2 1 1 1 5 5 5

image.png

When we draw all the possible lines, we see that there is a choice to draw line 2-2 or four lines 1-1 or three 5-5 in the middle. Suffix lines 5-5 and prefix lines 2-2 are optimal already and can be cached as a result. To find an optimal choice we can use DFS. We can prune some impossible combinations by precomputing the intersected numbers and considering them only.

Approach

  • use an array for the faster cache instead of HashMap
  • for the intersection there is an intersect method in Kotlin

Complexity

  • Time complexity: \(O(n^3)\)
  • Space complexity: \(O(n^3)\)

10.05.2023

59. Spiral Matrix II medium


fun generateMatrix(n: Int): Array<IntArray> = Array(n) { IntArray(n) }.apply {
    var dir = 0
    var dxdy = arrayOf(0, 1, 0, -1)
    var x = 0
    var y = 0
    val nextX = { x + dxdy[(dir + 1) % 4] }
    val nextY = { y + dxdy[dir] }
    val valid = { x: Int, y: Int -> x in 0..n-1 && y in 0..n-1 && this[y][x] == 0 }

    repeat (n * n) {
        this[y][x] = it + 1
        if (!valid(nextX(), nextY())) dir = (dir + 1) % 4
        x = nextX()
        y = nextY()
    }
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/208

Intuition

Just implement what is asked. Let’s have the strategy of a robot: move it in one direction until it hits a wall, then change the direction.

Approach

  • to detect an empty cell, we can check it for == 0

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

9.05.2023

54. Spiral Matrix medium


fun spiralOrder(matrix: Array<IntArray>): List<Int> = mutableListOf<Int>().apply {
    var x = 0
    var y = 0
    val dxy = arrayOf(0, 1, 0, -1)
    val borders = arrayOf(matrix[0].lastIndex, matrix.lastIndex, 0, 0)
    var dir = 0
    val moveBorder = { border: Int -> borders[border] += if (border < 2) -1 else 1 }
    repeat (matrix.size * matrix[0].size) {
        if ((if (dir % 2 == 0) x else y) == borders[dir]) {
            moveBorder((dir + 3) % 4)
            dir = (dir + 1) % 4
        }
        add(matrix[y][x])
        x += dxy[(dir + 1) % 4]
        y += dxy[dir]
    }
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/207

Intuition

Just implement what is asked. We can use a loop with four directions, or try to program a robot that will rotate after it hit a wall.

Approach

  • do track the borders left, top, right, bottom
  • use single direction variable dir
  • move the wall after a robot walked parallel to it

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

8.05.2023

1572. Matrix Diagonal Sum easy


fun diagonalSum(mat: Array<IntArray>): Int =
    (0..mat.lastIndex).sumBy {
        mat[it][it] + mat[it][mat.lastIndex - it]
    }!! - if (mat.size % 2 == 0) 0 else mat[mat.size / 2][mat.size / 2]

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/206

Intuition

Just do what is asked.

Approach

  • avoid double counting of the center element

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

7.05.2023

1964. Find the Longest Valid Obstacle Course at Each Position hard


fun longestObstacleCourseAtEachPosition(obstacles: IntArray): IntArray {
    // 2 3 1 3
    // 2          2
    //   3        2 3
    //     1      1 3    (pos = 1)
    //       3    1 3 3

    // 5 2 5 4 1 1 1 5 3 1
    // 5       .             5
    //   2     .             2
    //     5   .             2 5
    //       4 .             2 4
    //         1             1 4 (pos = 1)
    //           1           1 1
    //             1         1 1 1
    //               5       1 1 1 5
    //                 3     1 1 1 3
    //                   1   1 1 1 1

    val lis = IntArray(obstacles.size)
    var end = 0
    return obstacles.map { x ->
        var pos = -1
        var lo = 0
        var hi = end - 1
        while (lo <= hi) {
            val mid = lo + (hi - lo) / 2
            if (lis[mid] > x) {
                hi = mid - 1
                pos = mid
            } else lo = mid + 1
        }
        if (pos == -1) {
            lis[end++] = x
            end
        } else {
            lis[pos] = x
            pos + 1
        }
    }.toIntArray()
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/205

Intuition

This is the Longest Increasing Subsequence length problem, that have a classic algorithm, which must be learned and understood.

The trivial case of any increasing subsequence is broken by example: 2 3 1 3, when we consider the last 3 result must be: 233 instead of 13. So, we must track all the sequences.

To track all the sequences, we can use TreeMap that will hold the largest element and length of any subsequence. Adding a new element will take \(O(n^2)\).

The optimal LIS solution is to keep the largest increasing subsequence so far and cleverly add new elements:

  1. for a new element, search for the smallest element that is larger than it
  2. if found, replace
  3. if not, append lis.gif

Approach

  • google what is the solution of LIS
  • use an array for lis
  • carefully write binary search

    Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(n)\)

6.05.2023

1498. Number of Subsequences That Satisfy the Given Sum Condition medium


fun numSubseq(nums: IntArray, target: Int): Int {
    val m = 1_000_000_007
    nums.sort()
    val cache = IntArray(nums.size + 1) { 0 }
    cache[1] = 1
    for (i in 2..nums.size) cache[i] = (2 * cache[i - 1]) % m
    var total = 0
    nums.forEachIndexed { i, n ->
        var lo = 0
        var hi = i
        var removed = cache[i + 1]
        while (lo <= hi) {
            val mid = lo + (hi - lo) / 2
            if (nums[mid] + n <= target) {
                removed = cache[i - mid]
                lo = mid + 1
            } else hi = mid - 1
        }
        total = (total + cache[i + 1] - removed) % m
    }
    if (total < 0) total += m
    return total
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/204

Intuition

  1. We can safely sort an array, because order doesn’t matter for finding max or min in a subsequence.
  2. Having increasing order gives us the pattern: image.png Ignoring the target, each new number adds previous value to the sum: \(sum_2 = sum_1 + (1 + sum_1)\), or just \(2^i\).
  3. Let’s observe the pattern of the removed items: image.png For example, target = 12, for number 8, count of excluded values is 4 = [568, 58, 68, 8]; for number 9, it is 8 = [5689, 589, 569, 59, 689, 69, 89, 9]. We can observe, it is determined by the sequence 5 6 8 9, where all the numbers are bigger, than target - 9. That is, the law for excluding the elements is the same: \(r_2 = r_1 + (1 + r_1)\), or just \(2^x\), where x - is the count of the bigger numbers.

Approach

  • Precompute the 2-powers
  • Use binary search to count how many numbers are out of the equation n_i + x <= target
  • A negative result can be converted to positive by adding the modulo 1_000_000_7

    Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(n)\)

5.05.2023

1456. Maximum Number of Vowels in a Substring of Given Length medium


fun maxVowels(s: String, k: Int): Int {
    val vowels = setOf('a', 'e', 'i', 'o', 'u')
    var count = 0
    var max = 0
    for (i in 0..s.lastIndex) {
        if (s[i] in vowels) count++
        if (i >= k && s[i - k] in vowels) count--
        if (count > max) max = count
    }
    return max
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/203

Intuition

Count vowels, increasing them on the right border and decreasing on the left of the sliding window.

Approach

  • we can use Set to check if it is a vowel
  • look at a[i - k] to detect if we must start move left border from i == k

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

4.05.2023

649. Dota2 Senate medium


fun predictPartyVictory(senate: String): String {
    val queue = ArrayDeque<Char>()
        senate.forEach { queue.add(it) }
        var banR = 0
        var banD = 0
        while (true) {
            var haveR = false
            var haveD = false
            repeat(queue.size) {
                val c = queue.poll()
                if (c == 'R') {
                    haveR = true
                    if (banR > 0) banR--
                    else {
                        queue.add(c)
                        banD++
                    }
                } else {
                    haveD = true
                    if (banD > 0) banD--
                    else {
                        queue.add(c)
                        banR++
                    }
                }
            }
            if (!haveR) return "Dire"
            if (!haveD) return "Radiant"
        }
    }

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/202

Intuition

One can ban on any length to the right. We can just simulate the process, and it will take at most two rounds.

Approach

Use Queue and count how many bans are from the Radiant and from the Dire.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

3.05.2023

2215. Find the Difference of Two Arrays easy


fun findDifference(nums1: IntArray, nums2: IntArray): List<List<Int>> = listOf(
    nums1.subtract(nums2.toSet()).toList(),
    nums2.subtract(nums1.toSet()).toList()
    )

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/201

Intuition

Just do what is asked.

Approach

One way is to use two Sets and just filter them. Another is to use intersect and distinct. Third option is to sort both of them and iterate, that will use \(O(1)\) extra memory, but \(O(nlogn)\) time.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

2.05.2023

1822. Sign of the Product of an Array easy


fun arraySign(nums: IntArray): Int = nums.fold(1) { r, t -> if (t == 0) 0 else r * (t / Math.abs(t)) }

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/199

Intuition

Do what is asked, but avoid overflow.

Approach

There is an sign function in kotlin, but leetcode.com doesn’t support it yet. We can use fold.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

1.05.2023

1491. Average Salary Excluding the Minimum and Maximum Salary easy


fun average(salary: IntArray): Double = with (salary) {
    (sum() - max()!! - min()!!) / (size - 2).toDouble()
}

or


fun average(salary: IntArray): Double = salary.sorted().drop(1).dropLast(1).average()

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/198

Intuition

Just do what is asked.

Approach

We can do .fold and iterate only once, but sum, max and min operators are less verbose. We also can sort it, that will make code even shorter.

Complexity

  • Time complexity: \(O(n)\), \(O(nlog(n))\) for sorted
  • Space complexity: \(O(1)\)

30.04.2023

1579. Remove Max Number of Edges to Keep Graph Fully Traversable hard


fun IntArray.root(a: Int): Int {
    var x = a
    while (this[x] != x) x = this[x]
    this[a] = x
    return x
}
fun IntArray.union(a: Int, b: Int): Boolean {
    val rootA = root(a)
    val rootB = root(b)
    if (rootA != rootB) this[rootB] = rootA
    return rootA != rootB
}
fun IntArray.connected(a: Int, b: Int) = root(a) == root(b)
fun maxNumEdgesToRemove(n: Int, edges: Array<IntArray>): Int {
    val uf1 = IntArray(n + 1) { it }
    val uf2 = IntArray(n + 1) { it }
    var skipped = 0
    edges.forEach { (type, a, b) ->
        if (type == 3) {
            uf1.union(a, b)
            if (!uf2.union(a, b)) skipped++
        }
    }
    edges.forEach { (type, a, b) ->
        if (type == 1 && !uf1.union(a, b)) skipped++
    }
    edges.forEach { (type, a, b) ->
        if (type == 2 && !uf2.union(a, b)) skipped++
    }
    for (i in 2..n)
    if (!uf1.connected(i - 1, i) || !uf2.connected(i - 1, i)) return -1
    return skipped
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/196

Intuition

After connecting all type 3 nodes, we can skip already connected nodes for Alice and for Bob. To detect if all the nodes are connected, we can just check if all nodes connected to one particular node.

Approach

Use separate Union-Find objects for Alice and for Bob

Complexity

  • Time complexity: \(O(n)\), as root and union operations take < 5 for any n <= Int.MAX.
  • Space complexity: \(O(n)\)

29.04.2023

1697. Checking Existence of Edge Length Limited Paths hard


fun distanceLimitedPathsExist(n: Int, edgeList: Array<IntArray>, queries: Array<IntArray>): BooleanArray {
    val uf = IntArray(n) { it }
    fun root(x: Int): Int {
        var n = x
        while (uf[n] != n) n = uf[n]
        uf[x] = n
        return n
    }
    fun union(a: Int, b: Int) {
        val rootA = root(a)
        val rootB = root(b)
        if (rootA != rootB) uf[rootB] = rootA
    }
    val indices = queries.indices.sortedWith(compareBy( { queries[it][2] } ))
    edgeList.sortWith(compareBy( { it[2] } ))
    var edgePos = 0
    val res = BooleanArray(queries.size)
    indices.forEach { ind ->
        val (qfrom, qto, maxDist) = queries[ind]
        while (edgePos < edgeList.size) {
            val (from, to, dist) = edgeList[edgePos]
            if (dist >= maxDist) break
            union(from, to)
            edgePos++
        }
        res[ind] = root(qfrom) == root(qto)
    }
    return res
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/195

Intuition

The naive approach is to do BFS for each query, obviously gives TLE as it takes \(O(n^2)\) time. Using the hint, we can use somehow the sorted order of the queries. If we connect every two nodes with dist < query.dist we have connected groups with all nodes reachable inside them. The best data structure for union and finding connected groups is the Union-Find. To avoid iterating edgeList every time, we can sort it too and take only available distances.

Approach

  • for better time complexity, compress the Union-Find path uf[x] = n
  • track the edgePos - a position in a sorted edgeList
  • make separate indices list to sort queries without losing the order

    Complexity

  • Time complexity: \(O(nlog(n))\), time complexity for root and union operations is an inverse Ackerman function and < 5 for every possible number in Int.
  • Space complexity: \(O(n)\)

28.04.2023

839. Similar String Groups hard


fun numSimilarGroups(strs: Array<String>): Int {
    fun similar(i: Int, j: Int): Boolean {
        var from = 0
        while (from < strs[i].length && strs[i][from] == strs[j][from]) from++
        var to = strs[i].lastIndex
        while (to >= 0 && strs[i][to] == strs[j][to]) to--
        for (x in from + 1..to - 1)
        if (strs[i][x] != strs[j][x]) return false
        return true
    }
    val uf = IntArray(strs.size) { it }
    fun root(x: Int): Int {
        var n = x
        while (uf[n] != n) n = uf[n]
        uf[x] = n
        return n
    }
    var groups = strs.size
    fun union(a: Int, b: Int) {
        val rootA = root(a)
        val rootB = root(b)
        if (rootA != rootB) {
            groups--
            uf[rootB] = rootA
        }
    }
    for (i in 0..strs.lastIndex)
    for (j in i + 1..strs.lastIndex)
    if (similar(i, j)) union(i, j)
    return groups
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/194

Intuition

For tracking the groups, Union-Find is a good start. Next, we need to compare the similarity of each to each word, that is \(O(n^2)\). For the similarity, we need a linear algorithm. Let’s divide the words into three parts: prefix+a+body+b+suffix. Two words are similar if their prefix, suffix and body are similar, leaving the only different letters a and b.

Approach

  • decrease the groups when the two groups are joined together
  • shorten the Union-Find root’s path uf[x] = n
  • more complex Union-Find algorithm with ranks give the optimal time of \(O(lg*n)\), where lg*n is the inverse Ackerman function. It is inverse of the f(n) = 2^2^2^2..n times.

    Complexity

  • Time complexity: \(O(n^2a(n))\)
  • Space complexity: \(O(n)\)

27.04.2023

319. Bulb Switcher medium


fun bulbSwitch(n: Int): Int {
    if (n <= 1) return n
    var count = 1
    var interval = 3
    var x = 1
    while (x + interval <= n) {
        x = x + interval
        interval += 2
        count++
    }
    return count
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/193

Intuition

Let’s draw a diagram and see if any pattern here:


//      1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
//
// 1    1 1 1 1 1 1 1 1 1  1 1  1  1  1  1  1  1  1  1
// 2      0 . 0 . 0 . 0 .  0 .  0  .  0  .  0  .  0  .
// 3        0 . . 1 . . 0  . .  1  .  .  0  .  .  1  .
// 4          1 . . . 1 .  . .  0  .  .  .  1  .  .  .
// 5            0 . . . .  1 .  .  .  .  1  .  .  .  .
// 6              0 . . .  . .  1  .  .  .  .  .  0  .
// 7                0 . .  . .  .  .  1  .  .  .  .  .
// 8                  0 .  . .  .  .  .  .  0  .  .  .
// 9                    1  . .  .  .  .  .  .  .  1  .
// 10                      0 .  .  .  .  .  .  .  .  .
// 11                        0  .  .  .  .  .  .  .  .
// 12                           0  .  .  .  .  .  .  .
// 13                              0  .  .  .  .  .  .
// 14                                 0  .  .  .  .  .
// 15                                    0  .  .  .  .
// 16                                       1  .  .  .
// 17                                          0  .  .
// 18                                             0  .
// 19                                                0

One rule is: number of switches for each new value is a number of divisors. Another rule: we can reuse the previous result. However, those rules didn’t help much, let’s observe another pattern: diagonal sequence have increasing intervals of zeros by 2

Approach

Use found law and write the code.

Complexity

  • Time complexity: That is tricky, let’s derive it: \(n = 1 + 2 + (1+2+2) + (1+2+2+2) + (...) + (1+2k)\), or \(n = \sum_{i=0}^{k}1+2i = k(1 + 2 + 1 + 2k)/2\), then count of elements in arithmetic progression k is: \(O(k) = O(\sqrt{n})\), which is our time complexity.
  • Space complexity: \(O(1)\)

26.04.2023

258. Add Digits easy


fun addDigits(num: Int): Int = if (num == 0) 0 else 1 + ((num - 1) % 9)
// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
// 0 1 2 3 4 5 6 7 8 9 1  2  3  4  5  6  7  8  9  1  2  3  4  5  6  7  8  9  1  2  3  4  5  6  7  8  9  1  2
// 0 [1..9] [1..9] [1..9] ...

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/192

Intuition

Observing the pattern:


// 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
// 0 1 2 3 4 5 6 7 8 9 1  2  3  4  5  6  7  8  9  1  2  3  4  5  6  7  8  9  1  2  3  4  5  6  7  8  9  1  2
// 0 [1..9] [1..9] [1..9] ...

There is a repeating part of it: [1..9], so we can derive the formula.

Approach

It is just an array pointer loop shifted by 1.

Complexity

  • Time complexity: \(O(1)\)
  • Space complexity: \(O(1)\)

25.04.2023

2336. Smallest Number in Infinite Set medium


class SmallestInfiniteSet() {
    val links = IntArray(1001) { it + 1 }

    fun popSmallest(): Int {
        val smallest = links[0]
        val next = links[smallest]
        links[smallest] = 0
        links[0] = next
        return smallest
    }

    fun addBack(num: Int) {
        if (links[num] == 0) {
            var maxLink = 0
            while (links[maxLink] <= num) maxLink = links[maxLink]
            val next = links[maxLink]
            links[maxLink] = num
            links[num] = next
        }
    }

}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/191

Intuition

Given the constraints, we can hold every element as a link node to another in an Array. This will give us \(O(1)\) time for pop operation, but \(O(n)\) for addBack in the worst case. A more asymptotically optimal solution, is to use a TreeSet and a single pointer to the largest popped element.

Approach

Let’s implement a sparse array.

Complexity
  • Time complexity: \(O(1)\) - for pop \(O(n)\) - constructor and addBack
  • Space complexity: \(O(n)\)

24.04.2023

1046. Last Stone Weight easy


fun lastStoneWeight(stones: IntArray): Int =
with(PriorityQueue<Int>(compareByDescending { it } )) {
    stones.forEach { add(it) }
    while (size > 1) add(poll() - poll())
    if (isEmpty()) 0 else peek()
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/190

Intuition

Just run the simulation.

Approach

  • use PriorityQueue with compareByDescending

    Complexity

  • Time complexity: \(O(nlog(n))\)
  • Space complexity: \(O(n)\)

23.04.2023

1416. Restore The Array hard


fun numberOfArrays(s: String, k: Int): Int {
    // 131,7  k=1000
    // 1317 > 1000
    // 20001  k=2000
    // 2      count=1
    //  000   count=1, curr=2000
    //     1  count++, curr=1
    //
    // 220001 k=2000
    // 2      count=1 curr=1
    // 22     count+1=2 curr=22          [2, 2], [22]
    // 220    curr=220                   [2, 20], [220]
    // 2200   curr=2200 > 2000, curr=200 [2, 200], [2200]
    // 22000  curr=2000   count=1        [2, 2000]
    // 220001 count+1=3 curr=20001 > 2000, curr=1  [2, 2000, 1], []
    val m = 1_000_000_007L
    val cache = LongArray(s.length) { -1L }
    fun dfs(curr: Int): Long {
        if (curr == s.length) return 1L
        if (s[curr] == '0') return 0L
        if (cache[curr] != -1L) return cache[curr]
        var count = 0L
        var num = 0L
        for (i in curr..s.lastIndex) {
            val d = s[i].toLong() - '0'.toLong()
            num = num * 10L + d
            if (num > k) break
            val countOther = dfs(i + 1)
            count = (count + countOther) % m
        }
        cache[curr] = count
        return count
    }
    return dfs(0).toInt()
}

or bottom-up

fun numberOfArrays(s: String, k: Int): Int {
    val cache = LongArray(s.length)
    for (curr in s.lastIndex downTo 0) {
        if (s[curr] == '0') continue
        var count = 0L
        var num = 0L
        for (i in curr..s.lastIndex) {
            num = num * 10L + s[i].toLong() - '0'.toLong()
            if (num > k) break
            val next = if (i == s.lastIndex) 1 else cache[i + 1]
            count = (count + next) % 1_000_000_007L
        }
        cache[curr] = count
    }
    return cache[0].toInt()
}

memory optimization:

fun numberOfArrays(s: String, k: Int): Int {
    val cache = LongArray(k.toString().length + 1)
    for (curr in s.lastIndex downTo 0) {
        System.arraycopy(cache, 0, cache, 1, cache.size - 1)
        if (s[curr] == '0') {
            cache[0] = 0
            continue
        }

        var count = 0L
        var num = 0L
        for (i in curr..s.lastIndex) {
            num = num * 10L + s[i].toLong() - '0'.toLong()
            if (num > k) break
            val next = if (i == s.lastIndex) 1 else cache[i - curr + 1]
            count = (count + next) % 1_000_000_007L
        }
        cache[0] = count
    }
    return cache[0].toInt()
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/189

Intuition

One naive solution, is to find all the possible ways of splitting the string, and calculating soFar number, gives TLE as we must take soFar into consideration when memoizing the result. Let’s consider, that for every position in s there is only one number of possible arrays. Given that, we can start from each position and try to take the first number in all possible correct ways, such that num < k. Now, we can cache this result for reuse.

Approach

  • use Long to avoid overflow
  • we actually not need all the numbers in cache, just the \(lg(k)\) for the max length of the number

    Complexity

  • Time complexity: \(O(nlg(k))\)
  • Space complexity: \(O(lg(k))\)

22.04.2023

1312. Minimum Insertion Steps to Make a String Palindrome hard


fun minInsertions(s: String): Int {
    // abb -> abba
    // abb*
    //  ab -> aba / bab
    // *ab
    //  bba -> abba
    // *bba
    //   bbbaa -> aabbbaa
    // **bbbaa
    //    bbbcaa -> aacbbbcaa
    // ***bbbcaa
    // leetcode ->  leetcodocteel
    // leetcod***e**
    //      o -> 0
    //      od -> dod / dod -> 1
    //     cod -> codoc / docod -> 2
    //     code -> codedoc / edocode -> 2+1=3
    //    tcod -> tcodoct / doctcod -> 2+1=3
    //    tcode -> tcodedoct / edoctcode -> 3+1=4
    //   etcode = e{tcod}e -> e{tcodoct / doctcod}e -> 3
    //   etcod -> 1+{tcod} -> 1+3=4
    //  eetcod -> docteetcod 4 ?/ eetcodoctee 5
    //  eetcode -> edocteetcode 5 / eetcodedoctee 6 -> e{etcod}e 4 = e{etcodocte}e
    // leetcod -> 1+{eetcod} -> 5
    // leetcode -> 1+{eetcode} 1+4=5
    // aboba
    // a -> 0
    // ab -> 1
    // abo -> min({ab}+1, 1+{bo}) =2
    // abob -> min(1+{bob}, {abo} +1)=1
    // aboba -> min(0 + {bob}, 1+{abob}, 1+{boba}) = 0
    val cache = mutableMapOf<Pair<Int, Int>, Int>()
    fun dfs(from: Int, to: Int): Int {
        if (from > to || from < 0 || to > s.lastIndex) return -1
        if (from == to) return 0
        if (from + 1 == to) return if (s[from] == s[to]) 0 else 1
        return cache.getOrPut(from to to) {
            if (s[from] == s[to]) return@getOrPut dfs(from + 1, to - 1)
            val one = dfs(from + 1, to)
            val two = dfs(from, to - 1)
            when {
                one != -1 && two != -1 -> 1 + minOf(one, two)
                one != -1 -> 1 + one
                two != -1 -> 1 + two
                else -> -1
            }
        }
    }
    return dfs(0, s.lastIndex)
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/188

Intuition

Let’s add chars one by one. Single char is a palindrome. Two chars are palindrome if they are equal, and not if not: \(insertions_{ab} =\begin{cases} 0, & \text{if a==b}\\ 1 \end{cases}\). While adding a new character, we choose the minimum insertions. For example, aboba:


// aboba
// a -> 0
// ab -> 1
// abo -> min({ab}+1, 1+{bo}) =2
// abob -> min(1+{bob}, {abo} +1)=1
// aboba -> min(0 + {bob}, 1+{abob}, 1+{boba}) = 0

So, the DP equation is the following \(dp_{i,j} = min(0 + dp_{i+1, j-1}, 1 + dp_{i+1, j}, 1 + dp_{i, j-1}\), where DP - is the minimum number of insertions.

Approach

Just DFS and cache.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

21.04.2023

879. Profitable Schemes hard


fun profitableSchemes(n: Int, minProfit: Int, group: IntArray, profit: IntArray): Int {
    val cache = Array(group.size) { Array(n + 1) { Array(minProfit + 1) { -1 } } }
    fun dfs(curr: Int, guys: Int, cashIn: Int): Int {
        if (guys < 0) return 0
        val cash = minOf(cashIn, minProfit)
        if (curr == group.size) return if (cash == minProfit) 1 else 0
        with(cache[curr][guys][cash]) { if (this != -1) return@dfs this }
        val notTake = dfs(curr + 1, guys, cash)
        val take = dfs(curr + 1, guys - group[curr], cash + profit[curr])
        val res = (notTake + take) % 1_000_000_007
        cache[curr][guys][cash] = res
        return res
    }
    return dfs(0, n, 0)
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/187

Intuition

For every new item, j we can decide to take it or not take it. Given the inputs of how many guys we have and how much cash already earned, the result is always the same: \(count_j = notTake_j(cash, guys) + take_j(cash + profit[j], guys - group[j])\)

Approach

Do DFS and cache result in an array.

Complexity

  • Time complexity: \(O(n^3)\)
  • Space complexity: \(O(n^3)\)

20.04.2023

662. Maximum Width of Binary Tree medium


fun widthOfBinaryTree(root: TreeNode?): Int =
with(ArrayDeque<Pair<Int, TreeNode>>()) {
    root?.let { add(0 to it) }
    var width = 0
    while (isNotEmpty()) {
        var first = peek()
        var last = last()
        width = maxOf(width, last.first - first.first + 1)
        repeat(size) {
            val (x, node) = poll()
            node.left?.let { add(2 * x + 1 to it) }
            node.right?.let { add(2 * x + 2 to it) }
        }
    }
    width
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/186

Intuition

For every node, positions of it’s left child is \(2x +1\) and right is \(2x + 2\) leetcode_tree.gif

Approach

We can do BFS and track node positions.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

19.04.2023

1372. Longest ZigZag Path in a Binary Tree medium


fun longestZigZag(root: TreeNode?): Int {
    var max = 0
    fun dfs(n: TreeNode?, len: Int, dir: Int) {
        max = maxOf(max, len)
        if (n == null) return@dfs
        when (dir) {
            0 -> {
                dfs(n?.left, 0, -1)
                dfs(n?.right, 0, 1)
            }
            1 -> {
                dfs(n?.left, len + 1, -1)
                dfs(n?.right, 0, 1)
            }
            -1 -> {
                dfs(n?.right, len + 1, 1)
                dfs(n?.left, 0, -1)
            }
        }
    }
    dfs(root, 0, 0)
    return max
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/185

Intuition

Search all the possibilities with DFS

Approach

Compute the max as you go

Complexity

  • Time complexity: \(O(nlog_2(n))\), for each level of height we traverse the full tree
  • Space complexity: \(O(log_2(n))\)

18.04.2023

1768. Merge Strings Alternately easy


fun mergeAlternately(word1: String, word2: String): String =
(word1.asSequence().zip(word2.asSequence()) { a, b -> "$a$b" } +
word1.drop(word2.length) + word2.drop(word1.length))
.joinToString("")

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/184

Intuition

Do what is asked. Handle the tail.

Approach

  • we can use sequence zip operator
  • for the tail, consider drop

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

17.04.2023

1431. Kids With the Greatest Number of Candies easy


fun kidsWithCandies(candies: IntArray, extraCandies: Int): List<Boolean> =
    candies.max()?.let { max ->
        candies.map { it + extraCandies >= max}
    } ?: listOf()

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/183

Intuition

We can just find the maximum and then try to add extra to every kid and check

Approach

Let’s write the code

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

16.04.2023

1639. Number of Ways to Form a Target String Given a Dictionary hard


fun numWays(words: Array<String>, target: String): Int {
    val freq = Array(words[0].length) { LongArray(26) }
    for (i in 0..words[0].lastIndex)
    words.forEach { freq[i][it[i].toInt() - 'a'.toInt()]++ }

    val cache = Array(words[0].length) { LongArray(target.length) { -1L } }
    val m = 1_000_000_007L

    fun dfs(wpos: Int, tpos: Int): Long {
        if (tpos == target.length) return 1L
        if (wpos == words[0].length) return 0L
        if (cache[wpos][tpos] != -1L) return cache[wpos][tpos]
        val curr = target[tpos].toInt() - 'a'.toInt()
        val currFreq = freq[wpos][curr]
        val take = if (currFreq == 0L) 0L else
        dfs(wpos + 1, tpos + 1)
        val notTake = dfs(wpos + 1, tpos)
        val mul = (currFreq * take) % m
        val res = (mul + notTake) % m
        cache[wpos][tpos] = res
        return res
    }
    return dfs(0, 0).toInt()
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/182

Intuition

Consider an example: bbc aaa ccc, target = ac. We have 5 ways to form the ac:


// bbc aaa ccc   ac
//     a    c
//     a     c
//   c a
//      a    c
//   c  a

Looking at this, we deduce, that only count of every character at every position matter.


// 0 -> 1b 1a 1c
// 1 -> 1b 1a 1c
// 2 ->    1a 2c

To form ac we can start from position 0 or from 1. If we start at 0, we have one c at 1 plus two c at 2. And if we start at 1 we have two c at 3. \(DP_{i,j} = Freq * DP_{i + 1, j + 1} + DP_{i + 1, j}\)

Approach

  • precompute the freq array - count of each character at each position
  • use an Array for faster cache
  • use long to avoid overflow

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

15.04.2023

2218. Maximum Value of K Coins From Piles hard


fun maxValueOfCoins(piles: List<List<Int>>, k: Int): Int {
    val cache = Array(piles.size) { mutableListOf<Long>() }

        fun dfs(pile: Int, taken: Int): Long {
            if (taken >= k || pile >= piles.size) return 0
            if (cache[pile].size > taken) return cache[pile][taken]
            var max = dfs(pile + 1, taken)
            var sum = 0L
            for (j in 0..piles[pile].lastIndex) {
                val newTaken = taken + j + 1
                if (newTaken > k) break
                sum += piles[pile][j]
                max = maxOf(max, sum + dfs(pile + 1, newTaken))
            }
            cache[pile].add(max)
            return max
        }

        return dfs(0, 0).toInt()
    }

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/181

Intuition

Given the current pile, we can assume there is an optimal maximum value of the piles to the right of the current for every given number of k. leetcode_daily_backtrack.gif

Approach

We can cache the result by the keys of every pile to taken

Complexity

  • Time complexity: \(O(kn^2)\)
  • Space complexity: \(O(kn^2)\)

14.04.2023

516. Longest Palindromic Subsequence medium


fun longestPalindromeSubseq(s: String): Int {
    // b + abcaba
    // b + ab_ab_
    // b + a_cab_
    // acbbc + a -> [acbbc]a x[from]==x[to]?1 + p[from+1][to-1]
    val p = Array(s.length) { Array(s.length) { 0 } }
    for (i in s.lastIndex downTo 0) p[i][i] = 1
    for (from in s.lastIndex - 1 downTo 0)
    for (to in from + 1..s.lastIndex)
    p[from][to] = if (s[from] == s[to]) {
        2 + if (to == from + 1) 0 else p[from + 1][to - 1]
    } else {
        maxOf(p[from][to - 1], p[from + 1][to])
    }
    return p[0][s.lastIndex]
}

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/180

Intuition

Simple DFS would not work as it will take \(O(2^n)\) steps. Consider the sequence: acbbc and a new element a. The already existing largest palindrome is cbbc. When adding a new element, we do not care about what is inside between a..a, just the largest value of it. So, there is a DP equation derived from this observation: \(p[i][j] = eq ? 2 + p[i+1][j-1] : max(p[i][j-1], p[i+1][j])\).

Approach

For cleaner code:

  • precompute p[i][i] = 1
  • exclude 0 and lastIndex from iteration
  • start with to = from + 1

    Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

13.04.2023

946. Validate Stack Sequences medium


fun validateStackSequences(pushed: IntArray, popped: IntArray): Boolean =
with(Stack<Int>()) {
    var pop = 0
    pushed.forEach {
        push(it)
        while (isNotEmpty() && peek() == popped[pop]) {
            pop()
            pop++
        }
    }
    isEmpty()
}

blog post substack

Telegram

https://t.me/leetcode_daily_unstoppable/179

Intuition

Do simulation using a Stack.

Approach

  • use one iteration and a second pointer for pop
  • empty the stack after inserting an element

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

12.04.2023

71. Simplify Path medium


fun simplifyPath(path: String): String =
"/" + Stack<String>().apply {
    path.split("/").forEach {
        when (it) {
            ".." -> if (isNotEmpty()) pop()
            "." -> Unit
            "" -> Unit
            else -> push(it)
        }
    }
}.joinToString("/")

blog post substack

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/178

Intuition

We can simulate what each of the . and .. commands do by using a Stack.

Approach

  • split the string by /
  • add elements to the Stack if they are not commands and not empty

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

11.04.2023

2390. Removing Stars From a String medium


fun removeStars(s: String): String = StringBuilder().apply {
    s.forEach {
        if (it == '*') setLength(length - 1)
        else append(it)
    }
}.toString()

blog post

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/177

Intuition

Iterate over a string. When * symbol met, remove last character, otherwise add it.

Approach

  • we can use a Stack, or just StringBuilder

    Complexity

  • Time complexity:
\[O(n)\]
  • Space complexity:
\[O(n)\]

10.04.2023

20. Valid Parentheses medium


fun isValid(s: String): Boolean = with(Stack<Char>()) {
    val opened = hashSetOf('(', '[', '{')
    val match = hashMapOf(')' to '(' , ']' to '[', '}' to '{')
    !s.any { c ->
        when {
            c in opened -> false.also { push(c) }
            isEmpty() -> true
            else -> pop() != match[c]
        }
    } && isEmpty()
}

blog post

Join me on Telegram

telegram

Intuition

Walk the string and push brackets to the stack. When bracket is closing, pop from it.

Approach

  • use HashMap to check matching bracket.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

09.04.2023

1857. Largest Color Value in a Directed Graph hard

blog post


fun largestPathValue(colors: String, edges: Array<IntArray>): Int {
    if (edges.isEmpty()) return if (colors.isNotEmpty()) 1 else 0
    val fromTo = mutableMapOf<Int, MutableList<Int>>()
        edges.forEach { (from, to) -> fromTo.getOrPut(from) { mutableListOf() } += to }
        val cache = mutableMapOf<Int, IntArray>()
        var haveCycle = false
        fun dfs(curr: Int, visited: HashSet<Int> = HashSet()): IntArray {
            return cache.getOrPut(curr) {
                val freq = IntArray(26)
                if (visited.add(curr)) {
                    fromTo.remove(curr)?.forEach {
                        val childFreq = dfs(it, visited)
                        for (i in 0..25) freq[i] = maxOf(childFreq[i], freq[i])
                    }
                    freq[colors[curr].toInt() - 'a'.toInt()] += 1
                } else haveCycle = true
                freq
            }
        }
        var max = 0
        edges.forEach { (from, to) -> max = maxOf(max, dfs(from).max()!!) }
        return if (haveCycle) -1 else max
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/175

Intuition

image.png leetcode_daily_small.gif

For each node, there is only one answer of the maximum count of the same color. For each parent, \(c_p = max(freq_{child})+colors[curr]\). We can cache the result and compute it using DFS and selecting maximum count from all the children.

Approach

  • use visited set to detect cycles

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

08.04.2023

133. Clone Graph medium

blog post


fun cloneGraph(node: Node?): Node? {
    if (node == null) return null
    val oldToNew = mutableMapOf<Node, Node>()
    fun dfs(n: Node) {
        if (oldToNew[n] == null) {
            oldToNew[n] = Node(n.`val`)
            n.neighbors.forEach {
                if (it != null) dfs(it)
            }
        }
    }
    fun dfs2(n: Node) {
        oldToNew[n]!!.apply {
            if (neighbors.isEmpty()) {
                n.neighbors.forEach {
                    if (it != null) {
                        neighbors.add(oldToNew[it])
                        dfs2(it)
                    }
                }
            }
        }
    }
    dfs(node)
    dfs2(node)
    return oldToNew[node]
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/174

Intuition

We can map every old node to its new node. Then one DFS for the creation, another for the linking.

Approach

  • we can avoid using visited set by checking if a new node already has filled its neighbors.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

07.04.2023

1020. Number of Enclaves medium

blog post


fun numEnclaves(grid: Array<IntArray>): Int {
    val visited = HashSet<Pair<Int, Int>>()
    fun dfs(x: Int, y: Int): Int {
        return if (x < 0 || y < 0 || x > grid[0].lastIndex || y > grid.lastIndex) 0
        else if (grid[y][x] == 1 && visited.add(x to y))
        1 + dfs(x - 1, y) + dfs(x + 1, y) + dfs(x, y - 1) + dfs(x, y + 1)
        else 0
    }
    for (y in 0..grid.lastIndex) {
        dfs(0, y)
        dfs(grid[0].lastIndex, y)
    }
    for (x in 0..grid[0].lastIndex) {
        dfs(x, 0)
        dfs(x, grid.lastIndex)
    }
    var count = 0
    for (y in 0..grid.lastIndex)
    for(x in 0..grid[0].lastIndex)
    count += dfs(x, y)
    return count
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/173

Intuition

Walk count all the 1 cells using DFS and a visited set.

Approach

We can use visited set, or modify the grid or use Union-Find. To exclude the borders, we can visit them first with DFS.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

06.04.2023

1254. Number of Closed Islands medium

blog post


fun closedIsland(grid: Array<IntArray>): Int {
    val visited = HashSet<Pair<Int, Int>>()
    val seen = HashSet<Pair<Int, Int>>()

    fun dfs(x: Int, y: Int): Boolean {
        seen.add(x to y)
        if (x >= 0 && y >= 0 && x < grid[0].size && y < grid.size
        && grid[y][x] == 0 &&  visited.add(x to y)) {
            var isBorder = x == 0 || y == 0 || x == grid[0].lastIndex || y == grid.lastIndex
            isBorder = dfs(x - 1, y) || isBorder
            isBorder = dfs(x, y - 1) || isBorder
            isBorder = dfs(x + 1, y) || isBorder
            isBorder = dfs(x, y + 1) || isBorder
            return isBorder
        }
        return false
    }

    var count = 0
    for (y in 0..grid.lastIndex)
    for (x in 0..grid[0].lastIndex)
    if (grid[y][x] == 0 && seen.add(x to y) && !dfs(x, y)) count++
    return count
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/172

Intuition

Use hint #1, if we don’t count islands on the borders, we get the result. Now, just count all connected 0 cells that didn’t connect to the borders. We can use DFS or Union-Find.

Approach

DFS will solve the problem.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

05.04.2023

2439. Minimize Maximum of Array medium

blog post


fun minimizeArrayValue(nums: IntArray): Int {
    // 5 4 3 2 1 -> 5
    // 1 2 3 4 5 -> 3
    // 1 2 3 6 3
    // 1 2 6 3 3
    // 1 5 3 3 3
    // 3 3 3 3 3
    fun canArrangeTo(x: Long): Boolean {
        var diff = 0L
        for (i in nums.lastIndex downTo 0)
        diff = maxOf(0L, nums[i].toLong() - x + diff)
        return diff == 0L
    }
    var lo = 0
    var hi = Int.MAX_VALUE
    var min = Int.MAX_VALUE
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (canArrangeTo(mid.toLong())) {
            min = minOf(min, mid)
            hi = mid - 1
        } else lo = mid + 1
    }
    return min
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/171

Intuition

Observing the pattern, we can see, that any number from the end can be passed to the start of the array. One idea is to use two pointers, one pointing to the biggest value, another to the smallest. Given that biggest and smallest values changes, it will take \(O(nlog_2(n))\) time to maintain such sorted structure. Another idea, is that for any given maximum value we can walk an array from the end to the start and change values to be no bigger than it. This operation takes \(O(n)\) time, and with the growth of the maximum value also increases a possibility to comply for all the elements. So, we can binary search in that space.

Approach

  • careful with integers overflows
  • for more robust binary search code:
    • check the final condition lo == hi
    • use inclusive lo and hi
    • always check the resulting value min = minOf(min, mid)
    • always move the borders mid + 1 and mid - 1

      Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(1)\)

04.04.2023

2405. Optimal Partition of String medium

blog post


    var mask = 0
    fun partitionString(s: String): Int = 1 + s.count {
        val bit = 1 shl (it.toInt() - 'a'.toInt())
        (mask and bit != 0).also {
            if (it) mask = 0
            mask = mask or bit
        }
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/170

Intuition

Expand all the intervals until they met a duplicate character. This will be the optimal solution, as the minimum of the intervals correlates with the maximum of each interval length.

Approach

  • use hashset, [26] array or simple 32-bit mask to store visited flags for character

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

03.04.2023

881. Boats to Save People medium

blog post


fun numRescueBoats(people: IntArray, limit: Int): Int {
    people.sort()
    var count = 0
    var lo = 0
    var hi = people.lastIndex
    while (lo <= hi) {
        if (lo < hi && people[hi] + people[lo] <= limit) lo++
        hi--
        count++
    }
    return count
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/169

Intuition

The optimal strategy comes from an intuition: for each people[hi] of a maximum weight, we can or can not add the one man people[lo] of a minimum weight.

Approach

Sort an array and move two pointers lo and hi.

  • Careful with the ending condition, lo == hi

    Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(1)\)

02.04.2023

2300. Successful Pairs of Spells and Potions medium

blog post


fun successfulPairs(spells: IntArray, potions: IntArray, success: Long): IntArray {
    potions.sort()
    return IntArray(spells.size) { ind ->
        var lo = 0
        var hi = potions.lastIndex
        var minInd = potions.size
        while (lo <= hi) {
            val mid = lo + (hi - lo) / 2
            if (potions[mid].toLong() * spells[ind].toLong() >= success) {
                minInd = minOf(minInd, mid)
                hi = mid - 1
            } else lo = mid + 1
        }
        potions.size - minInd
    }
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/168

Intuition

If we sort potions, we can find the lowest possible value of spell[i]*potion[i] that is >= success. All other values are bigger by the math multiplication property.

Approach

  • sort potions
  • binary search the lowest index
  • use long to solve the integer overflow
    For more robust binary search code:
  • use inclusive lo and hi
  • do the last check lo == hi
  • always compute the result minInd
  • always move the lo and the hi
  • safely compute mid to not overflow

    Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(n)\)

01.04.2023

704. Binary Search easy

blog post


fun search(nums: IntArray, target: Int): Int {
    var lo = 0
    var hi = nums.lastIndex
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        if (nums[mid] == target) return mid
        if (nums[mid] < target) lo = mid + 1
        else hi = mid - 1
    }
    return -1
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/167

Intuition

Just write binary search.

Approach

For more robust code:

  • use including ranges lo..hi
  • check the last condition lo == hi
  • always check the exit condition == target
  • compute mid without the integer overflow
  • always move the boundary mid + or mid - 1
  • check yourself where to move the boundary, imagine moving closer to the target

    Complexity

  • Time complexity: \(O(log_2(n))\)
  • Space complexity: \(O(1)\)

31.03.2023

1444. Number of Ways of Cutting a Pizza hard

blog post


data class Key(val x: Int, val y: Int, val c: Int)
fun ways(pizza: Array<String>, k: Int): Int {
    val havePizza = Array(pizza.size) { Array<Int>(pizza[0].length) { 0 } }

        val lastX = pizza[0].lastIndex
        val lastY = pizza.lastIndex
        for (y in lastY downTo 0) {
            var sumX = 0
            for (x in lastX downTo 0) {
                sumX += if (pizza[y][x] == 'A') 1 else 0
                havePizza[y][x] = sumX + (if (y == lastY) 0 else havePizza[y + 1][x])
            }
        }

        val cache = mutableMapOf<Key, Int>()
        fun dfs(x: Int, y: Int, c: Int): Int {
            return cache.getOrPut(Key(x, y, c)) {
                if (c == 0) return@getOrPut if (havePizza[y][x] > 0) 1 else 0
                var res = 0
                for (xx in x + 1..lastX) if (havePizza[y][x] > havePizza[y][xx])
                res = (res + dfs(xx, y, c - 1)) % 1_000_000_007

                for (yy in y + 1..lastY) if (havePizza[y][x] > havePizza[yy][x])
                res = (res + dfs(x, yy, c - 1)) % 1_000_000_007

                return@getOrPut res
            }
        }
        return dfs(0, 0, k - 1)
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/165

Intuition

The tricky problem is to how to program a number of cuts. We can do the horizontal and vertical cuts decreasing available number k and tracking if we have any apples before and any apples after the cut. To track this, we can precompute a prefix sum of the apples, by each top-left corner to the end of the pizza. The stopping condition of the DFS is if we used all available cuts.

Approach

  • carefully precompute prefix sum. You move by row, increasing sumX, then you move by column and reuse the result of the previous row.
  • to detect if there are any apples above or to the left, compare the total number of apples precomputed from the start of the given x,y in the arguments and from the other side of the cut xx,y or x, yy.

    Complexity

  • Time complexity: \(O(mnk(m+n))\), mnk - number of cached states, (m+n) - search in each DFS step
  • Space complexity: \(O(mnk)\)

30.03.2023

87. Scramble String hard

blog post


data class Key(val afrom: Int, val ato: Int, val bfrom: Int, val bto: Int)
fun isScramble(a: String, b: String): Boolean {
    val dp = HashMap<Key, Boolean>()
    fun dfs(key: Key): Boolean {
        return dp.getOrPut(key) {
            val (afrom, ato, bfrom, bto) = key
            val alength = ato - afrom
            val blength = bto - bfrom
            if (alength != blength) return@getOrPut false
            var same = true
            for (i in 0..alength)
            if (a[afrom + i] != b[bfrom + i]) same = false
            if (same) return@getOrPut true
            for (i in afrom..ato - 1) {
                if (dfs(Key(afrom, i, bfrom, bfrom + (i - afrom)))
                && dfs(Key(i + 1, ato, bfrom + (i - afrom) + 1, bto))) return@getOrPut true
                if (dfs(Key(afrom, i, bto - (i - afrom), bto))
                && dfs(Key(i + 1, ato, bfrom, bto - (i - afrom) - 1))) return@getOrPut true
            }

            return@getOrPut false
        }
    }
    return dfs(Key(0, a.lastIndex, 0, b.lastIndex))
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/164

Intuition

This is not a permutation’s problem, as there are examples when we can’t scramble two strings consisting of the same characters. We can simulate the process and search the result using DFS.

Approach

A simple approach is to concatenate strings, but in Kotlin it gives TLE, so we need bottom up approach, or just operate with indices.

  • use including indices ranges
  • in Kotlin, don’t forget @getOrPut when exiting lambda

    Complexity

  • Time complexity: \(O(n^4)\)
  • Space complexity: \(O(n^4)\)

29.03.2023

1402. Reducing Dishes hard

blog post


fun maxSatisfaction(satisfaction: IntArray): Int {
    satisfaction.sort()
    var max = 0
    var curr = 0
    var diff = 0
    for (i in satisfaction.lastIndex downTo 0) {
        diff += satisfaction[i]
        curr += diff
        max = maxOf(max, curr)
    }

    return max
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/163

Intuition

Looking at the problem data examples, we intuitively deduce that the larger the number, the further it goes. We need to sort the array. With the negative numbers, we must compare all the results, excluding array prefixes.

Approach

The naive \(O(n^2)\) solution will work. However, there is an optimal one if we simply go from the end.

Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(n)\)

28.03.2023

983. Minimum Cost For Tickets medium

blog post


fun mincostTickets(days: IntArray, costs: IntArray): Int {
    val cache = IntArray(days.size) { -1 }
    fun dfs(day: Int): Int {
        if (day >= days.size) return 0
        if (cache[day] != -1) return cache[day]
        var next = day
        while (next < days.size && days[next] - days[day] < 1) next++
        val costOne = costs[0] + dfs(next)
        while (next < days.size && days[next] - days[day] < 7) next++
        val costSeven = costs[1] + dfs(next)
        while (next < days.size && days[next] - days[day] < 30) next++
        val costThirty = costs[2] + dfs(next)
        return minOf(costOne, costSeven, costThirty).also { cache[day] = it}
    }
    return dfs(0)
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/162

Intuition

For each day we can choose between tickets. Explore all of them and then choose minimum of the cost.

Approach

Let’s write DFS with memoization algorithm as it is simple to understand.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

27.03.2023

64. Minimum Path Sum medium

blog post


    fun minPathSum(grid: Array<IntArray>): Int {
        val cache = mutableMapOf<Pair<Int, Int>, Int>()
        fun dfs(xy: Pair<Int, Int>): Int {
        return cache.getOrPut(xy) {
            val (x, y) = xy
            val curr = grid[y][x]
            if (x == grid[0].lastIndex && y == grid.lastIndex) curr else
            minOf(
            if (x < grid[0].lastIndex) curr + dfs((x + 1) to y)
            else Int.MAX_VALUE,
            if (y < grid.lastIndex) curr + dfs(x to (y + 1))
            else Int.MAX_VALUE
            )
        }
    }
    return dfs(0 to 0)
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/161

Intuition

On each cell of the grid, there is only one minimum path sum. So, we can memorize it. Or we can use a bottom up DP approach.

Approach

Use DFS + memo, careful with the ending condition.

Complexity

  • Time complexity: \(O(n^2)\), where \(n\) - matrix size
  • Space complexity: \(O(n^2)\)

26.03.2023

2360. Longest Cycle in a Graph hard

blog post


    fun longestCycle(edges: IntArray): Int {
        var maxLen = -1
        fun checkCycle(node: Int) {
            var x = node
            var len = 0
            do {
                if (x != edges[x]) len++
                x = edges[x]
            } while (x != node)
            if (len > maxLen) maxLen = len
        }

        val visited = HashSet<Int>()
        fun dfs(curr: Int, currPath: HashSet<Int>) {
            val isCurrentLoop = !currPath.add(curr)
            if (curr != -1 && !isCurrentLoop && visited.add(curr)) {
                dfs(edges[curr], currPath)
            } else if (curr != -1 && isCurrentLoop) checkCycle(curr)
        }
        for (i in 0..edges.lastIndex) dfs(i, HashSet<Int>())

        return maxLen
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/160

Intuition

We can walk all paths once and track the cycles with the DFS.

Approach

  • Use separate visited sets for the current path and for the global visited nodes.
  • Careful with checkCycle corner cases.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

25.03.2023

2316. Count Unreachable Pairs of Nodes in an Undirected Graph medium

blog post


fun countPairs(n: Int, edges: Array<IntArray>): Long {
    val uf = IntArray(n) { it }
    val sz = LongArray(n) { 1L }
    fun root(x: Int): Int {
        var n = x
        while (uf[n] != n) n = uf[n]
        uf[x] = n
        return n
    }
    fun union(a: Int, b: Int) {
        val rootA = root(a)
        val rootB = root(b)
        if (rootA != rootB) {
            uf[rootB] = rootA
            sz[rootA] += sz[rootB]
            sz[rootB] = 0L
        }
    }
    edges.forEach { (from, to) -> union(from, to) }
    // 1 2 4 = 1*2 + 1*4 + 2*4 = 1*2 + (1+2)*4
    var sum = 0L
    var count = 0L
    sz.forEach { // 2 2 4 = 2*2 + 2*4 + 2*4 = 2*2 + (2+2)*4
        count += sum * it
        sum += it
    }
    return count
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/159

Intuition

To find connected components sizes, we can use Union-Find. To count how many pairs, we need to derive the formula, observing the pattern. Assume we have groups sizes 3, 4, 5, the number of pairs is the number of pairs between 3,4 + the number of pairs between 4,5 + between 3,5. Or, \(count(a,b,c) = count(a,b) + count(b,c) + count(a,c)\) where \(count(a,b) = a*b\). So, \(count_{abc} = ab + bc + ac = ab + (a + b)c = count_{ab} + (a+b)c\), or \(count_i = count_{i-1} + x_i*\sum_{j=0}^{i}x\)

Approach

  • use path compression for better root time complexity

    Complexity

  • Time complexity: \(O(height)\)
  • Space complexity: \(O(n)\)

24.03.2023

1466. Reorder Routes to Make All Paths Lead to the City Zero medium

blog post


    fun minReorder(n: Int, connections: Array<IntArray>): Int {
        val edges = mutableMapOf<Int, MutableList<Int>>()
        connections.forEach { (from, to) ->
            edges.getOrPut(from, { mutableListOf() }) += to
            edges.getOrPut(to, { mutableListOf() }) += -from
        }
        val visited = HashSet<Int>()
            var count = 0
            with(ArrayDeque<Int>().apply { add(0) }) {
                fun addNext(x: Int) {
                    if (visited.add(Math.abs(x))) {
                        add(Math.abs(x))
                        if (x > 0) count++
                    }
                }
                while (isNotEmpty()) {
                    repeat(size) {
                        val from = poll()
                        edges[from]?.forEach { addNext(it) }
                        edges[-from]?.forEach { addNext(it) }
                    }
                }
            }
            return count
        }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/158

Intuition

If our roads are undirected, the problem is simple: traverse with BFS from 0 and count how many roads are in the opposite direction.

Approach

We can use data structure or just use sign to encode the direction.

Complexity

  • Time complexity: \(O(V+E)\)
  • Space complexity: \(O(V+E)\)

23.03.2023

1319. Number of Operations to Make Network Connected medium

blog post


fun makeConnected(n: Int, connections: Array<IntArray>): Int {
    var extraCables = 0
    var groupsCount = n
    val uf = IntArray(n) { it }
    fun findRoot(x: Int): Int {
        var n = x
        while (uf[n] != n) n = uf[n]
        uf[x] = n
        return n
    }
    fun connect(a: Int, b: Int) {
        val rootA = findRoot(a)
        val rootB = findRoot(b)
        if (rootA == rootB) {
            extraCables++
            return
        }
        uf[rootB] = rootA
        groupsCount--
    }
    connections.forEach { (from, to) -> connect(from, to) }
    return if (extraCables < groupsCount - 1) -1 else groupsCount - 1
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/157

Intuition

The number of cables we need is the number of disconnected groups of connected computers. Cables can be taken from the computers that have extra connections. We can do this using BFS/DFS and tracking visited set, counting extra cables if already visited node is in connection. Another solution is to use Union-Find for the same purpose.

Approach

  • for the better time complexity of the findRoot use path compression: uf[x] = n

    Complexity

  • Time complexity: \(O(n*h)\), \(h\) - tree height, in a better implementation, can be down to constant. For Quick-Union-Find it is lg(n).
  • Space complexity: \(O(n)\)

22.03.2023

2492. Minimum Score of a Path Between Two Cities medium

blog post


fun minScore(n: Int, roads: Array<IntArray>): Int {
    val uf = Array(n + 1) { it }
    val minDist = Array(n + 1) { Int.MAX_VALUE }
    fun findRoot(x: Int): Int {
        var n = x
        while (uf[n] != n) n = uf[n]
        uf[x] = n
        return n
    }
    fun union(a: Int, b: Int, dist: Int) {
        val rootA = findRoot(a)
        val rootB = findRoot(b)
        uf[rootB] = rootA
        minDist[rootA] = minOf(minDist[rootA], minDist[rootB], dist)
    }
    roads.forEach { (from, to, dist) -> union(from, to, dist) }
    return minDist[findRoot(1)]
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/156

Intuition

Observing the problem definition, we don’t care about the path, but only about the minimum distance in a connected subset containing 1 and n. This can be solved by simple BFS, which takes \(O(V+E)\) time and space. But ideal data structure for this problem is Union-Find.

  • In an interview, it is better to just start with BFS, because explaining the time complexity of the find operation of Union-Find is difficult. https://algs4.cs.princeton.edu/15uf/

Approach

Connect all roads and update minimums in the Union-Find data structure. Use simple arrays for both connections and minimums.

  • updating a root after finding it gives more optimal time

    Complexity

  • Time complexity: \(O(E*tree_height)\)
  • Space complexity: \(O(n)\)

21.03.2023

2348. Number of Zero-Filled Subarrays medium

blog post


fun zeroFilledSubarray(nums: IntArray): Long {
    var currCount = 0L
    var sum = 0L
    nums.forEach {
        if (it == 0) currCount++ else currCount = 0L
        sum += currCount
    }
    return sum
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/155

Intuition

Consider the following sequence: 0, 00, 000. Each time we are adding another element to the end of the previous. For 0 count of subarrays \(c_1 = 1\), for 00 it is \(c_2 = c_1 + z_2\), where \(z_2\) is a number of zeros. So, the math equation is \(c_i = c_{i-1} + z_i\), or \(c_n = \sum_{i=0}^{n}z_i\)

Approach

We can count subarray sums, then add them to the result, or we can just skip directly to adding to the result.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

20.03.2023

605. Can Place Flowers easy

blog post


fun canPlaceFlowers(flowerbed: IntArray, n: Int): Boolean {
    var count = 0
    if (flowerbed.size == 1 && flowerbed[0] == 0) count++
    if (flowerbed.size >= 2 && flowerbed[0] == 0 && flowerbed[1] == 0) {
        flowerbed[0] = 1
        count++
    }
    for (i in 1..flowerbed.lastIndex - 1) {
        if (flowerbed[i] == 0 && flowerbed[i - 1] == 0 && flowerbed[i + 1] == 0) {
            flowerbed[i] = 1
            count++
        }
    }
    if (flowerbed.size >= 2 && flowerbed[flowerbed.lastIndex] == 0 && flowerbed[flowerbed.lastIndex - 1] == 0) count++
    return count >= n
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/154

Intuition

We can plant flowers greedily in every vacant place. This will be the maximum result because if we skip one item, the result is the same for even number of places or worse for odd.

Approach

  • careful with corner cases

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

19.03.2023

211. Design Add and Search Words Data Structure medium

blog post


class Trie {
    val next = Array<Trie?>(26) { null }
    fun Char.ind() = toInt() - 'a'.toInt()
    operator fun get(c: Char): Trie? = next[c.ind()]
    operator fun set(c: Char, t: Trie) { next[c.ind()] = t }
    var isWord = false
}
class WordDictionary(val root: Trie = Trie()) {
    fun addWord(word: String) {
        var t = root
        word.forEach { t = t[it] ?: Trie().apply { t[it] = this } }
        t.isWord = true
    }

    fun search(word: String): Boolean = with(ArrayDeque<Trie>().apply { add(root) }) {
        !word.any { c ->
            repeat(size) {
                val t = poll()
                if (c == '.') ('a'..'z').forEach { t[it]?.let { add(it) } }
                else t[c]?.let { add(it) }
            }
            isEmpty()
        } && any { it.isWord }
    }
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/153

Intuition

We are already familiar with a Trie data structure, however there is a wildcard feature added. We have two options: add wildcard for every character in addWord method in \(O(w26^w)\) time and then search in \(O(w)\) time, or just add a word to Trie in \(O(w)\) time and then search in \(O(w26^d)\) time, where \(d\) - is a wildcards count. In the description, there are at most 3 dots, so we choose the second option.

Approach

Let’s try to write it in a Kotlin way, using as little words as possible.

Complexity

  • Time complexity: \(O(w)\) add, \(O(w26^d)\) search, where \(d\) - wildcards count.
  • Space complexity: \(O(m)\), \(m\) - unique words suffixes count.

18.03.2023

1472. Design Browser History medium

blog post


class BrowserHistory(homepage: String) {
    val list = mutableListOf(homepage)
    var curr = 0
    var last = 0

    fun visit(url: String) {
        curr++
        if (curr == list.size) {
            list.add(url)
        } else {
            list[curr] = url
        }
        last = curr
    }

    fun back(steps: Int): String {
        curr = (curr - steps).coerceIn(0, last)
        return list[curr]
    }

    fun forward(steps: Int): String {
        curr = (curr + steps).coerceIn(0, last)
        return list[curr]
    }

}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/152

Intuition

Simple solution with array list will work, just not very optimal for the memory.

Approach

Just implement it.

Complexity

  • Time complexity: \(O(1)\) for all operations
  • Space complexity: \(O(n)\), will keep all the links

17.03.2023

208. Implement Trie (Prefix Tree) medium

blog post


class Trie() {
    val root = Array<Trie?>(26) { null }
    fun Char.ind() = toInt() - 'a'.toInt()
    operator fun get(c: Char): Trie? = root[c.ind()]
    operator fun set(c: Char, v: Trie) { root[c.ind()] = v }
    var isWord = false

    fun insert(word: String) {
        var t = this
        word.forEach { t = t[it] ?: Trie().apply { t[it] = this} }
        t.isWord = true
    }

    fun String.search(): Trie? {
        var t = this@Trie
        forEach { t = t[it] ?: return@search null }
        return t
    }

    fun search(word: String) = word.search()?.isWord ?: false

    fun startsWith(prefix: String) = prefix.search() != null

}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/151

Intuition

Trie is a common known data structure and all must know how to implement it.

Approach

Let’s try to write it Kotlin-way

Complexity

  • Time complexity: \(O(w)\) access for each method call, where \(w\) - is a word length
  • Space complexity: \(O(w*N)\), where \(N\) - is a unique words count.

16.03.2023

106. Construct Binary Tree from Inorder and Postorder Traversal medium

blog post


fun buildTree(inorder: IntArray, postorder: IntArray): TreeNode? {
    val inToInd = inorder.asSequence().mapIndexed { i, v -> v to i }.toMap()
    var postTo = postorder.lastIndex
    fun build(inFrom: Int, inTo: Int): TreeNode? {
        if (inFrom > inTo || postTo < 0) return null
        return TreeNode(postorder[postTo]).apply {
            val inInd = inToInd[postorder[postTo]]!!
            postTo--
            right = build(inInd + 1, inTo)
            left = build(inFrom, inInd - 1)
        }
    }
    return build(0, inorder.lastIndex)
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/150

Intuition

Postorder traversal gives us the root of every current subtree. Next, we need to find this value in inorder traversal: from the left of it will be the left subtree, from the right - right.

Approach

  • To more robust code, consider moving postTo variable as we go in the reverse-postorder: from the right to the left.
  • store indices in a hashmap

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

15.03.2023

958. Check Completeness of a Binary Tree medium

blog post


data class R(val min: Int, val max: Int, val complete: Boolean)
fun isCompleteTree(root: TreeNode?): Boolean {
    fun dfs(n: TreeNode): R {
        with(n) {
            if (left == null && right != null) return R(0, 0, false)
            if (left == null && right == null) return R(0, 0, true)
            val (leftMin, leftMax, leftComplete) = dfs(left)
            if (!leftComplete) return R(0, 0, false)
            if (right == null) return R(0, leftMax + 1, leftMin == leftMax && leftMin == 0)
            val (rightMin, rightMax, rightComplete) = dfs(right)
            if (!rightComplete) return R(0, 0, false)
            val isComplete = leftMin == rightMin && rightMin == rightMax
            || leftMin == leftMax && leftMin == rightMin + 1
            return R(1 + minOf(leftMin, rightMin), 1 + maxOf(leftMax, rightMax), isComplete)
        }
    }
    return root == null || dfs(root).complete
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/149

Intuition

image.png

For each node, we can compute it’s left and right child min and max depth, then compare them.

Approach

Right depth must not be larger than left. There are no corner cases, just be careful.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(log_2(n))\)

14.03.2023

129. Sum Root to Leaf Numbers medium

blog post


fun sumNumbers(root: TreeNode?): Int = if (root == null) 0 else {
    var sum = 0
    fun dfs(n: TreeNode, soFar: Int) {
        with(n) {
            val x = soFar * 10 + `val`
            if (left == null && right == null) sum += x
            if (left != null) dfs(left, x)
            if (right != null) dfs(right, x)
        }
    }
    dfs(root, 0)

    sum
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/148

Intuition

Just make DFS and add to the sum if the node is a leaf.

Approach

The most trivial way is to keep sum variable outside the dfs function.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(log_2(n))\)

13.03.2023

101. Symmetric Tree easy

blog post


data class H(val x: Int?)
fun isSymmetric(root: TreeNode?): Boolean {
    with(ArrayDeque<TreeNode>().apply { root?.let { add(it) } }) {
        while (isNotEmpty()) {
            val stack = Stack<H>()
                val sz = size
                repeat(sz) {
                    if (sz == 1 && peek().left?.`val` != peek().right?.`val`) return false
                    with(poll()) {
                        if (sz == 1 || it < sz / 2) {
                            stack.push(H(left?.`val`))
                            stack.push(H(right?.`val`))
                        } else {
                            if (stack.isEmpty() || stack.pop().x != left?.`val`) return false
                            if (stack.isEmpty() || stack.pop().x != right?.`val`) return false
                        }
                        left?.let { add(it)}
                        right?.let { add(it)}
                    }
                }
            }
        }
        return true
    }

    fun isSymmetric2(root: TreeNode?): Boolean {
        fun isSymmetric(leftRoot: TreeNode?, rightRoot: TreeNode?): Boolean {
            return leftRoot == null && rightRoot == null
            || leftRoot != null && rightRoot != null
            && leftRoot.`val` == rightRoot.`val`
            && isSymmetric(leftRoot.left, rightRoot.right)
            && isSymmetric(leftRoot.right, rightRoot.left)
        }
        return isSymmetric(root?.left, root?.right)
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/147

Intuition

Recursive solution based on idea that we must compare left.left with right.right and left.right with right.left. Iterative solution is just BFS and Stack.

Approach

Recursive: just write helper function. Iterative: save also null’s to solve corner cases.

Complexity

  • Time complexity: Recursive: \(O(n)\) Iterative: \(O(n)\)
  • Space complexity: Recursive: \(O(log_2(n))\) Iterative: \(O(n)\)

12.03.2023

23. Merge k Sorted Lists hard

blog post


    fun mergeKLists(lists: Array<ListNode?>): ListNode? {
        val root = ListNode(0)
        var curr: ListNode = root
        val pq = PriorityQueue<ListNode>(compareBy( { it.`val` }))
        lists.forEach { if (it != null) pq.add(it) }
        while (pq.isNotEmpty()) {
            val next = pq.poll()
            curr.next = next
            next.next?.let { pq.add(it) }
            curr = next
        }
        return root.next
    }
    fun mergeKLists2(lists: Array<ListNode?>): ListNode? {
        fun merge(oneNode: ListNode?, twoNode: ListNode?): ListNode? {
            val root = ListNode(0)
            var curr: ListNode = root
            var one = oneNode
            var two = twoNode
            while (one != null && two != null) {
                if (one.`val` <= two.`val`) {
                    curr.next = one
                    one = one.next
                } else {
                    curr.next = two
                    two = two.next
                }
                curr = curr.next!!
            }
            if (one != null) curr.next = one
            else if (two != null) curr.next = two

            return root.next
        }
        return lists.fold(null as ListNode?) { r, t -> merge(r, t) }
    }

Join me on telegram

https://t.me/leetcode_daily_unstoppable/146

Intuition

On each step, we need to choose a minimum from k variables. The best way to do this is to use PriorityQeueu Another solution is to just iteratively merge the result to the next list from the array.

Approach

  • use dummy head For the PriorityQueue solution:
  • use non-null values to more robust code For the iterative solution:
  • we can skip merging if one of the lists is empty

    Complexity

  • Time complexity:
  • PriorityQueue: \(O(nlog(k))\)
  • iterative merge: \(O(nk)\)
  • Space complexity:
  • PriorityQueue: \(O(k)\)
  • iterative merge: \(O(1)\)

11.03.2023

109. Convert Sorted List to Binary Search Tree medium

blog post


fun sortedListToBST(head: ListNode?): TreeNode? {
    if (head == null) return null
    if (head.next == null) return TreeNode(head.`val`)
    var one = head
    var twoPrev = head
    var two = head
    while (one != null && one.next != null) {
        one = one.next?.next
        twoPrev = two
        two = two?.next
    }
    twoPrev!!.next = null
    return TreeNode(two!!.`val`).apply {
        left = sortedListToBST(head)
        right = sortedListToBST(two!!.next)
    }
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/145

Intuition

One way is to convert linked list to array, then just build a binary search tree using divide and conquer technique. This will take \(O(nlog_2(n))\) additional memory, and \(O(n)\) time. We can skip using the array and just compute the middle of the linked list each time.

Approach

Compute the middle of the linked list.

  • careful with corner cases (check fast.next != null instead of fast != null)

    Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(log_2(n))\) of additional space (for recursion)

10.03.2023

382. Linked List Random Node medium

blog post


class Solution(val head: ListNode?) {
    val rnd = Random(0)
    var curr = head

    fun getRandom(): Int {
        val ind = rnd.nextInt(6)
        var peek: ListNode? = null
        repeat(6) {
            curr = curr?.next
            if (curr == null) curr = head
            if (it == ind) peek = curr
        }

        return peek!!.`val`
    }

}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/144

Intuition

Naive solution is trivial. For more interesting solution, you need to look at what others did on leetcode, read an article https://en.wikipedia.org/wiki/Reservoir_sampling and try to understand why it works.

My intuition was: if we need a probability 1/n, where n - is a total number of elements, then what if we split all the input into buckets of size k, then choose from every bucket with probability 1/k. It seems to work, but only for sizes starting from number 6 for the given input. We just need to be sure, that number of getRandom calls are equal to number of buckets n/k.

Approach

Write the naive solution, then go to Wikipedia, and hope you will not get this in the interview.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

09.03.2023

142. Linked List Cycle II medium

blog post


fun detectCycle(head: ListNode?): ListNode? {
    var one = head
    var two = head
    do {
        one = one?.next
        two = two?.next?.next
    } while (two != null && one != two)
    if (two == null) return null
    one = head
    while (one != two) {
        one = one?.next
        two = two?.next
    }
    return one
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/143

Intuition

image.png There is a known algorithm to detect a cycle in a linked list. Move slow pointer one node at a time, and move fast pointer two nodes at a time. Eventually, if they meet, there is a cycle. To know the connection point of the cycle, you can also use two pointers: one from where pointers were met, another from the start, and move both of them one node at a time until they meet. How to derive this yourself?

  • you can draw the diagram
  • notice, when all the list is a cycle, nodes met at exactly where they are started
  • meet point = cycle length + tail

    Approach

  • careful with corner cases.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

08.03.2023

875. Koko Eating Bananas medium

blog post


fun minEatingSpeed(piles: IntArray, h: Int): Int {
    fun canEatAll(speed: Long): Boolean {
        var time = 0L
        piles.forEach {
            time += (it.toLong() / speed) + if ((it.toLong() % speed) == 0L) 0L else 1L
        }
        return time <= h
    }
    var lo = 1L
    var hi = piles.asSequence().map { it.toLong() }.sum()!!
    var minSpeed = hi
    while (lo <= hi) {
        val speed = lo + (hi - lo) / 2
        if (canEatAll(speed)) {
            minSpeed = minOf(minSpeed, speed)
            hi = speed - 1
        } else {
            lo = speed + 1
        }
    }
    return minSpeed.toInt()
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/142

Intuition

Given the speed we can count how many hours take Coco to eat all the bananas. With growth of speed hours growth too, so we can binary search in that space.

Approach

For more robust binary search:

  • use inclusive condition check lo == hi
  • always move boundaries mid + 1, mid - 1
  • compute the result on each step

    Complexity

  • Time complexity: \(O(nlog_2(m))\), m - is hours range
  • Space complexity: \(O(1)\)

07.03.2023

2187. Minimum Time to Complete Trips medium

blog post


fun minimumTime(time: IntArray, totalTrips: Int): Long {
    fun tripCount(timeGiven: Long): Long {
        var count = 0L
        for (t in time) count += timeGiven / t.toLong()
        return count
    }
    var lo = 0L
    var hi = time.asSequence().map { it.toLong() * totalTrips }.max()!!
    var minTime = hi
    while (lo <= hi) {
        val timeGiven = lo + (hi - lo) / 2
        val trips = tripCount(timeGiven)
        if (trips >= totalTrips) {
            minTime = minOf(minTime, timeGiven)
            hi = timeGiven - 1
        } else {
            lo = timeGiven + 1
        }
    }
    return minTime
}

Join me on telergam

https://t.me/leetcode_daily_unstoppable/140

Intuition

Naive approach is just to simulate the time running, but given the problem range it is not possible. However, observing the time simulation results, we can notice, that by each given time there is a certain number of trips. And number of trips growths continuously with the growth of the time. This is a perfect condition to do a binary search in a space of the given time. With given time we can calculate number of trips in a \(O(n)\) complexity.

Approach

Do a binary search. For the hi value, we can peak a \(10^7\) or just compute all the time it takes for every bus to trip. For a more robust binary search:

  • use inclusive lo and hi
  • use inclusive check for the last case lo == hi
  • compute the result on every step instead of computing it after the search
  • always move the borders mid + 1, mid - 1

Complexity

  • Time complexity: \(O(nlog_2(m))\), \(m\) - is a time range
  • Space complexity: \(O(1)\)

06.03.2023

1539. Kth Missing Positive Number easy

blog post


fun findKthPositive(arr: IntArray, k: Int): Int {
    // 1 2 3 4 5 6 7 8 9 10 11
    // * 2 3 4 * * 7 * * *  11
    //   ^                  ^
    // 1 2 3 4 5
    // 2 3 4 7 11
    // 1
    //   1
    //     1
    //       3
    //         6
    //
    //       ^ 7 + (5-3) = 9
    //         arr[m] + (k-diff)
    //
    // 1 2
    // 7 8     k=1
    // 6
    //   6
    var lo = 0
    var hi = arr.lastIndex
    var res = -1
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val diff = arr[mid] - mid - 1
        if (diff < k) {
            res = arr[mid] + (k - diff)
            lo = mid + 1
        } else {
            hi  = mid - 1
        }
    }
    return if (res == -1) k else res
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/139

Intuition

Let’s observe an example:


// 1 2 3 4 5 6 7 8 9 10 11
// * 2 3 4 * * 7 * * *  11

For each number at its position, there are two conditions:

  • if it stays in a correct position, then num - pos == 0
  • if there is a missing number before it, then num - pos == diff > 0

We can observe the pattern and derive the formula for it:


// 1 2 3 4 5
// 2 3 4 7 11
// 1
//   1
//     1
//       3
//         6
//
//       ^ 7 + (5-3) = 9
//         arr[m] + (k-diff)

One corner case is if the missing numbers are at the beginning of the array:


// 1 2
// 7 8     k=1
// 6
//   6

Then the answer is just a k.

Approach

For more robust binary search code:

  • use inclusive borders lo and hi (don’t make of by 1 error)
  • use inclusive last check lo == hi (don’t miss one item arrays)
  • always move the borders mid + 1 or mid - 1 (don’t fall into an infinity loop)
  • always compute the search if the case is true (don’t compute it after the search to avoid mistakes)

    Complexity

  • Time complexity: \(O(log_2(n))\)
  • Space complexity: \(O(n)\)

05.03.2023

1345. Jump Game IV hard

blog post


fun minJumps(arr: IntArray): Int {
    val numToPos = mutableMapOf<Int, MutableList<Int>>()
        arr.forEachIndexed { i, n -> numToPos.getOrPut(n, { mutableListOf() }).add(i) }
        with(ArrayDeque<Int>().apply { add(0) }) {
            var jumps = 0
            val visited = HashSet<Int>()
                while(isNotEmpty()) {
                    repeat(size) {
                        val curr = poll()
                        if (curr == arr.lastIndex) return jumps
                        numToPos.remove(arr[curr])?.forEach { if (visited.add(it)) add(it) }
                        if (curr > 0 && visited.add(curr - 1)) add(curr - 1)
                        if (curr < arr.lastIndex && visited.add(curr + 1)) add(curr + 1)
                    }
                    jumps++
                }
            }
            return 0
        }

Join me on telegram

https://t.me/leetcode_daily_unstoppable/138

Intuition

Dynamic programming approach wouldn’t work here, as we can tell from position i is it optimal before visiting both left and right subarrays. Another way to find the shortest path is to just do Breath-First-Search.

Approach

This problem gives TLE until we do one trick:

  • remove the visited nodes from the graph

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

04.03.2023

2444. Count Subarrays With Fixed Bounds hard

blog post


fun countSubarrays(nums: IntArray, minK: Int, maxK: Int): Long {
    val range = minK..maxK
    var i = 0
    var sum = 0L
    if (minK == maxK) {
        var count = 0
        for (i in 0..nums.lastIndex) {
            if (nums[i] == minK) count++
            else count = 0
            if (count > 0) sum += count
        }
        return sum
    }
    while (i < nums.size) {
        val curr = nums[i]
        if (curr in range) {
            val minInds = TreeSet<Int>()
                val maxInds = TreeSet<Int>()
                    var end = i
                    while (end < nums.size && nums[end] in range) {
                        if (nums[end] == minK) minInds.add(end)
                        else if (nums[end] == maxK) maxInds.add(end)
                        end++
                    }
                    if (minInds.size > 0 && maxInds.size > 0) {
                        var prevInd = i - 1
                        while (minInds.isNotEmpty() && maxInds.isNotEmpty()) {
                            val minInd = minInds.pollFirst()!!
                            val maxInd = maxInds.pollFirst()!!
                            val from = minOf(minInd, maxInd)
                            val to = maxOf(minInd, maxInd)
                            val remainLenAfter = (end - 1 - to).toLong()
                            val remainLenBefore = (from - (prevInd + 1)).toLong()
                            sum += 1L + remainLenAfter + remainLenBefore + remainLenAfter * remainLenBefore
                            prevInd = from
                            if (to == maxInd) maxInds.add(to)
                            else if (to == minInd) minInds.add(to)
                        }
                    }
                    if (i == end) end++
                    i = end
                } else i++
            }
            return sum
        }
and more clever solution:
fun countSubarrays(nums: IntArray, minK: Int, maxK: Int): Long {
    var sum = 0L
    if (minK == maxK) {
        var count = 0
        for (i in 0..nums.lastIndex) {
            if (nums[i] == minK) count++
            else count = 0
            if (count > 0) sum += count
        }
        return sum
    }
    val range = minK..maxK
    // 0 1 2 3 4 5 6 7 8 91011
    // 3 7 2 2 2 2 2 1 2 3 2 1
    //   b
    //               *...*
    //                   *...*
    var border = -1
    var posMin = -1
    var posMax = -1
    for (i in 0..nums.lastIndex) {
        when (nums[i]) {
            !in range -> border = i
            minK -> posMin = i
            maxK -> posMax = i
        }
        if (posMin > border && posMax > border)
        sum += minOf(posMin, posMax) - border
    }
    return sum
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/137

Intuition

First thought is that we can observe only subarrays, where all the elements are in a range min..max. Next, there are two possible scenarios:

  1. If minK==maxK, our problem is a trivial count of the combinations, \(0 + 1 + .. + (n-1) + n = n*(n+1)/2\)
  2. If minK != maxK, we need to take every minK|maxK pair, and count how many items are in range before them and how many after. Then, as we observe the pattern of combinations:

// 0 1 2 3 4 5 6    min=1, max=3
// ------------------
// 1 2 3 2 1 2 3
// 1 2 3          *** 0..2 remainLenAfter = 6 - 2 = 4
// 1 2 3 2
// 1 2 3 2 1
// 1 2 3 2 1 2
// 1 2 3 2 1 2 3
//     3 2 1      *** 2..4 remainLenAfter = 6 - 4 = 2
//     3 2 1 2
//     3 2 1 2 3
//   2 3 2 1               remainLenBefore = 2 - (0 + 1) = 1, sum += 1 + remainLenAfter += 1+2 += 3
//   2 3 2 1 2
//   2 3 2 1 2 3
//         1 2 3  *** 4..6 remainLenBefore = 4 - 4 + 1 = 1
//       2 1 2 3

// 1 2 1 2 3 2 3
// *.......*      *** 0..4 sum += 1 + 2 = 3
//     *...*      *** 2..4 rla = 6 - 4 = 2, rlb = 2 - (0 + 1) = 1, sum += 1 + rla + rlb + rlb*rla += 6 = 9

// 1 3 5 2 7 5
// *...*
//

we derive the formula: \(sum += 1 + suffix + prefix + suffix*prefix\)

A more clever, but less understandable solution: is to count how many times we take a condition where we have a min and a max and each time add prefix count. Basically, it is the same formula, but with a more clever way of computing. (It is like computing a combination sum by adding each time the counter to sum).

Approach

For the explicit solution, we take each interval, store positions of the min and max in a TreeSet, then we must take poll those mins and maxes and consider each range separately:


// 3 2 3 2 1 2 1
// *.......*
//     *...*

// 3 2 1 2 3 2 1
// *...*
//     *...*
//         *...*

// 3 2 1 2 1 2 3
// *...*
//     *.......*
//         *...*

// 3 2 1 2 3 3 3
// *...*
//     *...*

// 3 2 2 2 2 2 1
// *...........*

// 1 1 1 1 1 1 1
// *.*
//   *.*
//     *.*
//       *.*
//         *.*
//           *.*

For the tricky one solution, just see what other clever man already wrote on the leetcode site and hope you will not get the same problem in an interview.

Complexity

  • Time complexity: \(O(nlog_2(n))\) -> \(O(n)\)

  • Space complexity: \(O(n)\) -> \(O(1)\)

03.03.2023

28. Find the Index of the First Occurrence in a String medium

blog post


fun strStr(haystack: String, needle: String): Int {
    // f(x) = a + 32 * f(x - 1)
    // abc
    // f(a) = a + 0
    // f(ab) = b + 32 * (a + 0)
    // f(abc) = c + 32 * (b + 32 * (a + 0))
    //
    // f(b) = b + 0
    // f(bc) = c + 32 * (b + 0)
    //
    // f(abc) - f(bc) = 32^0*c + 32^1*b + 32^2*a - 32^0*c - 32^1*b = 32^2*a
    // f(bc) = f(abc) - 32^2*a
    var needleHash = 0L
    needle.forEach { needleHash = it.toLong() + 32L * needleHash }
    var currHash = 0L
    var pow = 1L
    repeat(needle.length) { pow *= 32L}
    for (curr in 0..haystack.lastIndex) {
        currHash = haystack[curr].toLong() + 32L * currHash
        if (curr >= needle.length)
        currHash -= pow * haystack[curr - needle.length].toLong()
        if (curr >= needle.lastIndex
        && currHash == needleHash
        && haystack.substring(curr - needle.lastIndex, curr + 1) == needle)
        return curr - needle.lastIndex
    }
    return -1
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/136

Intuition

There is a rolling hash technique: you can compute hash for a sliding window using O(1) additional time. Consider the math behind it:


// f(x) = a + 32 * f(x - 1)
// abc
// f(a) = a + 0
// f(ab) = b + 32 * (a + 0)
// f(abc) = c + 32 * (b + 32 * (a + 0))
//
// f(b) = b + 0
// f(bc) = c + 32 * (b + 0)
//
// f(abc) - f(bc) = 32^0*c + 32^1*b + 32^2*a - 32^0*c - 32^1*b = 32^2*a
// f(bc) = f(abc) - 32^2*a

Basically, you can subtract char * 32^window_length from the lower side of the sliding window.

Approach

  • carefull with indexes

    Complexity

  • Time complexity: \(O(n)\), if our hash function is good, we good
  • Space complexity: \(O(n)\), for substring, can be improved to O(1)

02.03.2023

443. String Compression medium

blog post


fun compress(chars: CharArray): Int {
    var end = 0
    var curr = 0
    while (curr < chars.size) {
        val c = chars[curr++]
        var currCount = 1
        while (curr < chars.size && c == chars[curr]) {
            curr++
            currCount++
        }
        chars[end++] = c
        if (currCount > 1) currCount.toString().forEach { chars[end++] = it }
    }
    return end
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/135

Intuition

You don’t need to split a number into groups of 9’s. The right way to convert number 123 into a string is to divide it by 10 each time, then reverse a part of the array.

Approach

  • Let’s just do a naive toString for simplicity.
  • to avoid mistakes with indexes, use explicit variable for count the duplicate chars

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(lg_10(n))\), for storing toString. For this task it is a 4

01.03.2023

912. Sort an Array medium

blog post


fun sortArray(nums: IntArray, from: Int = 0, to: Int = nums.lastIndex): IntArray {
    if (from >= to) return nums
    val mid = partition(nums, from, to)
    sortArray(nums, from, mid - 1)
    sortArray(nums, mid + 1, to)
    return nums
}
fun IntArray.swap(i: Int, j: Int) { this[i] = this[j].also { this[j] = this[i] } }
fun partition(nums: IntArray, from: Int, to: Int): Int {
    var border = nums[to]
    var afterBorder = from
    for (curr in from until to)
    if (nums[curr] < border) nums.swap(curr, afterBorder++)
    nums.swap(to, afterBorder)
    return afterBorder
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/134

Intuition

There are some tricks to optimize naive quicksort algorithm.

  • choose between lo, mid and hi elements for the pivot instead of just hi
  • shuffling the array before sorting
  • starting with the smallest part of the array
  • making the last recursion call with a tailrec
  • sorting with insertion sort for a small parts

Approach

Let’s just implement naive quicksort.

Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(log_2(n))\) for the recursion

28.02.2023

652. Find Duplicate Subtrees medium

blog post


fun findDuplicateSubtrees(root: TreeNode?): List<TreeNode?> {
    val result = mutableListOf<TreeNode?>()
    val hashes = HashSet<String>()
        val added = HashSet<String>()
            fun hashDFS(node: TreeNode): String {
                return with(node) {
                    "[" + (left?.let { hashDFS(it) } ?: "*") +
                    "_" + `val` + "_" +
                    (right?.let { hashDFS(it) } ?: "*") + "]"
                }.also {
                    if (!hashes.add(it) && added.add(it)) result.add(node)
                }
            }
            if (root != null) hashDFS(root)
            return result
        }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/132

Intuition

We can traverse the tree and construct a hash of each node, then just compare nodes with equal hashes. Another way is to serialize the tree and compare that data.

Approach

Let’s use pre-order traversal and serialize each node into string, also add that into HashSet and check for duplicates.

Complexity

  • Time complexity: \(O(n^2)\), because of the string construction on each node.
  • Space complexity: \(O(n^2)\)

27.02.2023

427. Construct Quad Tree medium

blog post


fun construct(grid: Array<IntArray>): Node? {
    if (grid.isEmpty()) return null
    fun dfs(xMin: Int, xMax: Int, yMin: Int, yMax: Int): Node? {
        if (xMin == xMax) return Node(grid[yMin][xMin] == 1, true)
        val xMid = xMin + (xMax - xMin) / 2
        val yMid = yMin + (yMax - yMin) / 2
        return Node(false, false).apply {
            topLeft = dfs(xMin, xMid, yMin, yMid)
            topRight = dfs(xMid + 1, xMax, yMin, yMid)
            bottomLeft = dfs(xMin, xMid, yMid + 1, yMax)
            bottomRight = dfs(xMid + 1, xMax, yMid + 1, yMax)
            if (topLeft!!.isLeaf && topRight!!.isLeaf
            && bottomLeft!!.isLeaf && bottomRight!!.isLeaf) {
                if (topLeft!!.`val` == topRight!!.`val`
                && topRight!!.`val` == bottomLeft!!.`val`
                && bottomLeft!!.`val` == bottomRight!!.`val`) {
                    `val` = topLeft!!.`val`
                    isLeaf = true
                    topLeft = null
                    topRight = null
                    bottomLeft = null
                    bottomRight = null
                }
            }
        }
    }
    return dfs(0, grid[0].lastIndex, 0, grid.lastIndex)
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/131

Intuition

We can construct the tree using DFS and divide and conquer technique. Build four nodes, then check if all of them are equal leafs.

Approach

  • use inclusive ranges to simplify the code

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

26.02.2023

72. Edit Distance hard

blog post


fun minDistance(word1: String, word2: String): Int {
    val dp = Array(word1.length + 1) { IntArray(word2.length + 1) { -1 } }
    fun dfs(i: Int, j: Int): Int {
        return when {
            dp[i][j] != -1 -> dp[i][j]
            i == word1.length && j == word2.length -> 0
            i == word1.length -> 1 + dfs(i, j+1)
            j == word2.length -> 1 + dfs(i+1, j)
            word1[i] == word2[j] -> dfs(i+1, j+1)
            else -> {
                val insert1Delete2 = 1 + dfs(i, j+1)
                val insert2Delete1 = 1 + dfs(i+1, j)
                val replace1Or2 = 1 + dfs(i+1, j+1)
                val res = minOf(insert1Delete2, insert2Delete1, replace1Or2)
                dp[i][j] = res
                res
            }
        }
    }
    return dfs(0, 0)
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/130

Intuition

Compare characters from each positions of the two strings. If they are equal, do nothing. If not, we can choose from three paths: removing, inserting or replacing. That will cost us one point of operations. Then, do DFS and choose the minimum of the operations.

Approach

Do DFS and use array for memoizing the result.

Complexity

  • Time complexity: \(O(n^2)\), can be proven if you rewrite DP to bottom up code.
  • Space complexity: \(O(n^2)\)

25.02.2023

121. Best Time to Buy and Sell Stock easy

blog post


fun maxProfit(prices: IntArray): Int {
    var min = prices[0]
    var profit = 0
    prices.forEach {
        if (it < min) min = it
        profit = maxOf(profit, it - min)
    }
    return profit
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/129

Intuition

Max profit will be the difference between max and min. One thing to note, the max must follow after the min.

Approach

  • we can just use current value as a max candidate instead of managing the max variable.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

24.02.2023

1675. Minimize Deviation in Array hard

blog post


fun minimumDeviation(nums: IntArray): Int {
    var minDiff = Int.MAX_VALUE
    with(TreeSet<Int>(nums.map { if (it % 2 == 0) it else it * 2 })) {
        do {
            val min = first()
            val max = pollLast()
            minDiff = minOf(minDiff, Math.abs(max - min))
            add(max / 2)
        } while (max % 2 == 0)
    }

    return minDiff
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/128

Intuition

We can notice, that the answer is the difference between the min and max from some resulting set of numbers. My first (wrong) intuition was, that we can use two heaps for minimums and maximums, and only can divide by two from the maximum, and multiply by two from the minimum heap. That quickly transformed into too many edge cases. The correct and tricky intuition: we can multiply all the numbers by 2, and then we can safely begin to divide all the maximums until they can be divided.

Approach

Use TreeSet to quickly access to the min and max elements.

Complexity

  • Time complexity: \(O(n(log_2(n) + log_2(h)))\), where h - is a number’s range
  • Space complexity: \(O(n)\)

23.02.2023

502. IPO hard

blog post


fun findMaximizedCapital(k: Int, w: Int, profits: IntArray, capital: IntArray): Int {
  val indices = Array(profits.size) { it }.apply { sortWith(compareBy( { capital[it] })) }
  var money = w
  with(PriorityQueue<Int>(profits.size, compareBy({ -profits[it] }))) {
    var i = 0
    repeat (k) {
      while (i <= indices.lastIndex && money >= capital[indices[i]]) add(indices[i++])
      if (isNotEmpty()) money += profits[poll()]
    }
  }
  return money
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/127

Intuition

My first (wrong) intuition: greedy add elements to the min-profit priority queue, then remove all low-profit elements from it, keeping essential items. It wasn’t working, and the solution became too verbose. Second intuition, after the hint: greedy add elements to the max-profit priority queue, then remove the maximum from it, which will be the best deal for the current money.

Approach

Sort items by increasing capital. Then, on each step, add all possible deals to the priority queue and take one best from it.

Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(n)\)

22.02.2023

1011. Capacity To Ship Packages Within D Days medium

blog post


fun shipWithinDays(weights: IntArray, days: Int): Int {
  var lo = weights.max()!!
  var hi = weights.sum()!!
  fun canShip(weight: Int): Boolean {
    var curr = 0
    var count = 1
    weights.forEach {
      curr += it
      if (curr > weight) {
        curr = it
        count++
      }
    }
    if (curr > weight) count++
    return count <= days
  }
  var min = hi
  while (lo <= hi) {
    val mid = lo + (hi - lo) / 2
    val canShip = canShip(mid)
    if (canShip) {
      min = minOf(min, mid)
      hi = mid - 1
    } else lo = mid + 1
  }
  return min
}

Join me on telegram

https://t.me/leetcode_daily_unstoppable/126

Intuition

Of all the possible capacities, there is an increasing possibility to carry the load. It may look like this: not possible, not possible, .., not possible, possible, possible, .., possible. We can binary search in that sorted space of possibilities.

Approach

To more robust binary search code:

  • use inclusive lo and hi
  • check the last case lo == hi
  • check target condition separately min = minOf(min, mid)
  • always move boundaries lo and hi

    Complexity

  • Time complexity: \(O(nlog_2(n))\)
  • Space complexity: \(O(1)\)

21.02.2023

540. Single Element in a Sorted Array medium

blog post


fun singleNonDuplicate(nums: IntArray): Int {
    var lo = 0
    var hi = nums.lastIndex
    // 0 1 2 3 4
    // 1 1 2 3 3
    while (lo <= hi) {
        val mid = lo + (hi - lo) / 2
        val prev = if (mid > 0) nums[mid-1] else -1
        val next = if (mid < nums.lastIndex) nums[mid+1] else Int.MAX_VALUE
        val curr = nums[mid]
        if (prev < curr && curr < next) return curr

        val oddPos = mid % 2 != 0
        val isSingleOnTheLeft = oddPos && curr == next || !oddPos && curr == prev

        if (isSingleOnTheLeft) hi = mid - 1 else lo = mid + 1
    }
    return -1
}

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/125

Intuition

This problem is a brain-teaser until you notice that pairs are placed at even-odd positions before the target and at odd-even positions after.

Approach

Let’s write a binary search. For more robust code, consider:

  • use inclusive lo and hi
  • always move lo or hi
  • check for the target condition and return early

    Complexity

  • Time complexity: \(O(log_2(n))\)
  • Space complexity: \(O(1)\)

20.02.2023

35. Search Insert Position easy

blog post


    fun searchInsert(nums: IntArray, target: Int): Int {
        var lo = 0
        var hi = nums.lastIndex
        while (lo <= hi) {
            val mid = lo + (hi - lo) / 2
            if (target == nums[mid]) return mid
            if (target > nums[mid]) lo = mid + 1
            else hi = mid - 1
        }
        return lo
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/124

Intuition

Just do a binary search

Approach

For more robust code consider:

  • use only inclusive boundaries lo and hi
  • loop also the last case when lo == hi
  • always move boundaries mid + 1 or mid - 1
  • use distinct check for the exact match nums[mid] == target
  • return lo position - this is an insertion point

Complexity

  • Time complexity: \(O(log_2(n))\)
  • Space complexity: \(O(1)\)

19.02.2023

103. Binary Tree Zigzag Level Order Traversal medium

blog post

    fun zigzagLevelOrder(root: TreeNode?): List<List<Int>> = mutableListOf<List<Int>>().also { res ->
            with(ArrayDeque<TreeNode>().apply { root?.let { add(it) } }) {
                while (isNotEmpty()) {
                    val curr = LinkedList<Int>().apply { res.add(this) }
                    repeat(size) {
                        with(poll()) {
                            with(curr) { if (res.size % 2 == 0) addFirst(`val`) else addLast(`val`) }
                            left?.let { add(it) }
                            right?.let { add(it) }
                        }
                    }
                }
            }
        }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/123

Intuition

Each BFS step gives us a level, which one we can reverse if needed.

Approach

  • for zigzag, we can skip a boolean variable and track result count.

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

18.02.2023

226. Invert Binary Tree easy

blog post

    fun invertTree(root: TreeNode?): TreeNode? = 
        root?.apply { left = invertTree(right).also { right = invertTree(left) } }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/122

Intuition

Walk tree with Depth-First Search and swap each left and right nodes.

Approach

Let’s write a recursive one-liner.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(log_2(n))\)

17.02.2023

783. Minimum Distance Between BST Nodes easy

blog post

    fun minDiffInBST(root: TreeNode?): Int {
        var prev: TreeNode? = null
        var curr = root
        var minDiff = Int.MAX_VALUE
        while (curr != null) {
            if (curr.left == null) {
                if (prev != null) minDiff = minOf(minDiff, Math.abs(curr.`val` - prev.`val`))
                prev = curr
                curr = curr.right
            } else {
                var right = curr.left!!
                while (right.right != null && right.right != curr) right = right.right!!
                if (right.right == curr) {
                    right.right = null
                    curr = curr.right
                } else {
                    right.right = curr
                    val next = curr.left
                    curr.left = null
                    curr = next
                }
            }
        }
        return minDiff
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/121

Intuition

Given that this is a Binary Search Tree, inorder traversal will give us an increasing sequence of nodes. Minimum difference will be one of the adjacent nodes differences.

Approach

Let’s write Morris Traversal. Store current node at the rightmost end of the left children.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

16.02.2023

104. Maximum Depth of Binary Tree easy

blog post

    fun maxDepth(root: TreeNode?): Int =
        root?.run { 1 + maxOf(maxDepth(left), maxDepth(right)) } ?: 0

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/120

Intuition

Do DFS and choose the maximum on each step.

Approach

Let’s write a one-liner.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(log_2(n))\)

15.02.2023

989. Add to Array-Form of Integer easy

blog post

    fun addToArrayForm(num: IntArray, k: Int): List<Int> {
        var carry = 0
        var i = num.lastIndex
        var n = k
        val res = LinkedList<Int>()
        while (i >= 0 || n > 0 || carry > 0) {
            val d1 = if (i >= 0) num[i--] else 0
            val d2 = if (n > 0) n % 10 else 0
            var d = d1 + d2 + carry
            res.addFirst(d % 10)
            carry = d / 10 
            n = n / 10
        }
        return res
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/119

Intuition

Iterate from the end of the array and calculate sum of num % 10, carry and num[i].

Approach

  • use linked list to add to the front of the list in O(1)

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

14.02.2023

67. Add Binary easy

blog post

        fun addBinary(a: String, b: String): String = StringBuilder().apply {
        var o = 0
        var i = a.lastIndex
        var j = b.lastIndex
        while (i >= 0 || j >= 0 || o == 1) {
            var num = o
            o = 0
            if (i >= 0 && a[i--] == '1') num++
            if (j >= 0 && b[j--] == '1') num++
            when (num) {
                0 -> append('0')
                1 -> append('1')
                2 -> {
                    append('0')
                    o = 1
                }
                else -> {
                    append('1')
                    o = 1
                }
            }
        }
    }.reverse().toString()

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/118

Intuition

Scan two strings from the end and calculate the result.

Approach

  • keep track of the overflow

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

13.02.2023

1523. Count Odd Numbers in an Interval Range easy

blog post

    fun countOdds(low: Int, high: Int): Int {
        if (low == high) return if (low % 2 == 0) 0 else 1
        val lowOdd = low % 2 != 0
        val highOdd = high % 2 != 0
        val count = high - low + 1
        return if (lowOdd && highOdd) {
            1 + count / 2
        } else if (lowOdd || highOdd) {
            1 + (count - 1) / 2
        } else {
            1 + ((count - 2) / 2)
        }
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/117

Intuition

Count how many numbers in between, subtract even on the start and the end, then divide by 2.

Complexity

  • Time complexity: \(O(1)\)
  • Space complexity: \(O(1)\)

12.02.2023

2477. Minimum Fuel Cost to Report to the Capital medium

blog post

    data class R(val cars: Long, val capacity: Int, val fuel: Long)
    fun minimumFuelCost(roads: Array<IntArray>, seats: Int): Long {
        val nodes = mutableMapOf<Int, MutableList<Int>>()
        roads.forEach { (from, to) ->
            nodes.getOrPut(from, { mutableListOf() }) += to
            nodes.getOrPut(to, { mutableListOf() }) += from
        }
        fun dfs(curr: Int, parent: Int): R {
            val children = nodes[curr]
            if (children == null) return R(1L, seats - 1, 0L)
            var fuel = 0L
            var capacity = 0
            var cars = 0L
            children.filter { it != parent }.forEach {
                val r = dfs(it, curr)
                fuel += r.cars + r.fuel
                capacity += r.capacity
                cars += r.cars
            }
            // seat this passenger
            if (capacity == 0) {
                cars++
                capacity = seats - 1
            } else capacity--
            // optimize cars
            while (capacity - seats >= 0) {
                capacity -= seats
                cars--
            }
            return R(cars, capacity, fuel)
        }
        return dfs(0, 0).fuel
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/116

Intuition

image.png

Let’s start from each leaf (node without children). We give one car, seats-1 capacity and zero fuel. When children cars arrive, each of them consume cars capacity of the fuel. On the hub (node with children), we sat another one passenger, so capacity-- and we can optimize number of cars arrived, if total capacity is more than one car seats number.

Approach

Use DFS and data class for the result.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(h)\), h - height of the tree, can be 0..n

11.02.2023

1129. Shortest Path with Alternating Colors medium

blog post

    fun shortestAlternatingPaths(n: Int, redEdges: Array<IntArray>, blueEdges: Array<IntArray>): IntArray {
        val edgesRed = mutableMapOf<Int, MutableList<Int>>()
        val edgesBlue = mutableMapOf<Int, MutableList<Int>>()
        redEdges.forEach { (from, to) ->
            edgesRed.getOrPut(from, { mutableListOf() }).add(to)
        }
        blueEdges.forEach { (from, to) ->
            edgesBlue.getOrPut(from, { mutableListOf() }).add(to)
        }
        val res = IntArray(n) { -1 }
        val visited = hashSetOf<Pair<Int, Boolean>>()
        var dist = 0
        with(ArrayDeque<Pair<Int, Boolean>>()) {
            add(0 to true)
            add(0 to false)
            visited.add(0 to true)
            visited.add(0 to false)
            while (isNotEmpty()) {
                repeat(size) {
                    val (node, isRed) = poll()
                    if (res[node] == -1 || res[node] > dist) res[node] = dist
                    val edges = if (isRed) edgesRed else edgesBlue
                    edges[node]?.forEach {
                        if (visited.add(it to !isRed)) add(it to !isRed)
                    }
                }
                dist++
            }
        }
        return res
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/115

Intuition

We can calculate all the shortest distances in one pass BFS.

Approach

Start with two simultaneous points, one for red and one for blue. Keep track of the color.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

10.02.2023

1162. As Far from Land as Possible medium

blog post

    fun maxDistance(grid: Array<IntArray>): Int = with(ArrayDeque<Pair<Int, Int>>()) {
        val n = grid.size
        val visited = hashSetOf<Pair<Int, Int>>()
        fun tryAdd(x: Int, y: Int) {
            if (x < 0 || y < 0 || x >= n || y >= n) return
            (x to y).let { if (visited.add(it)) add(it) }
        }
        for (yStart in 0 until n)
            for (xStart in 0 until n) 
                if (grid[yStart][xStart] == 1) tryAdd(xStart, yStart)
        if (size == n*n) return -1
        var dist = -1
        while(isNotEmpty()) {
            repeat(size) {
                val (x, y) = poll()
                tryAdd(x-1, y)
                tryAdd(x, y-1)
                tryAdd(x+1, y)
                tryAdd(x, y+1)
            }
            dist++
        }
        dist
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/114

Intuition

Let’s do a wave from each land and wait until all the last water cell reached. This cell will be the answer.

Approach

Add all land cells into BFS, then just run it.

Complexity

  • Time complexity: \(O(n^2)\)
  • Space complexity: \(O(n^2)\)

9.02.2023

2306. Naming a Company hard

blog post

    fun distinctNames(ideas: Array<String>): Long {
        // c -> offee
        // d -> onuts
        // t -> ime, offee
        val prefToSuf = Array(27) { hashSetOf<String>() }
        for (idea in ideas)
            prefToSuf[idea[0].toInt() - 'a'.toInt()].add(idea.substring(1, idea.length))
        var count = 0L
        for (i in 0..26) 
            for (j in i + 1..26) 
                count += prefToSuf[i].count { !prefToSuf[j].contains(it) } * prefToSuf[j].count { ! prefToSuf[i].contains(it) }
        return count * 2L
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/113

Intuition

If we group ideas by the suffixes and consider only the unique elements, the result will be the intersection of the sizes of the groups. (To deduce this you must sit and draw, or have a big brain, or just use a hint)

Approach

Group and multiply. Don’t forget to remove repeating elements in each two groups.

Complexity

  • Time complexity: \(O(26^2n)\)
  • Space complexity: \(O(n)\)

8.02.2023

45. Jump Game II medium

blog post

    fun jump(nums: IntArray): Int {
        if (nums.size <= 1) return 0
        val stack = Stack<Int>()
        // 0 1 2 3 4 5 6 7 8 9 1011121314
        // 7 0 9 6 9 6 1 7 9 0 1 2 9 0 3
        //                             *
        //                           *
        //                         * * *
        //                       * * *
        //                     * *
        //                   *    
        //                 * * * * * * *
        //               * * * * * * * *
        //             * *
        //           * * * * * * *
        //         * * * * * * * * * *
        //       * * * * * * *
        //     * * * * * * * * * *
        //   *
        // * * * * * * * *
        // 3 4 3 2 5 4 3
        //             *
        //           * *
        //         * * *
        //       * * *
        //     * * * *
        //   * * * * *
        // * * * *
        // 0 1 2 3 4 5 6 7 8 9 1011
        // 5 9 3 2 1 0 2 3 3 1 0 0
        //                       *
        //                     *
        //                   * *
        //                 * * * *
        //               * * * *
        //             * * *
        //           *
        //         * *
        //       * * *
        //     * * * *
        //   * * * * * * * * * *
        // * * * * * *
        for (pos in nums.lastIndex downTo 0) {
            var canReach = minOf(pos + nums[pos], nums.lastIndex)
            if (canReach == nums.lastIndex) stack.clear()
            while (stack.size > 1 && stack.peek() <= canReach) {
                val top = stack.pop()
                if (stack.peek() > canReach) {
                    stack.push(top)
                    break
                }
            }
            stack.push(pos)
        }
        return stack.size
    }

Join me on Telegram

https://t.me/leetcode_daily_unstoppable/112

Intuition

The dynamic programming solution is trivial, and can be done in \(O(n^2)\). Greedy solution is to scan from back to front and keep only jumps that starts after the current max jump.

Approach

  • use stack to store jumps
  • pop all jumps less than current maxReach
  • pop all except the last that can reach, so don’t break the sequence.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

7.02.2023

904. Fruit Into Baskets medium

blog post

    fun totalFruit(fruits: IntArray): Int {
        if (fruits.size <= 2) return fruits.size
        var type1 = fruits[fruits.lastIndex]
        var type2 = fruits[fruits.lastIndex - 1]
        var count = 2
        var max = 2
        var prevType = type2
        var prevTypeCount = if (type1 == type2) 2 else 1
        for (i in fruits.lastIndex - 2 downTo 0) {
            val type = fruits[i]
            if (type == type1 || type == type2 || type1 == type2) {
                if (type1 == type2 && type != type1) type2 = type
                if (type == prevType) prevTypeCount++
                else prevTypeCount = 1
                count++
            } else {
                count = prevTypeCount + 1
                type2 = type
                type1 = prevType
                prevTypeCount = 1
            }
            max = maxOf(max, count)
            prevType = type
        }
        return max
    }

Join daily telegram

https://t.me/leetcode_daily_unstoppable/111

Intuition

We can scan fruits linearly from the tail and keep only two types of fruits.

Approach

  • careful with corner cases

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

6.02.2023

1470. Shuffle the Array easy

blog post

    fun shuffle(nums: IntArray, n: Int): IntArray {
        val arr = IntArray(nums.size)
        var left = 0
        var right = n
        var i = 0
        while (i < arr.lastIndex) {
            arr[i++] = nums[left++]
            arr[i++] = nums[right++]
        }
        return arr
    }

Telegram

https://t.me/leetcode_daily_unstoppable/110

Intuition

Just do what is asked.

Approach

For simplicity, use two pointers for the source, and one for the destination.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

5.02.2023

438. Find All Anagrams in a String medium

blog post

    fun findAnagrams(s: String, p: String): List<Int> {
        val freq = IntArray(26) { 0 }
        var nonZeros = 0
        p.forEach { 
            val ind = it.toInt() - 'a'.toInt()
            if (freq[ind] == 0) nonZeros++
            freq[ind]--
        }
        val res = mutableListOf<Int>()
        for (i in 0..s.lastIndex) {
            val currInd = s[i].toInt() - 'a'.toInt()
            if (freq[currInd] == 0) nonZeros++
            freq[currInd]++
            if (freq[currInd] == 0) nonZeros--
            if (i >= p.length) {
                val ind = s[i - p.length].toInt() - 'a'.toInt()
                if (freq[ind] == 0) nonZeros++
                freq[ind]--
                if (freq[ind] == 0) nonZeros--
            }
            if (nonZeros == 0) res += i - p.length + 1
        }
        return res
    }

Telegram

https://t.me/leetcode_daily_unstoppable/109

Intuition

We can count frequencies of p and then scan s to match them.

Approach

  • To avoid checking a frequencies arrays, we can count how many frequencies are not matching, and add only when non-matching count is zero.

    Complexity

  • Time complexity: \(O(n)\)

  • Space complexity: \(O(1)\)

4.02.2023

567. Permutation in String medium

blog post

    fun checkInclusion(s1: String, s2: String): Boolean {
        val freq1 = IntArray(26) { 0 }
        s1.forEach {  freq1[it.toInt() - 'a'.toInt()]++  }
        val freq2 = IntArray(26) { 0 }
        for (i in 0..s2.lastIndex) {
            freq2[s2[i].toInt() - 'a'.toInt()]++
            if (i >= s1.length) freq2[s2[i - s1.length].toInt() - 'a'.toInt()]--
            if (Arrays.equals(freq1, freq2)) return true
        }
        return false
    }

Telegram

https://t.me/leetcode_daily_unstoppable/108

Intuition

We can count the chars frequencies in the s1 string and use the sliding window technique to count and compare char frequencies in the s2.

Approach

  • to decrease cost of comparing arrays, we can also use hashing

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

3.02.2023

6. Zigzag Conversion medium

blog post

    fun convert(s: String, numRows: Int): String {
        if (numRows <= 1) return s
        // nr = 5
        //
        // 0    8       16        24
        // 1   7 9     15 17     23 25
        // 2  6  10   14   18   22   26   30
        // 3 5    11 13     19 21     27 29
        // 4       12        20        28
        //
        val indices = Array(numRows) { mutableListOf<Int>() }
        var y = 0
        var dy = 1
        for (i in 0..s.lastIndex) {
            indices[y].add(i)
            if (i > 0 && (i % (numRows - 1)) == 0) dy = -dy
            y += dy
        }
        return StringBuilder().apply {
            indices.forEach { it.forEach { append(s[it]) } }
        }.toString()
    }

Telegram

https://t.me/leetcode_daily_unstoppable/107

Intuition


        // nr = 5
        //
        // 0    8       16        24
        // 1   7 9     15 17     23 25
        // 2  6  10   14   18   22   26   30
        // 3 5    11 13     19 21     27 29
        // 4       12        20        28
        //

We can just simulate zigzag.

Approach

Store simulation result in a [rowsNum][simulation indice] - matrix, then build the result.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

2.02.2023

953. Verifying an Alien Dictionary easy

blog post

    fun isAlienSorted(words: Array<String>, order: String): Boolean {
        val orderChars = Array<Char>(26) { 'a' }
        for (i in 0..25) orderChars[order[i].toInt() - 'a'.toInt()] = (i + 'a'.toInt()).toChar()
        val arr = Array<String>(words.size) { 
            words[it].map { orderChars[it.toInt() - 'a'.toInt()] }.joinToString("")
        }
        
        val sorted = arr.sorted()
        for (i in 0..arr.lastIndex) if (arr[i] != sorted[i]) return false
        return true
    }

Telegram

https://t.me/leetcode_daily_unstoppable/106

Intuition

For the example hello and order hlabcdefgijkmnopqrstuvwxyz we must translate like this: h -> a, l -> b, a -> c and so on. Then we can just use compareTo to check the order.

Approach

Just translate and then sort and compare. (But we can also just scan linearly and compare).

Complexity

  • Time complexity: \(O(n\log_2{n})\)
  • Space complexity: \(O(n)\)

1.02.2023

1071. Greatest Common Divisor of Strings easy

blog post

    fun gcdOfStrings(str1: String, str2: String): String {
        if (str1 == "" || str2 == "") return ""
        if (str1.length == str2.length) return if (str1 == str2) str1 else ""
        fun gcd(a: Int, b: Int): Int {
            return if (a == 0) b
            else gcd(b % a, a)
        }
        val len = gcd(str1.length, str2.length)
        for (i in 0..str1.lastIndex)  if (str1[i] != str1[i % len]) return ""
        for (i in 0..str2.lastIndex)  if (str2[i] != str1[i % len]) return ""
        return str1.substring(0, len)
        
    }

Telegram

https://t.me/leetcode_daily_unstoppable/105

Intuition

Consider the following example: ababab and abab. If we scan them linearly, we see, the common part is abab. Now, we need to check if the last part from the first abab_ab is a part of the common part: ab vs abab. This can be done recursively, and we come to the final consideration: "" vs "ab". That all procedure give us the common divisor - ab. The actual hint is in the method’s name ;)

Approach

We can first find the length of the greatest common divisor, then just check both strings.

Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(n)\)

31.01.2023

1626. Best Team With No Conflicts medium

blog post

    fun bestTeamScore(scores: IntArray, ages: IntArray): Int {
        val dp = Array(scores.size + 1) { IntArray(1001) { -1 }}
        val indices = scores.indices.toMutableList()
        indices.sortWith(compareBy( { scores[it] }, { ages[it] } ))
        fun dfs(curr: Int, prevAge: Int): Int {
            if (curr == scores.size) return 0
            if (dp[curr][prevAge] != -1) return dp[curr][prevAge]
            val ind = indices[curr]
            val age = ages[ind]
            val score = scores[ind]
            val res = maxOf(
                dfs(curr + 1, prevAge),
                if (age < prevAge) 0  else score + dfs(curr + 1, age)
            )
            dp[curr][prevAge] = res
            return res
        }
        return dfs(0, 0)
    }

Telegram

https://t.me/leetcode_daily_unstoppable/103

Intuition

If we sort arrays by score and age, then every next item will be with score bigger than previous. If current age is less than previous, then we can’t take it, as score for current age can’t be bigger than previous. Let’s define dp[i][j] is a maximum score for a team in i..n sorted slice, and j is a maximum age for that team.

Approach

We can use DFS to search all the possible teams and memorize the result in dp cache.

Complexity

  • Time complexity: \(O(n^2)\), we can only visit n by n combinations of pos and age
  • Space complexity: \(O(n^2)\)

30.01.2023

1137. N-th Tribonacci Number easy

blog post

    fun tribonacci(n: Int): Int = if (n < 2) n else {
        var t0 = 0
        var t1 = 1
        var t2 = 1
        repeat(n - 2) {
            t2 += (t0 + t1).also { 
                t0 = t1
                t1 = t2
            }
        }
        t2
    }

Telegram

https://t.me/leetcode_daily_unstoppable/102

Intuition

Just do what is asked.

Approach

  • another way is to use dp cache

    Complexity

  • Time complexity: \(O(n)\)
  • Space complexity: \(O(1)\)

29.01.2023

460. LFU Cache hard

blog post

class LFUCache(val capacity: Int) {
    data class V(val key: Int, val value: Int, val freq: Int)
    val mapKV = mutableMapOf<Int, V>()
    val freqToAccessListOfK = TreeMap<Int, LinkedHashSet<V>>()

    fun get(key: Int): Int {
        val v = mapKV.remove(key)
        if (v == null) return -1
        increaseFreq(v, v.value)
        return v.value
    }

    fun getAccessListForFreq(freq: Int) = freqToAccessListOfK.getOrPut(freq, { LinkedHashSet<V>() })

    fun increaseFreq(v: V, value: Int) {
        val oldFreq = v.freq
        val newFreq = oldFreq + 1
        val newV = V(v.key, value, newFreq)
        mapKV[v.key] = newV
        val accessList = getAccessListForFreq(oldFreq)
        accessList.remove(v)
        if (accessList.isEmpty()) freqToAccessListOfK.remove(oldFreq)
        getAccessListForFreq(newFreq).add(newV)
    }

    fun put(key: Int, value: Int) {
        if (capacity == 0) return
        val oldV = mapKV[key]
        if (oldV == null) {
            if (mapKV.size == capacity) {
                val lowestFreq = freqToAccessListOfK.firstKey()
                val accessList = freqToAccessListOfK[lowestFreq]!!
                val iterator = accessList.iterator()
                val leastFreqV = iterator.next()
                iterator.remove()
                mapKV.remove(leastFreqV.key)
                if (accessList.isEmpty()) freqToAccessListOfK.remove(lowestFreq)
            }
            val v = V(key, value, 1)
            mapKV[key] = v
            getAccessListForFreq(1).add(v)
        } else {
            increaseFreq(oldV, value)
        }
    }

}

Telegram

https://t.me/leetcode_daily_unstoppable/101

Intuition

Let’s store access-time list in a buckets divided by access-count frequencies. We can store each bucked in a TreeMap, that will give us O(1) time to get the least frequent list. For the list we can use LinkedHashSet, that can give us O(1) operations for remove, removeFirst and add and will help to maintain access order.

Approach

  • one thing to note, on each increaseFreq operation we are retrieving a random item from TreeMap, that increases time to O(log(F)), where F is a unique set of frequencies.
  • How many unique access frequencies k we can have if there is a total number of N operations? If sequence 1,2,3...k-1, k is our unique set, then 1+2+3+...+(k-1)+k = N. Or: \(1+2+3+\cdots+k=\sum_{n=1}^{k}i = k(k-1)/2 = N\) so, \(k = \sqrt{N}\)

    Complexity

  • Time complexity: \(O(\log_2(\sqrt{N}))\)
  • Space complexity: \(O(\log_2(\sqrt{N}))\)

28.01.2023

352. Data Stream as Disjoint Intervals hard

blog post

class SummaryRanges() {
    data class Node(var start: Int, var end: Int, var next: Node? = null) 

    val root = Node(-1, -1)

    fun mergeWithNext(n: Node?): Boolean {
        if (n == null) return false
        val curr = n
        val next = n.next
        if (next == null) return false
        val nextNext = next.next
        if (next.start - curr.end <= 1) {
            curr.end = next.end
            curr.next = nextNext
            return true
        }
        return false
    }

    fun addNum(value: Int) {
        var n = root
        while (n.next != null && n.next!!.start < value) n = n.next!!
        if (value in n.start..n.end) return
        n.next = Node(value, value, n.next)
        if (n != root && mergeWithNext(n)) 
            mergeWithNext(n)
        else 
            mergeWithNext(n.next)
    }

    fun getIntervals(): Array<IntArray> {
        val list = mutableListOf<IntArray>()
        var n = root.next
        while (n != null) {
            list.add(intArrayOf(n.start, n.end)) 
            n = n.next
        }
        return list.toTypedArray()
    }

}

Telegram

https://t.me/leetcode_daily_unstoppable/100

Intuition

In Kotlin there is no way around to avoid the O(n) time of an operation while building the result array. And there is no way to insert to the middle of the array in a less than O(n) time. So, the only way is to use the linked list, and to walk it linearly.

Approach

  • careful with merge

    Complexity

  • Time complexity: \(O(IN)\), I - number of the intervals
  • Space complexity: \(O(I)\)

27.01.2023

472. Concatenated Words hard

blog post

    data class Trie(val ch: Char = '.', var isWord: Boolean = false) {
        val next = Array<Trie?>(26) { null }
        fun ind(c: Char) = c.toInt() - 'a'.toInt()
        fun exists(c: Char) = next[ind(c)] != null
        operator fun get(c: Char): Trie {
            val ind = ind(c)
            if (next[ind] == null) next[ind] = Trie(c)
            return next[ind]!!
        }
    }
    fun findAllConcatenatedWordsInADict(words: Array<String>): List<String> {
        val trie = Trie()
        words.forEach { word ->
            var t = trie
            word.forEach { t = t[it] }
            t.isWord = true
        }
        val res = mutableListOf<String>()
        words.forEach { word ->
            var tries = ArrayDeque<Pair<Trie,Int>>()
            tries.add(trie to 0)
            for (c in word) {
                repeat(tries.size) {
                    val (t, wc) = tries.poll()
                    if (t.exists(c)) {
                        val curr = t[c]
                        if (curr.isWord)  tries.add(trie to (wc + 1))
                        tries.add(curr to wc)
                    }
                }
            }
            if (tries.any { it.second > 1 && it.first === trie } ) res.add(word)
        }
        return res
    }

Telegram

https://t.me/leetcode_daily_unstoppable/99

Intuition

When we scan a word we must know if current suffix is a word. Trie data structure will help.

Approach

  • first, scan all the words, and fill the Trie
  • next, scan again, and for each suffix begin a new scan from the root of the trie
  • preserve a word count for each of the possible suffix concatenation

    Complexity

  • Time complexity: \(O(nS)\), S - is a max suffix count in one word
  • Space complexity: \(O(n)\)

26.01.2023

787. Cheapest Flights Within K Stops medium

https://t.me/leetcode_daily_unstoppable/98

blog post

    fun findCheapestPrice(n: Int, flights: Array<IntArray>, src: Int, dst: Int, k: Int): Int {
        var dist = IntArray(n) { Int.MAX_VALUE }
        dist[src] = 0
        repeat(k + 1) {
            val nextDist = dist.clone()
            flights.forEach { (from, to, price) ->
                if (dist[from] != Int.MAX_VALUE && dist[from] + price < nextDist[to]) 
                    nextDist[to] = dist[from] + price
            }
            dist = nextDist
        }
        return if (dist[dst] == Int.MAX_VALUE) -1 else dist[dst]
    }

Intuition

DFS and Dijkstra gives TLE. As we need to find not just shortest path price, but only for k steps, naive Bellman-Ford didn’t work. Let’s define dist, where dist[i] - the shortest distance from src node to i-th node. We initialize it with MAX_VALUE, and dist[src] is 0 by definition. Next, we walk exactly k steps, on each of them, trying to minimize price. If we have known distance to node a, dist[a] != MAX. And if there is a link to node b with price(a,b), then we can optimize like this dist[b] = min(dist[b], dist[a] + price(a,b)). Because we’re starting from a single node dist[0], we will increase distance only once per iteration. So, making k iterations made our path exactly k steps long.

Approach

  • by the problem definition, path length is k+1, not just k
  • we can’t optimize a path twice in a single iteration, because then it will overreach to the next step before the current is finished.
  • That’s why we only compare distance from the previous step.

Space: O(kE), Time: O(k)

25.01.2023

2359. Find Closest Node to Given Two Nodes medium

https://t.me/leetcode_daily_unstoppable/97

blog post

    fun closestMeetingNode(edges: IntArray, node1: Int, node2: Int): Int {
        val distances = mutableMapOf<Int, Int>()
        var n = node1
        var dist = 0
        while (n != -1) {
            if (distances.contains(n)) break
            distances[n] = dist
            n = edges[n]
            dist++
        }
        n = node2
        dist = 0
        var min = Int.MAX_VALUE
        var res = -1
        while (n != -1) {
            if (distances.contains(n)) {
                val one = distances[n]!!
                val max = maxOf(one, dist)
                if (max < min || max == min && n < res) {
                    min = max
                    res = n
                }
            }
            val tmp = edges[n]
            edges[n] = -1
            n = tmp
            dist++
        }
        return res
    }

image.png

We can walk with DFS and remember all distances, then compare them and choose those with minimum of maximums.

  • we can use visited set, or modify an input
  • corner case: don’t forget to also store starting nodes

Space: O(n), Time: O(n)

24.01.2023

909. Snakes and Ladders medium

https://t.me/leetcode_daily_unstoppable/96

blog post

    fun snakesAndLadders(board: Array<IntArray>): Int {
        fun col(pos: Int): Int {
            return if (((pos/board.size) % 2) == 0) 
                    (pos % board.size)
                else 
                    (board.lastIndex - (pos % board.size))
        }
        val last = board.size * board.size
        var steps = 0
        val visited = mutableSetOf<Int>()
        with(ArrayDeque<Int>().apply { add(1) }) {
            while (isNotEmpty() && steps <= last) {
                repeat(size) {
                    var curr = poll()
                    val jump = board[board.lastIndex - (curr-1)/board.size][col(curr-1)]
                    if (jump != -1) curr = jump
                    if (curr == last) return steps
                    for (i in 1..6)  
                        if (visited.add(curr + i) && curr + i <= last) add(curr + i) 
                }
                steps++
            }
        }
        return -1
    }

In each step, we can choose the best outcome, so we need to travel all of them in the parallel and calculate steps number. This is a BFS.

We can avoid that strange order by iterating it and store into the linear array. Or just invent a formula for row and column by given index.

Space: O(n^2), Time: O(n^2), n is a grid size

23.01.2023

997. Find the Town Judge easy

https://t.me/leetcode_daily_unstoppable/95

blog post

    fun findJudge(n: Int, trust: Array<IntArray>): Int {
        val judges = mutableMapOf<Int, MutableSet<Int>>()
        for (i in 1..n) judges[i] = mutableSetOf()
        val notJudges = mutableSetOf<Int>()
        trust.forEach { (from, judge) ->
            judges[judge]!! += from
            notJudges += from
        }
        judges.forEach { (judge, people) ->
            if (people.size == n - 1 
                && !people.contains(judge) 
                && !notJudges.contains(judge)) 
                return judge
        }
        return -1
    }

We need to count how much trust have each judge and also exclude all judges that have trust in someone.

  • use map and set
  • there is a better solution with just counting of trust, but it is not that clear to understand and prove

Space: O(max(N, T)), Time: O(max(N, T))

22.01.2023

131. Palindrome Partitioning medium

https://t.me/leetcode_daily_unstoppable/93

blog post

    fun partition(s: String): List<List<String>> {
        val dp = Array(s.length) { BooleanArray(s.length) { false } }
        for (from in s.lastIndex downTo 0) 
            for (to in from..s.lastIndex) 
                dp[from][to] = s[from] == s[to] && (from == to || from == to - 1 || dp[from+1][to-1])
        val res = mutableListOf<List<String>>()
        fun dfs(pos: Int, partition: MutableList<String>) {
            if (pos == s.length) res += partition.toList()
            for (i in pos..s.lastIndex) 
                if (dp[pos][i]) {
                    partition += s.substring(pos, i+1)
                    dfs(i+1, partition)
                    partition.removeAt(partition.lastIndex)
                }
        }
        dfs(0, mutableListOf())
        return res
    }

First, we need to be able to quickly tell if some range a..b is a palindrome. Let’s dp[a][b] indicate that range a..b is a palindrome. Then the following is true: dp[a][b] = s[a] == s[b] && dp[a+1][b-1], also two corner cases, when a == b and a == b-1. For example, “a” and “aa”.

  • Use dp for precomputing palindrome range answers.
  • Try all valid partitions with backtracking.

Space: O(2^N), Time: O(2^N)

21.01.2023

93. Restore IP Addresses medium

https://t.me/leetcode_daily_unstoppable/92

blog post

    fun restoreIpAddresses(s: String): List<String> {
	val res = mutableSetOf<String>()
	fun dfs(pos: Int, nums: MutableList<Int>) {
		if (pos == s.length || nums.size > 4) {
			if (nums.size == 4) res += nums.joinToString(".")
			return
		}
		var n = 0

		for (i in pos..s.lastIndex) {
			n = n*10 + s[i].toInt() - '0'.toInt()
			if (n > 255) break
			nums += n
			dfs(i + 1, nums)
			nums.removeAt(nums.lastIndex)
			if (n == 0) break
		}
	}
	dfs(0, mutableListOf())
	return res.toList()
}

So, the size of the problem is small. We can do full DFS. At every step, choose either take a number or split. Add to the solution if the result is good.

  • use set for results
  • use backtracking to save some space

Some optimizations:

  • exit early when nums.size > 5,
  • use math to build a number instead of parsing substring

Space: O(2^N), Time: O(2^N)

20.01.2023

491. Non-decreasing Subsequences medium

https://t.me/leetcode_daily_unstoppable/91

blog post

    fun findSubsequences(nums: IntArray): List<List<Int>> {
        val res = mutableSetOf<List<Int>>()
        fun dfs(pos: Int, currList: MutableList<Int>) {
            if (currList.size > 1) res += currList.toList()
            if (pos == nums.size) return
            val currNum = nums[pos]
            //not add
            dfs(pos + 1, currList)
            //to add
            if (currList.isEmpty() || currList.last()!! <= currNum) {
                currList += currNum
                dfs(pos + 1, currList)
                currList.removeAt(currList.lastIndex)
            }
        }
        dfs(0, mutableListOf())
        return res.toList()
    }

Notice the size of the problem, we can do a brute force search for all solutions. Also, we only need to store the unique results, so we can store them in a set.

  • we can reuse pre-filled list and do backtracking on the return from the DFS.

Space: O(2^N) to store the result, Time: O(2^N) for each value we have two choices, and we can build a binary tree of choices with the 2^n number of elements.

19.01.2023

974. Subarray Sums Divisible by K medium

https://t.me/leetcode_daily_unstoppable/90

blog post

    fun subarraysDivByK(nums: IntArray, k: Int): Int {
        // 4 5 0 -2 -3 1    k=5   count
        // 4                4:1   0
        //   9              4:2   +1
        //     9            4:3   +2
        //       7          2:1   
        //          4       4:4   +3
        //             5    0:2   +1
        // 2 -2 2 -4       k=6
        // 2               2:1
        //    0            0:2    +1
        //      2          2:2    +1
        //        -2       2:3    +2
        // 1 2 13 -2 3  k=7
        // 1
        //   3
        //     16
        //        14
        //          17 (17-1*7= 10, 17-2*7=3, 17-3*7=-4, 17-4*7 = -11)
        val freq = mutableMapOf<Int, Int>()
        freq[0] = 1
        var sum = 0 
        var res = 0
        nums.forEach {
            sum += it
            var ind = (sum % k)
            if (ind < 0) ind += k
            val currFreq = freq[ind] ?: 0
            res += currFreq
            freq[ind] = 1 + currFreq
        }
        return res
    }

We need to calculate a running sum. For every current sum, we need to find any subsumes that are divisible by k, so sum[i]: (sum[i] - sum[any prev]) % k == 0. Or, sum[i] % k == sum[any prev] % k. Now, we need to store all sum[i] % k values, count them and add to result.

We can save frequency in a map, or in an array [0..k], because all the values are from that range.

Space: O(N), Time: O(N)

18.01.2023

918. Maximum Sum Circular Subarray medium

https://t.me/leetcode_daily_unstoppable/89

blog post

    fun maxSubarraySumCircular(nums: IntArray): Int {
        var maxEndingHere = 0
        var maxEndingHereNegative = 0
        var maxSoFar = Int.MIN_VALUE
        var total = nums.sum()
        nums.forEach {
            maxEndingHere += it
            maxEndingHereNegative += -it
            maxSoFar = maxOf(maxSoFar, maxEndingHere, if (total == -maxEndingHereNegative) Int.MIN_VALUE else total+maxEndingHereNegative)
            if (maxEndingHere < 0) {
                maxEndingHere = 0
            }
            if (maxEndingHereNegative < 0) {
                maxEndingHereNegative = 0
            }
        }
        return maxSoFar
    }

Simple Kadane’s Algorithm didn’t work when we need to keep a window of particular size. One idea is to invert the problem and find the minimum sum and subtract it from the total.

One corner case:

  • we can’t subtract all the elements when checking the negative sum.

Space: O(1), Time: O(N)

17.01.2023

926. Flip String to Monotone Increasing medium

https://t.me/leetcode_daily_unstoppable/88

blog post

    fun minFlipsMonoIncr(s: String): Int {
        // 010110  dp0  dp1    min
        // 0       0    0      0
        //  1      1    0      1
        //   0     1    1      1
        //    1    2    1      1
        //     1   3    1      1
        //      0  3    2      2
        var dp0 = 0
        var dp1 = 0

        for (i in 0..s.lastIndex) {
            dp0 = if (s[i] == '0') dp0 else 1 + dp0
            dp1 = if (s[i] == '1') dp1 else 1 + dp1
            if (dp0 <= dp1) dp1 = dp0
        }
        
        return minOf(dp0, dp1)
    }

We can propose the following rule: let’s define dp0[i] is a min count of flips from 1 to 0 in the 0..i interval. Let’s also define dp1[i] is a min count of flips from 0 to 1 in the 0..i interval. We observe that dp0[i] = dp0[i-1] + (flip one to zero? 1 : 0) and dp1[i] = dp1[i-1] + (flip zero to one? 1 : 0). One special case: if on the interval 0..i one-to-zero flips count is less than zero-to-one then we prefer to flip everything to zeros, and dp1[i] in that case becomes dp0[i].

Just write down what is described above.

  • dp arrays can be simplified to single variables.

Space: O(1), Time: O(N)

16.01.2023

57. Insert Interval medium

https://t.me/leetcode_daily_unstoppable/87

blog post

    fun insert(intervals: Array<IntArray>, newInterval: IntArray): Array<IntArray> {
        val res = mutableListOf<IntArray>()
        var added = false
        fun add() {
            if (!added) {
                added = true
                if (res.isNotEmpty() && res.last()[1] >= newInterval[0]) {
                    res.last()[1] = maxOf(res.last()[1], newInterval[1])
                } else res += newInterval
            }
        }
        intervals.forEach { interval -> 
            if (newInterval[0] <= interval[0]) add()
            
            if (res.isNotEmpty() && res.last()[1] >= interval[0]) {
                res.last()[1] = maxOf(res.last()[1], interval[1])
            } else  res += interval
        }
        add()
       
        return res.toTypedArray()
    }

There is no magic, just be careful with corner cases.

Make another list, and iterate interval, merging them and adding at the same time.

  • don’t forget to add newInterval if it is not added after iteration.

Space: O(N), Time: O(N)

15.01.2023

2421. Number of Good Paths hard

https://t.me/leetcode_daily_unstoppable/86

blog post

    fun numberOfGoodPaths(vals: IntArray, edges: Array<IntArray>): Int {
        if (edges.size == 0) return vals.size
        edges.sortWith(compareBy(  { maxOf( vals[it[0]], vals[it[1]] ) }  ))
        val uf = IntArray(vals.size) { it }
        val freq = Array(vals.size) { mutableMapOf(vals[it] to 1) }
        fun find(x: Int): Int {
            var p = x
            while (uf[p] != p) p = uf[p]
            uf[x] = p
            return p
        }
        fun union(a: Int, b: Int): Int {
            val rootA = find(a)
            val rootB = find(b)
            if (rootA == rootB) return 0
            uf[rootA] = rootB
            val vMax = maxOf(vals[a], vals[b]) // if we connect tree [1-3] to tree [2-1], only `3` matters
            val countA = freq[rootA][vMax] ?:0
            val countB = freq[rootB][vMax] ?:0
            freq[rootB][vMax] = countA + countB
            return countA * countB
        }
        return edges.map { union(it[0], it[1])}.sum()!! + vals.size
    }

The naive solution with single DFS and merging frequency maps gives TLE. Now, use hint, and they tell you to sort edges and use Union-Find :) The idea is to connect subtrees, but walk them from smallest to the largest of value. When we connect two subtrees, we look at the maximum of each subtree. The minimum values don’t matter because the path will break at the maximums by definition of the problem.

Use IntArray for Union-Find, and also keep frequencies maps for each root.

Space: O(NlogN), Time: O(N)

14.01.2023

1061. Lexicographically Smallest Equivalent String medium

https://t.me/leetcode_daily_unstoppable/85

blog post

    fun smallestEquivalentString(s1: String, s2: String, baseStr: String): String {
        val uf = IntArray(27) { it }
        fun find(ca: Char): Int {
            val a = ca.toInt() - 'a'.toInt()
            var x = a
            while (uf[x] != x) x = uf[x]
            uf[a] = x
            return x
        }
        fun union(a: Char, b: Char) {
            val rootA = find(a)
            val rootB = find(b)
            if (rootA != rootB) {
                val max = maxOf(rootA, rootB)
                val min = minOf(rootA, rootB)
                uf[max] = min
            }
        }
        for (i in 0..s1.lastIndex) union(s1[i], s2[i])
        return baseStr.map { (find(it) + 'a'.toInt()).toChar() }.joinToString("")
    }

We need to find connected groups, the best way is to use the Union-Find.

Iterate over strings and connect each of their chars.

  • to find a minimum, we can select the minimum of the current root.

Space: O(N) for storing a result, Time: O(N)

13.01.2023

2246. Longest Path With Different Adjacent Characters hard

https://t.me/leetcode_daily_unstoppable/84

blog post

    fun longestPath(parent: IntArray, s: String): Int {
        val graph = mutableMapOf<Int, MutableList<Int>>()
        for (i in 1..parent.lastIndex) 
            if (s[i] != s[parent[i]]) graph.getOrPut(parent[i], { mutableListOf() }) += i
        
        var maxLen = 0
        fun dfs(curr: Int): Int {
            parent[curr] = curr
            var max1 = 0
            var max2 = 0
            graph[curr]?.forEach { 
                val childLen = dfs(it) 
                if (childLen > max1) {
                    max2 = max1
                    max1 = childLen
                } else if (childLen > max2) max2 = childLen
            }
            val childChainLen = 1 + (max1 + max2)
            val childMax = 1 + max1
            maxLen = maxOf(maxLen, childMax, childChainLen)
            return childMax
        }
        for (i in 0..parent.lastIndex) if (parent[i] != i) dfs(i)

        return maxLen
    }

Longest path is a maximum sum of the two longest paths of the current node.

Let’s build a graph and then recursively iterate it by DFS. We need to find two largest results from the children DFS calls.

  • make parent[i] == i to store a visited state

Space: O(N), Time: O(N), in DFS we visit each node only once.

12.01.2023

1519. Number of Nodes in the Sub-Tree With the Same Label medium

https://t.me/leetcode_daily_unstoppable/83

blog post

fun countSubTrees(n: Int, edges: Array<IntArray>, labels: String): IntArray {
	val graph = mutableMapOf<Int, MutableList<Int>>()
	edges.forEach { (from, to) ->
		graph.getOrPut(from, { mutableListOf() }) += to
		graph.getOrPut(to, { mutableListOf() }) += from
	}
	val answer = IntArray(n) { 0 }
	fun dfs(node: Int, parent: Int, counts: IntArray) {
		val index = labels[node].toInt() - 'a'.toInt()
		val countParents = counts[index]
		counts[index]++
		graph[node]?.forEach {
			if (it != parent) {
				dfs(it, node, counts)
			}
		}
		answer[node] = counts[index] - countParents
	}
	dfs(0, 0, IntArray(27) { 0 })
	return answer
}

First, we need to build a graph. Next, just do DFS and count all 'a'..'z' frequencies in the current subtree.

For building a graph let’s use a map, and for DFS let’s use a recursion.

  • use parent node instead of the visited set
  • use in-place counting and subtract count before

Space: O(N), Time: O(N)

11.01.2023

1443. Minimum Time to Collect All Apples in a Tree medium

https://t.me/leetcode_daily_unstoppable/82

blog post

    fun minTime(n: Int, edges: Array<IntArray>, hasApple: List<Boolean>): Int {
        val graph = mutableMapOf<Int, MutableList<Int>>()
        edges.forEach { (from, to) ->
            graph.getOrPut(to, { mutableListOf() }) += from
            graph.getOrPut(from, { mutableListOf() }) += to
        }

        val queue = ArrayDeque<Int>()
        queue.add(0)
        val parents = IntArray(n+1) { it }
        while (queue.isNotEmpty()) {
            val node = queue.poll()
            graph[node]?.forEach {
                if (parents[it] == it && it != 0) {
                    parents[it] = node
                    queue.add(it)
                }
            }
        }
        var time = 0
        hasApple.forEachIndexed { i, has ->
            if (has) {
                var node = i
                while (node != parents[node]) {
                    val parent = parents[node]
                    parents[node] = node
                    node = parent
                    time++
                }
            }
        }
        return time * 2
    }

We need to count all paths from apples to 0-node and don’t count already walked path.

  • notice, that problem definition doesn’t state the order of the edges in edges array. We need to build the tree first.

First, build the tree, let it be a parents array, where parent[i] is a parent of the i. Walk graph with DFS and write the parents. Next, walk hasApple list and for each apple count parents until reach node 0 or already visited node. To mark a node as visited, make it the parent of itself.

Space: O(N), Time: O(N)

10.01.2023

100. Same Tree easy

https://t.me/leetcode_daily_unstoppable/81

blog post

fun isSameTree(p: TreeNode?, q: TreeNode?): Boolean =  p == null && q == null || 
            p?.`val` == q?.`val` && isSameTree(p?.left, q?.left) && isSameTree(p?.right, q?.right)

Check for the current node and repeat for the children. Let’s write one-liner

Space: O(logN) for stack, Time: O(n)

9.01.2023

144. Binary Tree Preorder Traversal easy

https://t.me/leetcode_daily_unstoppable/80

blog post

class Solution {
    fun preorderTraversal(root: TreeNode?): List<Int> {
        val res = mutableListOf<Int>()
        var node = root
        while(node != null) {
            res.add(node.`val`)
            if (node.left != null) {
                if (node.right != null) {
                    var rightmost = node.left!!
                    while (rightmost.right != null) rightmost = rightmost.right
                    rightmost.right = node.right
                }
                node = node.left
            } else if (node.right != null) node = node.right
            else node = null
        }
        return res
    }
    fun preorderTraversalStack(root: TreeNode?): List<Int> {
        val res = mutableListOf<Int>()
        var node = root
        val rightStack = ArrayDeque<TreeNode>()
        while(node != null) {
            res.add(node.`val`)
            if (node.left != null) {
                if (node.right != null) {
                    rightStack.addLast(node.right!!) // <-- this step can be replaced with Morris
                    // traversal.
                }
                node = node.left
            } else if (node.right != null) node = node.right
            else if (rightStack.isNotEmpty()) node = rightStack.removeLast()
            else node = null
        }
        return res
    }
    fun preorderTraversalRec(root: TreeNode?): List<Int> = mutableListOf<Int>().apply {
        root?.let {
            add(it.`val`)
            addAll(preorderTraversal(it.left))
            addAll(preorderTraversal(it.right))
        }
    }
        
}

Recursive solution is a trivial. For stack solution, we need to remember each right node. Morris’ solution use the tree modification to save each right node in the rightmost end of the left subtree. Let’s implement them all.

Space: O(logN) for stack, O(1) for Morris’, Time: O(n)

8.01.2023

149. Max Points on a Line hard

https://t.me/leetcode_daily_unstoppable/79

blog post

    fun maxPoints(points: Array<IntArray>): Int {
        if (points.size == 1) return 1
        val pointsByTan = mutableMapOf<Pair<Double, Double>, HashSet<Int>>()
        fun gcd(a: Int, b: Int): Int {
            return if (b == 0) a else gcd(b, a%b)
        }
        for (p1Ind in points.indices) {
            val p1 = points[p1Ind]
            for (p2Ind in (p1Ind+1)..points.lastIndex) {
                val p2 = points[p2Ind]
                val x1 = p1[0]
                val x2 = p2[0]
                val y1 = p1[1]
                val y2 = p2[1]
                var dy = y2 - y1
                var dx = x2 - x1
                val greatestCommonDivider = gcd(dx, dy)
                dy /= greatestCommonDivider
                dx /= greatestCommonDivider
                val tan = dy/dx.toDouble()
                val b = if (dx == 0) x1.toDouble() else (x2*y1 - x1*y2 )/(x2-x1).toDouble()
                val line = pointsByTan.getOrPut(tan to b, { HashSet() })
                line.add(p1Ind)
                line.add(p2Ind)
            }
        }
        return pointsByTan.values.maxBy { it.size }?.size?:0
    }

Just do the linear algebra to find all the lines through each pair of points. Store slope and b coeff in the hashmap. Also, compute gcd to find precise slope. In this case it works for double precision slope, but for bigger numbers we need to store dy and dx separately in Int precision.

Space: O(n^2), Time: O(n^2)

7.01.2023

134. Gas Station medium

https://t.me/leetcode_daily_unstoppable/78

blog post

    fun canCompleteCircuit(gas: IntArray, cost: IntArray): Int {
        var sum = 0
        var minSum = gas[0]
        var ind = -1
        for (i in 0..gas.lastIndex) {
            sum += gas[i] - cost[i]
            if (sum < minSum) {
                minSum = sum
                ind = (i+1) % gas.size
            }
        }
        return if (sum < 0) -1 else ind
    }

We can start after the station with the minimum decrease in gasoline. image.png Calculate running gasoline volume and find the minimum of it. If the total net gasoline is negative, there is no answer.

Space: O(1), Time: O(N)

6.01.2023

1833. Maximum Ice Cream Bars medium

https://t.me/leetcode_daily_unstoppable/77

blog post

    fun maxIceCream(costs: IntArray, coins: Int): Int {
       costs.sort() 
       var coinsRemain = coins
       var iceCreamCount = 0
       for (i in 0..costs.lastIndex) {
           coinsRemain -= costs[i]
           if (coinsRemain < 0) break
           iceCreamCount++
       }
       return iceCreamCount
    }

The maximum ice creams would be if we take as many minimum costs as possible Sort the costs array, then greedily iterate it and buy ice creams until all the coins are spent.

Space: O(1), Time: O(NlogN) (there is also O(N) solution based on count sort)

5.01.2023

452. Minimum Number of Arrows to Burst Balloons medium

https://t.me/leetcode_daily_unstoppable/75

blog post

    fun findMinArrowShots(points: Array<IntArray>): Int {
        if (points.isEmpty()) return 0
        if (points.size == 1) return 1
        Arrays.sort(points, Comparator<IntArray> { a, b -> 
            if (a[0] == b[0]) a[1].compareTo(b[1]) else a[0].compareTo(b[0]) })
        var arrows = 1
        var arrX = points[0][0]
        var minEnd = points[0][1]
        for (i in 1..points.lastIndex) {
            val (start, end) = points[i]
            if (minEnd < start) {
                arrows++
                minEnd = end
            }
            if (end < minEnd) minEnd = end
            arrX = start
        }
        return arrows
    }

The optimal strategy to achieve the minimum number of arrows is to find the maximum overlapping intervals. For this task, we can sort the points by their start and end coordinates and use line sweep technique. Overlapping intervals are separate if their minEnd is less than start of the next interval. minEnd - the minimum of the end’s of the overlapping intervals. Let’s move the arrow to each start interval and fire a new arrow if this start is greater than minEnd.

  • for sorting without Int overflowing, use compareTo instead of subtraction
  • initial conditions are better to initialize with the first interval and iterate starting from the second

Space: O(1), Time: O(NlogN)

4.01.2023

2244. Minimum Rounds to Complete All Tasks medium

https://t.me/leetcode_daily_unstoppable/74

blog post

    fun minimumRounds(tasks: IntArray): Int {
        val counts = mutableMapOf<Int, Int>()
        tasks.forEach { counts[it] = 1 + counts.getOrDefault(it, 0)}
        var round = 0
        val cache = mutableMapOf<Int, Int>()
        fun fromCount(count: Int): Int {
            if (count == 0) return 0
            if (count < 0 || count == 1) return -1
            return if (count % 3 == 0) {
                count/3
            } else {
                cache.getOrPut(count, {
                    var v = fromCount(count - 3)
                    if (v == -1) v = fromCount(count - 2)
                    if (v == -1) -1 else 1 + v
                })
            }
        }
        counts.values.forEach { 
            val rounds = fromCount(it)
            if (rounds == -1) return -1
            round += rounds
        }
        return round
    }

For the optimal solution, we must take as many 3’s of tasks as possible, then take 2’s in any order. First, we need to count how many tasks of each type there are. Next, we need to calculate the optimal rounds for the current tasks type count. There is a math solution, but ultimately we just can do DFS

Space: O(N), Time: O(N), counts range is always less than N

3.01.2023

944. Delete Columns to Make Sorted easy

https://t.me/leetcode_daily_unstoppable/73

blog post

    fun minDeletionSize(strs: Array<String>): Int =
       (0..strs[0].lastIndex).asSequence().count { col ->
           (1..strs.lastIndex).asSequence().any { strs[it][col] < strs[it-1][col] }
        } 

Just do what is asked. We can use Kotlin’s sequence api.

Space: O(1), Time: O(wN)

2.01.2023

520. Detect Capital easy

https://t.me/leetcode_daily_unstoppable/72

blog post

    fun detectCapitalUse(word: String): Boolean =
       word.all { Character.isUpperCase(it) } ||
       word.all { Character.isLowerCase(it) } ||
       Character.isUpperCase(word[0]) && word.drop(1).all { Character.isLowerCase(it) }

We can do this optimally by checking the first character and then checking all the other characters in a single pass. Or we can write a more understandable code that directly translates from the problem description. Let’s write one-liner.

Space: O(1), Time: O(N)

1.01.2023

290. Word Pattern easy

https://t.me/leetcode_daily_unstoppable/71

blog post

    fun wordPattern(pattern: String, s: String): Boolean {
        val charToWord = Array<String>(27) { "" }
        val words = s.split(" ")
        if (words.size != pattern.length) return false
        words.forEachIndexed { i, w ->
            val cInd = pattern[i].toInt() - 'a'.toInt()

            if (charToWord[cInd] == "") {
                charToWord[cInd] = w
            } else if (charToWord[cInd] != w) return false
        }
        charToWord.sort()
        for (i in 1..26) 
            if (charToWord[i] != "" && charToWord[i] == charToWord[i-1]) 
                return false
        return true
    }

Each word must be in 1 to 1 relation with each character in the pattern. We can check this rule.

Use string[27] array for char -> word relation and also check each char have a unique word assigned.

  • don’t forget to check lengths

Space: O(N), Time: O(N)

31.12.2022

980. Unique Paths III hard

https://t.me/leetcode_daily_unstoppable/69

blog post

    fun uniquePathsIII(grid: Array<IntArray>): Int {
        var countEmpty = 1
        var startY = 0
        var startX = 0
        for (y in 0..grid.lastIndex) {
            for (x in 0..grid[0].lastIndex) {
                when(grid[y][x]) {
                    0 -> countEmpty++
                    1 -> { startY = y; startX = x}
                    else -> Unit
                }
            }
        }
        fun dfs(y: Int, x: Int): Int {
            if (y < 0 || x < 0 || y >= grid.size || x >= grid[0].size) return 0
            val curr = grid[y][x]
            if (curr == 2) return if (countEmpty == 0) 1 else 0
            if (curr == -1) return 0
            grid[y][x] = -1
            countEmpty--
            val res =  dfs(y-1, x) + dfs(y, x-1) + dfs(y+1, x) + dfs(y, x+1)
            countEmpty++
            grid[y][x] = curr
            return res
        }
        return dfs(startY, startX)
    }

There is only 20x20 cells, we can brute-force the solution. We can use DFS, and count how many empty cells passed. To avoid visiting cells twice, modify grid cell and then modify it back, like backtracking.

Space: O(1), Time: O(4^N)

30.12.2022

797. All Paths From Source to Target medium

https://t.me/leetcode_daily_unstoppable/68

blog post

    fun allPathsSourceTarget(graph: Array<IntArray>): List<List<Int>> {
        val res = mutableListOf<List<Int>>()
        val currPath = mutableListOf<Int>()
        fun dfs(curr: Int) {
            currPath += curr
            if (curr == graph.lastIndex) res += currPath.toList()
            graph[curr].forEach { dfs(it) }
            currPath.removeAt(currPath.lastIndex)
        }
        dfs(0)
        return res
    }

We must find all the paths, so there is no shortcuts to the visiting all of them. One technique is backtracking - reuse existing visited list of nodes.

Space: O(VE), Time: O(VE)

29.12.2022

1834. Single-Threaded CPU medium

https://t.me/leetcode_daily_unstoppable/67

blog post

    fun getOrder(tasks: Array<IntArray>): IntArray {
        val pqSource = PriorityQueue<Int>(compareBy(
            { tasks[it][0] },
            { tasks[it][1] },
            { it }
        ))
        (0..tasks.lastIndex).forEach { pqSource.add(it) }
        val pq = PriorityQueue<Int>(compareBy(
            { tasks[it][1] },
            { it }
        ))
        val res = IntArray(tasks.size) { 0 }
        var time = 1 
        for(resPos in 0..tasks.lastIndex) {
            while (pqSource.isNotEmpty() && tasks[pqSource.peek()][0] <= time) {
                pq.add(pqSource.poll())
            }
            if (pq.isEmpty()) {
                //idle
                pq.add(pqSource.poll())
                time = tasks[pq.peek()][0]
            }
            //take task
            val taskInd = pq.poll()
            val task = tasks[taskInd]
            time += task[1]
            res[resPos] = taskInd
        }
        return res
    }

First we need to sort tasks by their availability (and other rules), then take tasks one by one and add them to another sorted set/heap where their start time doesn’t matter, but running time and order does. When we take the task from the heap, we increase the time and fill in the heap.

  • use two heaps, one for the source of tasks, another for the current available tasks.
  • don’t forget to increase time to the nearest task if all of them unavailable

Space: O(n), Time: O(nlogn)

28.12.2022

1962. Remove Stones to Minimize the Total medium

https://t.me/leetcode_daily_unstoppable/66

blog post

    fun minStoneSum(piles: IntArray, k: Int): Int {
        val pq = PriorityQueue<Int>() 
        var sum = 0
        piles.forEach { 
            sum += it
            pq.add(-it) 
        }
        for (i in 1..k) {
            if (pq.isEmpty()) break
            val max = -pq.poll()
            if (max == 0) break 
            val newVal = Math.round(max/2.0).toInt()
            sum -= max - newVal
            pq.add(-newVal)
        }
        return sum
    }

By the problem definition, intuitively the best strategy is to reduce the maximum each time. Use PriorityQueue to keep track of the maximum value and update it dynamically.

  • one can use variable sum and update it each time.

Space: O(n), Time: O(nlogn)

27.12.2022

2279. Maximum Bags With Full Capacity of Rocks medium

https://t.me/leetcode_daily_unstoppable/65

blog post

    fun maximumBags(capacity: IntArray, rocks: IntArray, additionalRocks: Int): Int {
       val inds = Array<Int>(capacity.size) { it }
       inds.sortWith(Comparator { a,b -> capacity[a]-rocks[a] - capacity[b] + rocks[b] })
       var rocksRemain = additionalRocks
       var countFull = 0
       for (i in 0..inds.lastIndex) {
           val toAdd = capacity[inds[i]] - rocks[inds[i]]
           if (toAdd > rocksRemain) break
           rocksRemain -= toAdd
           countFull++
       }
       return countFull
    }

We can logically deduce that the optimal solution is to take first bags with the smallest empty space. Make an array of indexes and sort it by difference between capacity and rocks. Then just simulate rocks addition to each bug from the smallest empty space to the largest.

Space: O(n), Time: O(nlogn)

26.12.2022

55. Jump Game medium

https://t.me/leetcode_daily_unstoppable/64

blog post

    fun canJump(nums: IntArray): Boolean {
       var minInd = nums.lastIndex 
       for (i in nums.lastIndex - 1 downTo 0) {
           if (nums[i] + i >= minInd) minInd = i
       }
       return minInd == 0
    }

For any position i we can reach the end if there is a minInd such that nums[i] + i >= minInd and minInd is a known to be reaching the end. We can run from the end and update minInd - minimum index reaching the end.

Space: O(1), Time: O(N)

25.12.2022

2389. Longest Subsequence With Limited Sum easy

https://t.me/leetcode_daily_unstoppable/63

blog post

    fun answerQueries(nums: IntArray, queries: IntArray): IntArray {
       nums.sort() 
       for (i in 1..nums.lastIndex) nums[i] += nums[i-1]
       return IntArray(queries.size) {
           val ind = nums.binarySearch(queries[it])
           if (ind < 0) -ind-1 else ind+1
       }
    }

We can logically deduce that for the maximum number of arguments we need to take as much as possible items from the smallest to the largest. We can sort items. Then pre-compute sums[i] = sum from [0..i]. Then use binary search target sum in sums. Also, can modify nums but that’s may be not necessary.

Space: O(N), Time: O(NlogN)

24.12.2022

790. Domino and Tromino Tiling medium

https://t.me/leetcode_daily_unstoppable/62

blog post

  fun numTilings(n: Int): Int {
        val cache = Array<Array<Array<Long>>>(n) { Array(2) { Array(2) { -1L }}}
        fun dfs(pos: Int, topFree: Int, bottomFree: Int): Long {
            return when {
                pos > n -> 0L
                pos == n -> if (topFree==1 && bottomFree==1) 1L else 0L
                else -> {
                    var count = cache[pos][topFree][bottomFree]
                    if (count == -1L) {
                        count = 0L
                        when {
                            topFree==1 && bottomFree==1 -> {
                                count += dfs(pos+1, 1, 1) // vertical
                                count += dfs(pos+1, 0, 0) // horizontal
                                count += dfs(pos+1, 1, 0) // tromino top
                                count += dfs(pos+1, 0, 1) // tromino bottom
                            }
                            topFree==1 -> {
                                count += dfs(pos+1, 0, 0) // tromino
                                count += dfs(pos+1, 1, 0) // horizontal
                            }
                            bottomFree==1 -> {
                                count += dfs(pos+1, 0, 0) // tromino
                                count += dfs(pos+1, 0, 1) // horizontal
                            }
                        else -> {
                                count += dfs(pos+1, 1, 1) // skip
                            }
                        }

                        count = count % 1_000_000_007L
                    }
                    cache[pos][topFree][bottomFree] = count
                    count
                }
            }
        }
        return dfs(0, 1, 1).toInt()
    }

We can walk the board horizontally and monitor free cells. On each step, we can choose what figure to place. When end reached and there are no free cells, consider that a successful combination. Result depends only on the current position and on the top-bottom cell combination.* just do dfs+memo

  • use array for a faster cache

Space: O(N), Time: O(N) - we only visit each column 3 times

23.12.2022

309. Best Time to Buy and Sell Stock with Cooldown medium

https://t.me/leetcode_daily_unstoppable/61

blog post

    data class K(val a:Int, val b: Boolean, val c:Boolean)
    fun maxProfit(prices: IntArray): Int {
        val cache = mutableMapOf<K, Int>()
        fun dfs(pos: Int, canSell: Boolean, canBuy: Boolean): Int {
            return if (pos == prices.size) 0
                else cache.getOrPut(K(pos, canSell, canBuy), {
                    val profitSkip = dfs(pos+1, canSell, !canSell)
                    val profitSell = if (canSell) {prices[pos] + dfs(pos+1, false, false)} else 0
                    val profitBuy = if (canBuy) {-prices[pos] + dfs(pos+1, true, false)} else 0
                    maxOf(profitSkip, profitBuy, profitSell)
                })
        }
        return dfs(0, false, true)
    }

Progress from dfs solution to memo. DFS solution - just choose what to do in this step, go next, then compare results and peek max.

Space: O(N), Time: O(N)

22.12.2022

834. Sum of Distances in Tree hard

https://t.me/leetcode_daily_unstoppable/60

blog post

    fun sumOfDistancesInTree(n: Int, edges: Array<IntArray>): IntArray {
        val graph = mutableMapOf<Int, MutableList<Int>>()
        edges.forEach { (from, to) -> 
            graph.getOrPut(from, { mutableListOf() }) += to
            graph.getOrPut(to, { mutableListOf() }) += from
        }
        val counts = IntArray(n) { 1 }
        val sums = IntArray(n) { 0 }
        fun distSum(pos: Int, visited: Int) {
            graph[pos]?.forEach {
                if (it != visited) {
                    distSum(it, pos)
                    counts[pos] += counts[it]
                    sums[pos] += counts[it] + sums[it]
                }
            }
        }
        fun dfs(pos: Int, visited: Int) {
            graph[pos]?.forEach {
                if (it != visited) {
                    sums[it] = sums[pos] - counts[it] + (n - counts[it])
                    dfs(it, pos)
                }
            }
        }
        distSum(0, -1)
        dfs(0, -1)
        return sums
    }

We can do the job for item #0, then we need to invent a formula to reuse some data when we change the node.

How to mathematically prove formula for a new sum: image

image.png Store count of children in a counts array, and sum of the distances to children in a dist array. In a first DFS traverse from a node 0 and fill the arrays. In a second DFS only modify dist based on previous computed dist value, using formula: sum[curr] = sum[prev] - count[curr] + (N - count[curr])

Space: O(N), Time: O(N)

21.12.2022

886. Possible Bipartition medium

https://t.me/leetcode_daily_unstoppable/59

blog post

fun possibleBipartition(n: Int, dislikes: Array<IntArray>): Boolean {
	val love = IntArray(n+1) { it }
	fun leader(x: Int): Int {
		var i = x
		while (love[i] != i) i = love[i]
		love[x] = i
		return i
	}
	val hate = IntArray(n+1) { -1 }
	dislikes.forEach { (one, two) ->
		val leaderOne = leader(one)
		val leaderTwo = leader(two)
		val enemyOfOne = hate[leaderOne]
		val enemyOfTwo = hate[leaderTwo]
		if (enemyOfOne != -1 && enemyOfOne == enemyOfTwo) return false
		if (enemyOfOne != -1) {
			love[leader(enemyOfOne)] = leaderTwo
		}
		if (enemyOfTwo != -1) {
			love[leader(enemyOfTwo)] = leaderOne
		}
		hate[leaderOne] = leaderTwo
		hate[leaderTwo] = leaderOne
	}
	return true
}

We need somehow to union people that hate the same people. We can do it making someone a leader of a group and make just leaders to hate each other.

Keep track of the leaders hating each other in the hate array, and people loving their leader in love array. (love array is basically a Union-Find).

  • also use path compression for leader method

Space: O(N), Time: O(N) - adding to Union-Find is O(1) amortised

20.12.2022

841. Keys and Rooms medium

https://t.me/leetcode_daily_unstoppable/58

blog post

    fun canVisitAllRooms(rooms: List<List<Int>>): Boolean {
       val visited = hashSetOf(0)
       with(ArrayDeque<Int>()) {
           add(0)
           while(isNotEmpty()) {
               rooms[poll()].forEach {
                   if (visited.add(it)) add(it)
               }
           }
       }
       return visited.size == rooms.size
    }

We need to visit each room, and we have positions of the other rooms and a start position. This is a DFS problem. Keep all visited rooms numbers in a hash set and check the final size. Other solution is to use boolean array and a counter of the visited rooms.

Space: O(N) - for queue and visited set, Time: O(N) - visit all the rooms once

19.12.2022

1971. Find if Path Exists in Graph easy

https://t.me/leetcode_daily_unstoppable/57

blog post

    fun validPath(n: Int, edges: Array<IntArray>, source: Int, destination: Int): Boolean {
        if (source == destination) return true
        val graph = mutableMapOf<Int, MutableList<Int>>()
        edges.forEach { (from, to) -> 
            graph.getOrPut(from, { mutableListOf() }).add(to)
            graph.getOrPut(to, { mutableListOf() }).add(from)
        }
        val visited = mutableSetOf<Int>()
        with(ArrayDeque<Int>()) {
            add(source)
            var depth = 0
            while(isNotEmpty() && ++depth < n) {
                repeat(size) {
                    graph[poll()]?.forEach {
                        if (it == destination) return true
                        if (visited.add(it)) add(it) 
                    }
                }
            }
        }
        return false
    }

BFS will do the job. Make node to nodes map, keep visited set and use queue for BFS.

  • also path can’t be longer than n elements

Space: O(N), Time: O(N)

18.12.2022

739. Daily Temperatures medium

https://t.me/leetcode_daily_unstoppable/55

blog post

    fun dailyTemperatures(temperatures: IntArray): IntArray {
       val stack = Stack<Int>() 
       val res = IntArray(temperatures.size) { 0 }
       for (i in temperatures.lastIndex downTo 0) {
           while(stack.isNotEmpty() && temperatures[stack.peek()] <= temperatures[i]) stack.pop()
           if (stack.isNotEmpty()) {
               res[i] = stack.peek() - i
           }
           stack.push(i)
       }
       return res
    }

Intuitively, we want to go from the end of the array to the start and keep the maximum value. But, that doesn’t work, because we must also store smaller numbers, as they are closer in distance. For example, 4 3 5 6, when we observe 4 we must compare it to 5, not to 6. So, we store not just max, but increasing max: 3 5 6, and throw away all numbers smaller than current, 3 < 4 - pop().

We will iterate in reverse order, storing indexes in increasing by temperatures stack.

Space: O(N), Time: O(N)

17.12.2022

150. Evaluate Reverse Polish Notation medium

https://t.me/leetcode_daily_unstoppable/54

blog post

    fun evalRPN(tokens: Array<String>): Int = with(Stack<Int>()) {
        tokens.forEach {
            when(it) {
                "+" -> push(pop() + pop())
                "-" -> push(-pop() + pop())
                "*" -> push(pop() * pop())
                "/" -> with(pop()) { push(pop()/this) }
                else -> push(it.toInt())
            }
      }
      pop()
    }

Reverse polish notations made explicitly for calculation using stack. Just execute every operation immediately using last two numbers in the stack and push the result.

  • be aware of the order of the operands

Space: O(N), Time: O(N)

16.12.2022

232. Implement Queue using Stacks easy

https://t.me/leetcode_daily_unstoppable/53

blog post

class MyQueue() {
	val head = Stack<Int>()
	val tail = Stack<Int>()

	//  []       []
	//  1 2 3 4 -> 4 3 2 - 1
	//  5         4 3 2
	//            4 3 2 5
	fun push(x: Int) {
		head.push(x)
	}

	fun pop(): Int {
		peek()

		return tail.pop()
	}

	fun peek(): Int {
		if (tail.isEmpty()) while(head.isNotEmpty()) tail.push(head.pop())

		return tail.peek()
	}

	fun empty(): Boolean = head.isEmpty() && tail.isEmpty()

}

One stack for the head of the queue and other for the tail. When we need to do pop we first drain from one stack to another, so items order will be restored.

  • we can skip rotation on push if we fill tail only when its empty

Space: O(1), Time: O(1)

15.12.2022

1143. Longest Common Subsequence medium

https://t.me/leetcode_daily_unstoppable/52

blog post

    fun longestCommonSubsequence(text1: String, text2: String): Int {
        val cache = Array(text1.length + 1) { IntArray(text2.length + 1) { -1 } }
        fun dfs(pos1: Int, pos2: Int): Int {
            if (pos1 == text1.length) return 0
            if (pos2 == text2.length) return 0
            val c1 = text1[pos1]
            val c2 = text2[pos2]
            if (cache[pos1][pos2] != -1) return cache[pos1][pos2]
            val res = if (c1 == c2) {
                    1 + dfs(pos1 + 1, pos2 + 1)
                } else {
                    maxOf(dfs(pos1, pos2+1), dfs(pos1+1, pos2))
                }
            cache[pos1][pos2] = res
            return res
        }
        return dfs(0, 0)
    }

We can walk the two strings simultaneously and compare their chars. If they are the same, the optimal way will be to use those chars and continue exploring next. If they are not, we have two choices: use the first char and skip the second or skip the first but use the second. Also, observing our algorithm we see, the result so far is only dependent of the positions from which we begin to search (and all the remaining characters). And also see that the calls are repetitive. That mean we can cache the result. (meaning this is a dynamic programming solution). Use depth first search by starting positions and memoize results in a two dimension array. Another approach will be bottom up iteration and filling the same array.

Space: O(N^2), Time: O(N^2)

14.12.2022

198. House Robber medium

https://t.me/leetcode_daily_unstoppable/51

blog post

    fun rob(nums: IntArray): Int {
        val cache = mutableMapOf<Int, Int>()
        fun dfs(pos: Int): Int {
            if (pos > nums.lastIndex) return 0
            return cache.getOrPut(pos) {
                maxOf(nums[pos] + dfs(pos+2), dfs(pos+1))
            }
        } 
        return dfs(0)
    }

Exploring each house one by one we can make a decision to rob or not to rob. The result is only depends on our current position (and all houses that are remaining to rob) and decision, so we can memoize it based on position.

We can use memoization or walk houses bottom up.

Space: O(N), Time: O(N)

13.12.2022

931. Minimum Falling Path Sum medium

https://t.me/leetcode_daily_unstoppable/50

blog post

    fun minFallingPathSum(matrix: Array<IntArray>): Int {
       for (y in matrix.lastIndex-1 downTo 0) {
           val currRow = matrix[y]
           val nextRow = matrix[y+1]
           for (x in 0..matrix[0].lastIndex) {
               val left = if (x > 0) nextRow[x-1] else Int.MAX_VALUE
               val bottom = nextRow[x]
               val right = if (x < matrix[0].lastIndex) nextRow[x+1] else Int.MAX_VALUE
               val minSum = currRow[x] + minOf(left, bottom, right)
               currRow[x] = minSum
           }
       } 
       return matrix[0].min()!!
    }

There is only three ways from any cell to it’s siblings. We can compute all three paths sums for all cells in a row so far. And then choose the smallest. Iterate over rows and compute prefix sums of current + minOf(left min sum, bottom min sum, right min sum)

Space: O(N), Time: O(N^2)

12.12.2022

70. Climbing Stairs easy

https://t.me/leetcode_daily_unstoppable/49

blog post

    val cache = mutableMapOf<Int, Int>()
    fun climbStairs(n: Int): Int = when {
        n < 1  -> 0
        n == 1 -> 1
        n == 2 -> 2
        else -> cache.getOrPut(n) {
            climbStairs(n-1) + climbStairs(n-2)
        }
    }

You can observe that result is only depend on input n. And also that result(n) = result(n-1) + result(n-2). Just use memoization for storing already solved inputs.

Space: O(N), Time: O(N)

11.12.2022

124. Binary Tree Maximum Path Sum hard

https://t.me/leetcode_daily_unstoppable/48

blog post

    fun maxPathSum(root: TreeNode?): Int {
        fun dfs(root: TreeNode): Pair<Int, Int> {
            val lt = root.left
            val rt = root.right
            if (lt == null && rt == null) return root.`val` to root.`val`
            if (lt == null || rt == null) {
                val sub = dfs(if (lt == null) rt else lt)
                val currRes = root.`val` + sub.second
                val maxRes = maxOf(sub.first, currRes, root.`val`)
                val maxPath = maxOf(root.`val`, root.`val` + sub.second)
                return maxRes to maxPath
            } else {
                val left = dfs(root.left)
                val right = dfs(root.right)
                val currRes1 = root.`val` + left.second + right.second
                val currRes2 = root.`val`
                val currRes3 = root.`val` + left.second
                val currRes4 = root.`val` + right.second
                val max1 = maxOf(currRes1, currRes2)
                val max2 = maxOf(currRes3, currRes4)
                val maxRes = maxOf(left.first, right.first, maxOf(max1, max2))
                val maxPath = maxOf(root.`val`, root.`val` + maxOf(left.second, right.second))
                return maxRes to maxPath
            }
        }
        return if (root == null) 0 else dfs(root).first
    }

Space: O(logN), Time: O(N)

10.12.2022

1339. Maximum Product of Splitted Binary Tree medium

https://t.me/leetcode_daily_unstoppable/47

blog post


    fun maxProduct(root: TreeNode?): Int {
        fun sumDfs(root: TreeNode?): Long {
            return if (root == null) 0L
            else with(root) { `val`.toLong() + sumDfs(left) + sumDfs(right) }
        }
        val total = sumDfs(root)
        fun dfs(root: TreeNode?) : Pair<Long, Long> {
            if (root == null) return Pair(0,0)
            val left = dfs(root.left)
            val right = dfs(root.right)
            val sum = left.first + root.`val`.toLong() + right.first
            val productLeft = left.first * (total - left.first) 
            val productRight = right.first * (total - right.first)
            val prevProductMax = maxOf(right.second, left.second)
            return sum to maxOf(productLeft, productRight, prevProductMax)
        }
        return (dfs(root).second % 1_000_000_007L).toInt()
    }

Just iterate over all items and compute all products. We need to compute total sum before making the main traversal.

Space: O(logN), Time: O(N)

9.12.2022

1026. Maximum Difference Between Node and Ancestor medium

https://t.me/leetcode_daily_unstoppable/46

blog post


    fun maxAncestorDiff(root: TreeNode?): Int {
        root?: return 0

        fun dfs(root: TreeNode, min: Int = root.`val`, max: Int = root.`val`): Int {
            val v = root.`val`
            val currDiff = maxOf(Math.abs(v - min), Math.abs(v - max))
            val currMin = minOf(min, v)
            val currMax = maxOf(max, v)
            val leftDiff = root.left?.let { dfs(it, currMin, currMax) } ?: 0
            val rightDiff = root.right?.let { dfs(it, currMin, currMax) } ?: 0
            return maxOf(currDiff, leftDiff, rightDiff)
        }
        
        return dfs(root)
    }

Based on math we can assume, that max difference is one of the two: (curr - max so far) or (curr - min so far). Like, for example, let our curr value be 3, and from all visited we have min 0 and max 7.


 0--3---7

  • we can write helper recoursive method and compute max and min so far

Space: O(logN), Time: O(N)

8.12.2022

872. Leaf-Similar Trees easy

https://t.me/leetcode_daily_unstoppable/45


    fun leafSimilar(root1: TreeNode?, root2: TreeNode?): Boolean {
        fun dfs(root: TreeNode?): List<Int> {
            return when {
                root == null -> listOf()
                root.left == null && root.right == null -> listOf(root.`val`)
                else -> dfs(root.left) + dfs(root.right)
            }
        }
        
        return dfs(root1) == dfs(root2)
    }

There is only 200 items, so we can concatenate lists. One optimization would be to collect only first tree and just compare it to the second tree while doing the inorder traverse.

Space: O(N), Time: O(N)

7.12.2022

938. Range Sum of BST easy

https://t.me/leetcode_daily_unstoppable/44


    fun rangeSumBST(root: TreeNode?, low: Int, high: Int): Int =
	if (root == null) 0 else
		with(root) {
			(if (`val` in low..high) `val` else 0) +
				(if (`val` < low) 0 else rangeSumBST(left, low, high)) +
				(if (`val` > high) 0 else rangeSumBST(right, low, high))
		}

  • be careful with ternary operations, better wrap them in a brackets

Space: O(log N), Time: O(R), r - is a range [low, high]

6.12.2022

328. Odd Even Linked List medium

https://t.me/leetcode_daily_unstoppable/43


       // 1 2
    fun oddEvenList(head: ListNode?): ListNode? {
       var odd = head //1
       val evenHead = head?.next
       var even = head?.next //2
       while(even!=null) { //2
           val oddNext = odd?.next?.next //null
           val evenNext = even?.next?.next //null
           odd?.next = oddNext // 1->null
           even?.next = evenNext //2->null
           if (oddNext != null) odd = oddNext //
           even = evenNext // null
       }
       odd?.next = evenHead // 1->2
       return head //1->2->null
    }

  • be careful and store evenHead in a separate variable

Space: O(1), Time: O(n)

5.12.2022

876. Middle of the Linked List easy

https://t.me/leetcode_daily_unstoppable/42


  fun middleNode(head: ListNode?, fast: ListNode? = head): ListNode? =
        if (fast?.next == null) head else middleNode(head?.next, fast?.next?.next)

  • one-liner, but in the interview (or production) I would prefer to write a loop

Space: O(n), Time: O(n)

4.12.2022

2256. Minimum Average Difference medium

https://t.me/leetcode_daily_unstoppable/41


    fun minimumAverageDifference(nums: IntArray): Int {
        var sum = 0L
        nums.forEach { sum += it.toLong() }
        var leftSum = 0L
        var min = Long.MAX_VALUE
        var minInd = 0
        for (i in 0..nums.lastIndex) {
            val leftCount = (i+1).toLong()
            leftSum += nums[i].toLong()
            val front = leftSum/leftCount
            val rightCount = nums.size.toLong() - leftCount
            val rightSum = sum - leftSum
            val back = if (rightCount == 0L) 0L else rightSum/rightCount
            val diff = Math.abs(front - back)
            if (diff < min) {
                min = diff
                minInd = i
            }
        }
        return minInd
    }

Intuition

Two pointers, one for even, one for odd indexes.

Approach

To avoid mistakes you need to be verbose, and don’t skip operations:

  • store evenHead in a separate variable
  • don’t switch links before both pointers jumped
  • don’t make odd pointer null
  • try to run for simple input 1->2->null by yourself

Space: O(1), Time: O(n)

3.12.2022

451. Sort Characters By Frequency medium

https://t.me/leetcode_daily_unstoppable/40


    fun frequencySort(s: String): String =
        s.groupBy { it }
        .values
        .map { it to it.size }
        .sortedBy { -it.second }
        .map { it.first }
        .flatten()
        .joinToString("")

Very simple task, can be written in a functional style. Space: O(n), Time: O(n)

2.12.2022

https://leetcode.com/problems/determine-if-two-strings-are-close/ medium

https://t.me/leetcode_daily_unstoppable/39


    // cabbba -> c aa bbb -> 1 2 3 
    // a bb ccc -> 1 2 3
    // uau
    // ssx
    fun closeStrings(word1: String, word2: String, 
         f: (String) -> List<Int> = { it.groupBy { it }.values.map { it.size }.sorted() }
    ): Boolean = f(word1) == f(word2) && word1.toSet() == word2.toSet()

That is a simple task, you just need to know what exactly you asked for. Space: O(n), Time: O(n)

1.12.2022

1704. Determine if String Halves Are Alike easy

https://t.me/leetcode_daily_unstoppable/38


    fun halvesAreAlike(s: String): Boolean {
        val vowels = setOf('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')
        var c1 = 0
        var c2 = 0
        s.forEachIndexed { i, c -> 
          if (c in vowels) {
              if (i < s.length / 2) c1++ else c2++
            }
        }
        return c1 == c2
    }

Just do what is asked.

O(N) time, O(1) space

30.11.2022

1207. Unique Number of Occurrences easy

https://t.me/leetcode_daily_unstoppable/36


fun uniqueOccurrences(arr: IntArray): Boolean {
	val counter = mutableMapOf<Int, Int>()
	arr.forEach { n -> counter[n] = 1 + (counter[n] ?: 0) }
	val freq = mutableSetOf<Int>()
	return !counter.values.any { count -> !freq.add(count) }
}

Nothing interesting, just count and filter.

O(N) time, O(N) space

29.11.2022

380. Insert Delete GetRandom O(1) medium

https://t.me/leetcode_daily_unstoppable/35


class RandomizedSet() {
    val rnd = Random(0)
    val list = mutableListOf<Int>()
    val vToInd = mutableMapOf<Int, Int?>()
    fun insert(v: Int): Boolean {
        if (!vToInd.contains(v)) {
            vToInd[v] = list.size
            list.add(v)
            return true
        }
        return false
    }
    fun remove(v: Int): Boolean {
        val ind = vToInd[v] ?: return false
        val prevLast = list[list.lastIndex]
        list[ind] = prevLast
        vToInd[prevLast] = ind
        list.removeAt(list.lastIndex)
        vToInd.remove(v)
        return true
    }
    fun getRandom(): Int = list[rnd.nextInt(list.size)]
}

The task is simple, one trick is to remove elements from the end of the list, and replacing item with the last one. Some thoughts:

  • don’t optimize lines of code, that can backfire. You can use syntax sugar, clever operations inlining, but also can shoot in the foot.

O(1) time, O(N) space

28.11.2022

2225. Find Players With Zero or One Losses medium

https://t.me/leetcode_daily_unstoppable/34


    fun findWinners(matches: Array<IntArray>): List<List<Int>> {
        val winners = mutableMapOf<Int, Int>()
        val losers = mutableMapOf<Int, Int>()
        matches.forEach { (w, l) ->
            winners[w] = 1 + (winners[w]?:0)
            losers[l] = 1 + (losers[l]?:0)
        }
        return listOf(
            winners.keys
                .filter { !losers.contains(it) }
                .sorted(),
            losers
                .filter { (k, v) -> v == 1 }
                .map { (k, v) -> k}
                .sorted()
        )
    }

Just do what is asked.

O(NlogN) time, O(N) space

27.11.2022

446. Arithmetic Slices II - Subsequence hard

https://t.me/leetcode_daily_unstoppable/33


    fun numberOfArithmeticSlices(nums: IntArray): Int {
        // 0 1 2 3 4 5 
        // 1 2 3 1 2 3                diff = 1
        //   ^     ^ *                dp[5][diff] = 
        //   |     |  \__ curr        1 + dp[1][diff] +
        //  prev   |                  1 + dp[4][diff]
        //        prev
        // 
        val dp = Array(nums.size) { mutableMapOf<Long, Long> () }
        for (curr in 0..nums.lastIndex) {
            for (prev in 0 until curr) {
                val diff = nums[curr].toLong() - nums[prev].toLong()
                dp[curr][diff] = 1 + (dp[curr][diff]?:0L) + (dp[prev][diff]?:0L)
            }
        }
        return dp.map { it.values.sum()!! }.sum().toInt() - (nums.size)*(nums.size-1)/2
    }

dp[i][d] is the number of subsequences in range [0..i] with difference = d


array: "1 2 3 1 2 3"
For items  1  2  curr = 2:
diff = 1,  dp = 1
For items  1  2  3  curr = 3:
diff = 2,  dp = 1
diff = 1,  dp = 2
For items  1  2  3  1  curr = 1:
diff = 0,  dp = 1
diff = -1,  dp = 1
diff = -2,  dp = 1
For items  1  2  3  1  2  curr = 2:
diff = 1,  dp = 2
diff = 0,  dp = 1
diff = -1,  dp = 1
For items  1  2  3  1  2  3  curr = 3:
diff = 2,  dp = 2
diff = 1,  dp = 5
diff = 0,  dp = 1

and finally, we need to subtract all the sequences of length 2 and 1, count of them is (n)*(n-1)/2

O(N^2) time, O(N^2) space

26.11.2022

1235. Maximum Profit in Job Scheduling hard

https://t.me/leetcode_daily_unstoppable/32


    fun jobScheduling(startTime: IntArray, endTime: IntArray, profit: IntArray): Int {
        val n = startTime.size
        val inds = Array<Int>(n) { it }
        inds.sortWith (Comparator<Int> { a, b -> 
            if (startTime[a] == startTime[b])
                endTime[a] - endTime[b]
            else
                startTime[a] - startTime[b]
        })
        val maxProfit = IntArray(n) { 0 }
        maxProfit[n-1] = profit[inds[n-1]]
        for (i in n-2 downTo 0) {
            val ind = inds[i]
            val end = endTime[ind]
            val prof = profit[ind]
            
            var lo = l + 1
            var hi = n - 1
            var nonOverlapProfit = 0
            while (lo <= hi) {
                val mid = lo + (hi - lo) / 2
                if (end <= startTime[inds[mid]]) {
                    nonOverlapProfit = maxOf(nonOverlapProfit, maxProfit[mid])
                    hi = mid - 1
                } else lo = mid + 1
            }
            maxProfit[i] = maxOf(prof + nonOverlapProfit, maxProfit[i+1])
        }
        return maxProfit[0]
    }

Use the hints from the description. THis cannot be solved greedily, because you need to find next non-overlapping job. Dynamic programming equation: from last job to the current, result is max of next result and current + next non-overlapping result.


f(i) = max(f(i+1), profit[i] + f(j)), where j is the first non-overlapping job after i.

Also, instead of linear search for non overlapping job, use binary search.

O(NlogN) time, O(N) space

25.11.2022

907. Sum of Subarray Minimums medium


    data class V(val v: Int, val count: Int)
    fun sumSubarrayMins(arr: IntArray): Int {
        val M = 1_000_000_007
        // 1 2 3 4 2 2 3 4
        //  1 2 3 2 2 2 3
        //   1 2 2 2 2 2
        //    1 2 2 2 2
        //     1 2 2 2
        //      1 2 2
        //       1 2
        //        1
        // f(1) = 1
        // f(2) = 2>1 ? f(1) + [1, 2]
        // f(3) = 3>2 ? f(2) + [1, 2, 3]
        // f(4) = 4>3 ? f(3) + [1, 2, 3, 4]
        // f(2) = 2<4 ? f(4) + [1, 2, 2, 2, 2] (1, 2, 3, 4 -> 3-2, 4-2, +2)
        // f(2) = 2=2 ? f(2) + [1, 2, 2, 2, 2, 2]
        // f(3) = 3>2 ? f(2) + [1, 2, 2, 2, 2, 2, 3]
        // f(4) = 4>3 ? f(3) + [1, 2, 2, 2, 2, 2, 3, 4]
        // 3 1 2 4    f(3) = 3    sum = 3  stack: [3]
        //  1 1 2     f(1): 3 > 1 , remove V(3,1), sum = sum - 3 + 1*2= 2, f=3+2=5, [(1,2)]
        //   1 1      f(2): 2>1, sum += 2 = 4, f+=4=9
        //    1       f(4): 4>2, sum+=4=8, f+=8=17
        val stack = Stack<V>()
        var f = 0
        var sum = 0
        arr.forEach { n ->
            var countRemoved = 0
            while (stack.isNotEmpty() && stack.peek().v > n) {
                val v = stack.pop()
                countRemoved += v.count
                var removedSum = (v.v*v.count) % M
                if (removedSum < 0) removedSum = M + removedSum
                sum = (sum - removedSum) % M
                if (sum < 0) sum = sum + M
            }
            val count = countRemoved + 1
            stack.add(V(n, count))
            sum = (sum + (n * count) % M) % M
            f = (f + sum) % M
            
        }
        return f
    }

First attempt is to build an N^2 tree of minimums, comparing adjacent elements row by row and finding a minimum. That will take O(N^2) time and gives TLE. Next observe that there is a repetition of the results if we computing result for each new element: result = previous result + some new elements. That new elements are also have a law of repetition: sum = current element + if (current element < previous element) count of previous elements * current element else previous sum We can use a stack to keep lowest previous elements, all values in stack must be less than current element.

O(N) time, O(N) space

24.11.2022

79. Word Search medium


    fun exist(board: Array<CharArray>, word: String): Boolean {
        fun dfs(y: Int, x: Int, pos: Int): Boolean {
            if (pos == word.length) return true
            if (y < 0 || x < 0 || y == board.size || x == board[0].size) return false
            val c = board[y][x]
            if (c != word[pos]) return false
            board[y][x] = '.'
            val res = dfs(y-1, x, pos+1)
                   || dfs(y+1, x, pos+1)
                   || dfs(y, x-1, pos+1)
                   || dfs(y, x+1, pos+1)
            board[y][x] = c
            return res
        }
        for (y in 0..board.lastIndex) {
            for (x in 0..board[0].lastIndex) {
                if (dfs(y, x, 0)) return true
            }
        }
        return false
    }

We can brute force this problem. Backtracking help to preserve memory.

Complexity: O(MNW) Memory: O(W)

23.11.2022

https://leetcode.com/problems/valid-sudoku/ medium


    fun isValidSudoku(board: Array<CharArray>): Boolean {
        val cell9 = arrayOf(0 to 0, 0 to 1, 0 to 2, 
                            1 to 0, 1 to 1, 1 to 2, 
                            2 to 0, 2 to 1, 2 to 2)
        val starts = arrayOf(0 to 0, 0 to 3, 0 to 6, 
                             3 to 0, 3 to 3, 3 to 6, 
                             6 to 0, 6 to 3, 6 to 6)
        return !starts.any { (sy, sx) ->
                val visited = HashSet<Char>()
                cell9.any { (dy, dx) ->
                    val c = board[sy+dy][sx+dx]
                    c != '.' && !visited.add(c)
                }
            } && !board.any { row -> 
                val visited = HashSet<Char>()
                row.any { it != '.' && !visited.add(it) }
            } && !(0..8).any { x ->
                val visited = HashSet<Char>()
                (0..8).any { board[it][x] != '.' && !visited.add(board[it][x]) }
            }
    }

This is an easy problem, just do what is asked.

Complexity: O(N) Memory: O(N), N = 81, so it O(1)

22.11.2022

https://leetcode.com/problems/perfect-squares/ medium


    val cache = mutableMapOf<Int, Int>()
    fun numSquares(n: Int): Int {
        if (n < 0) return -1
        if (n == 0) return 0
        if (cache[n] != null) return cache[n]!!
        var min = Int.MAX_VALUE
        for (x in Math.sqrt(n.toDouble()).toInt() downTo 1) {
            val res = numSquares(n - x*x)
            if (res != -1) {
                min = minOf(min, 1 + res)
            }
        }
        if (min == Int.MAX_VALUE) min = -1
        cache[n] = min
        return min
    }

The problem gives stable answers for any argument n. So, we can use memoization technique and search from the biggest square to the smallest one.

Complexity: O(Nsqrt(N)) Memory: O(N)

21.11.2022

https://leetcode.com/problems/nearest-exit-from-entrance-in-maze/ medium


    fun nearestExit(maze: Array<CharArray>, entrance: IntArray): Int {
        val queue = ArrayDeque<Pair<Int, Int>>()
        queue.add(entrance[1] to entrance[0])
        maze[entrance[0]][entrance[1]] = 'x'
        var steps = 1
        val directions = intArrayOf(-1, 0, 1, 0, -1)
        while(queue.isNotEmpty()) {
            repeat(queue.size){
                val (x, y) = queue.poll()
                for (i in 1..directions.lastIndex) {
                    val nx = x + directions[i-1]
                    val ny = y + directions[i]
                    if (nx in 0..maze[0].lastIndex &&
                            ny in 0..maze.lastIndex &&
                            maze[ny][nx] == '.') {
                        if (nx == 0 || 
                                ny == 0 || 
                                nx == maze[0].lastIndex || 
                                ny == maze.lastIndex) return steps
                        maze[ny][nx] = 'x'
                        queue.add(nx to ny)
                    }
                }
            }
            steps++
        }
        
        return -1
    }

Just do BFS.

  • we can modify input matrix, so we can use it as visited array

Complexity: O(N), N - number of cells in maze Memory: O(N)

20.11.2022

https://leetcode.com/problems/basic-calculator/ hard


    fun calculate(s: String): Int {
        var i = 0
        var sign = 1
        var eval = 0
        while (i <= s.lastIndex) {
            val chr = s[i]
            if (chr == '(') {
                //find the end
                var countOpen = 0
                for (j in i..s.lastIndex) {
                    if (s[j] == '(') countOpen++
                    if (s[j] == ')') countOpen--
                    if (countOpen == 0) {
                        //evaluate substring
                        eval += sign * calculate(s.substring(i+1, j)) // [a b)
                        sign = 1
                        i = j
                        break
                    }
                }
            } else if (chr == '+') {
                sign = 1
            } else if (chr == '-') {
                sign = -1
            } else if (chr == ' ') {
                //nothing
            } else {
                var num = (s[i] - '0').toInt()
                for (j in (i+1)..s.lastIndex) {
                    if (s[j].isDigit()) {
                        num = num * 10 + (s[j] - '0').toInt()
                        i = j
                    } else  break
                }
                eval += sign * num
                sign = 1
            }
            i++
        }
        return eval
    }

This is a classic calculator problem, nothing special.

  • be careful with the indexes

Complexity: O(N) Memory: O(N), because of the recursion, worst case is all the input is brackets

19.11.2022

https://leetcode.com/problems/erect-the-fence/ hard


    fun outerTrees(trees: Array<IntArray>): Array<IntArray> {
        if (trees.size <= 3) return trees
        trees.sortWith(Comparator { a, b -> if (a[0]==b[0]) a[1]-b[1] else a[0] - b[0]} )
        fun cmp(a: IntArray, b: IntArray, c: IntArray): Int {
            val xab = b[0] - a[0]
            val yab = b[1] - a[1]
            val xbc = c[0] - b[0]
            val ybc = c[1] - b[1]
            return xab*ybc - yab*xbc
        }
        val up = mutableListOf<IntArray>()
        val lo = mutableListOf<IntArray>()
        trees.forEach { curr ->
            while(up.size >= 2 && cmp(up[up.size-2], up[up.size-1], curr) < 0) up.removeAt(up.lastIndex)
            while(lo.size >= 2 && cmp(lo[lo.size-2], lo[lo.size-1], curr) > 0) lo.removeAt(lo.lastIndex)
            up.add(curr)
            lo.add(curr)
        }
        return (up+lo).distinct().toTypedArray()
    }

This is an implementation of the Andrew’s monotonic chain algorithm.

  • need to remember vector algebra equation for ccw (counter clockwise) check (see here)
  • don’t forget to sort by x and then by y

Complexity: O(Nlog(N)) Memory: O(N)

18.11.2022

https://leetcode.com/problems/ugly-number/ easy


    fun isUgly(n: Int): Boolean {
        if (n <= 0) return false
        var x = n
        while(x%2==0) x = x/2
        while(x%3==0) x = x/3
        while(x%5==0) x = x/5
        return x == 1
    }

There is also a clever math solution, but I don’t understand it yet.

Complexity: O(log(n)) Memory: O(1)

17.11.2022

https://leetcode.com/problems/rectangle-area/ middle


class Solution {
    class P(val x: Int, val y: Int)
    class Rect(val l: Int, val t: Int, val r: Int, val b: Int) {
        val corners = arrayOf(P(l, t), P(l, b), P(r, t), P(r, b))
        val s = (r - l) * (t - b)
        fun contains(p: P) = p.x in l..r && p.y in b..t
        fun intersect(o: Rect): Rect {
            val allX = intArrayOf(l, r, o.l, o.r).apply { sort() }
            val allY = intArrayOf(b, t, o.b, o.t).apply { sort() }
            val r = Rect(allX[1], allY[2], allX[2], allY[1])
            return if (r.corners.all { contains(it) && o.contains(it)}) 
                r else Rect(0,0,0,0)
        }
    }
    
    fun computeArea(ax1: Int, ay1: Int, ax2: Int, ay2: Int, bx1: Int, by1: Int, bx2: Int, by2: Int): Int {
        val r1 = Rect(ax1, ay2, ax2, ay1)
        val r2 = Rect(bx1, by2, bx2, by1)
        return r1.s + r2.s -  r1.intersect(r2).s
    }
}

This is an OOP problem. One trick to write intersection function is to notice that all corners of intersection rectangle must be inside both rectangles. Also, intersection rectangle formed from middle coordinates of all corners sorted by x and y.

Complexity: O(1) Memory: O(1)

16.11.2022

https://leetcode.com/problems/guess-number-higher-or-lower/ easy


    override fun guessNumber(n:Int):Int {
       var lo = 1
       var hi = n
       while(lo <= hi) {
           val pick = lo + (hi - lo)/2
           val answer = guess(pick)
           if (answer == 0) return pick
           if (answer == -1) hi = pick - 1
           else lo = pick + 1
       }
       return lo
    }

This is a classic binary search algorithm. The best way of writing it is:

  • use safe mid calculation (lo + (hi - lo)/2)
  • use lo <= hi instead of lo < hi and mid+1/mid-1 instead of mid

Complexity: O(log(N)) Memory: O(1)

15.11.2022

https://leetcode.com/problems/count-complete-tree-nodes/ medium


       x
     *   x
   *   *   x
 *   x   *   x
* x x x x * x x
          \
          on each node we can check it's left and right depths
          this only takes us O(logN) time on each step
          there are logN steps in total (height of the tree)
          so the total time complexity is O(log^2(N))


    fun countNodes(root: TreeNode?): Int {
        var hl = 0
        var node = root  
        while (node != null) {
            node = node.left
            hl++
        }
        var hr = 0
        node = root  
        while (node != null) {
            node = node.right
            hr++
        }
        return when {
            hl == 0 -> 0 
            hl == hr -> (1 shl hl) - 1
            else -> 1  + 
            (root!!.left?.let {countNodes(it)}?:0) +
            (root!!.right?.let {countNodes(it)}?:0)
        }
    }

Complexity: O(log^2(N)) Memory: O(logN)

14.11.2022

https://leetcode.com/problems/most-stones-removed-with-same-row-or-column/ medium

From observing the problem, we can see, that the task is in fact is to find an isolated islands:


        // * 3 *         * 3 *        * * *
        // 1 2 *    ->   * * *   or   1 * *
        // * * 4         * * 4        * * 4

        // * 3 *         * * *
        // 1 2 5    ->   * * *
        // * * 4         * * 4


    fun removeStones(stones: Array<IntArray>): Int {
        val uf = IntArray(stones.size) { it }
        var rootsCount = uf.size
        fun root(a: Int): Int {
            var x = a
            while (uf[x] != x) x = uf[x]
            return x
        }
        fun union(a: Int, b: Int) {
           val rootA = root(a) 
           val rootB = root(b)
           if (rootA != rootB) {
               uf[rootA] = rootB
               rootsCount--
           }
        }
        val byY = mutableMapOf<Int, MutableList<Int>>()
        val byX = mutableMapOf<Int, MutableList<Int>>()
        stones.forEachIndexed { i, st ->
            byY.getOrPut(st[0], { mutableListOf() }).add(i)
            byX.getOrPut(st[1], { mutableListOf() }).add(i)
        }
        byY.values.forEach { list ->
            if (list.size > 1) 
                for (i in 1..list.lastIndex) union(list[0], list[i])
        }
        byX.values.forEach { list ->
            if (list.size > 1) 
                for (i in 1..list.lastIndex) union(list[0], list[i])
        }
        return stones.size - rootsCount
    }

Complexity: O(N) Memory: O(N)

13.11.2022

https://leetcode.com/problems/reverse-words-in-a-string/ medium

A simple trick: reverse all the string, then reverse each word.


    fun reverseWords(s: String): String {
        val res = StringBuilder()
        val curr = Stack<Char>()
        (s.lastIndex downTo 0).forEach { i ->
            val c = s[i]
            if (c in '0'..'z') curr.push(c)
            else if (curr.isNotEmpty()) {
                if (res.length > 0) res.append(' ')
                while (curr.isNotEmpty()) res.append(curr.pop())
            }
        }
        if (curr.isNotEmpty() && res.length > 0) res.append(' ')
        while (curr.isNotEmpty()) res.append(curr.pop())
        return res.toString()
    }

Complexity: O(N) Memory: O(N) - there is no O(1) solution for string in JVM

12.11.2022

https://leetcode.com/problems/find-median-from-data-stream/ hard

To find the median we can maintain two heaps: smaller and larger. One decreasing and one increasing. Peeking the top from those heaps will give us the median.


    //  [5 2 0] [6 7 10]
    //  dec     inc
    //   ^ peek  ^ peek


class MedianFinder() {
    val queDec = PriorityQueue<Int>(reverseOrder())
    val queInc = PriorityQueue<Int>()
    fun addNum(num: Int) {
        if (queDec.size == queInc.size) {
            queInc.add(num)
            queDec.add(queInc.poll())
        } else {
            queDec.add(num)
            queInc.add(queDec.poll())
        }
    }

    fun findMedian(): Double = if (queInc.size == queDec.size)
            (queInc.peek() + queDec.peek()) / 2.0
        else 
            queDec.peek().toDouble()
}

Complexity: O(NlogN) Memory: O(N)

11.11.2022

https://leetcode.com/problems/remove-duplicates-from-sorted-array/ easy

Just do what is asked. Keep track of the pointer to the end of the “good” part.


    fun removeDuplicates(nums: IntArray): Int {
        var k = 0
        for (i in 1..nums.lastIndex) {
            if (nums[k] != nums[i]) nums[++k] = nums[i]
        }
        
        return k + 1
    }

Complexity: O(N) Memory: O(1)

10.11.2022

https://leetcode.com/problems/remove-all-adjacent-duplicates-in-string/ easy

Solution:


    fun removeDuplicates(s: String): String {
        val stack = Stack<Char>()
        s.forEach { c ->
            if (stack.isNotEmpty() && stack.peek() == c) {
                stack.pop()
            } else {
                stack.push(c)
            }
        }
        return stack.joinToString("")
    }

Explanation: Just scan symbols one by one and remove duplicates from the end. Complexity: O(N) Memory: O(N)

9.11.2022

https://leetcode.com/problems/online-stock-span/ medium

So, we need to keep increasing sequence of numbers, increasing/decreasing stack will help. Consider example, this is how decreasing stack will work

        // 100   [100-1]                            1
        // 80    [100-1, 80-1]                      1
        // 60    [100-1, 80-1, 60-1]                1
        // 70    [100-1, 80-1, 70-2] + 60           2
        // 60    [100-1, 80-1, 70-2, 60-1]          1
        // 75    [100-1, 80-1, 75-4] + 70-2+60-1    4
        // 85    [100-1, 85-6] 80-1+75-4            6

Solution:


class StockSpanner() {
    val stack = Stack<Pair<Int,Int>>()

    fun next(price: Int): Int {
        // 100   [100-1]                            1
        // 80    [100-1, 80-1]                      1
        // 60    [100-1, 80-1, 60-1]                1
        // 70    [100-1, 80-1, 70-2] + 60           2
        // 60    [100-1, 80-1, 70-2, 60-1]          1
        // 75    [100-1, 80-1, 75-4] + 70-2+60-1    4
        // 85    [100-1, 85-6] 80-1+75-4            6
       var span = 1
       while(stack.isNotEmpty() && stack.peek().first <= price) {
          span += stack.pop().second 
       } 
       stack.push(price to span)
       return span
    }

}

Complexity: O(N) Memory: O(N)

8.11.2022

https://leetcode.com/problems/make-the-string-great/ easy


    fun makeGood(s: String): String {
        var ss = s.toCharArray()
        var finished = false
        while(!finished) {
            finished = true
            for (i in 0 until s.lastIndex) {
                if (ss[i] == '.') continue
                var j = i+1
                while(j <= s.lastIndex && ss[j] == '.') {
                    j++
                    continue
                }
                if (j == s.length) break
                
                var a = ss[i]
                var b = ss[j]
                if (a != b && Character.toLowerCase(a) == 
                        Character.toLowerCase(b)) {
                    ss[i] = '.'
                    ss[j] = '.'
                    finished = false
                }
            }
        }
        return ss.filter { it != '.' }.joinToString("")
    }

Explanation: The simplest solution is just to simulate all the process, as input string is just 100 symbols.

Speed: O(n^2) Memory: O(n)

7.11.2022

https://leetcode.com/problems/maximum-69-number/ easy


    fun maximum69Number (num: Int): Int {
        var n = num
        if (6666 <= n && n <= 6999) return num + 3000
        if (n > 9000) n -= 9000
        if (666 <= n && n <= 699) return num + 300
        if (n > 900) n -= 900
        if (66 <= n && n <= 69) return num + 30
        if (n > 90) n -= 90
        if (6 == n) return num + 3
        return num
    }

Explanation: The simplest implementations would be converting to array of digits, replacing the first and converting back. However we can observe that numbers are in range 6-9999, so we can hardcode some logic.

Speed: O(1), Memory: O(1)

6.11.2022

https://leetcode.com/problems/orderly-queue/ hard


    fun orderlyQueue(s: String, k: Int): String {
        val chrs = s.toCharArray()
        if (k == 1) {
            var smallest = s
            for (i in 0..s.lastIndex) {
                val prefix = s.substring(0, i)
                val suffix = s.substring(i)
                val ss = suffix + prefix
                if (ss.compareTo(smallest) < 0) smallest = ss
            }
            return smallest
        } else {
            chrs.sort()
            return String(chrs)
        }
    }

O(n^2)

Explanation: One idea that come to my mind is: if k >= 2 then you basically can swap any adjacent elements. That means you can actually sort all the characters.

Speed: O(n^2), Memory: O(n)

6.11.2022

https://leetcode.com/problems/word-search-ii/ hard

Solution [kotlin]


    class Node {
        val next = Array<Node?>(26) { null }
        var word: String?  = null
        operator fun invoke(c: Char): Node {
            val ind = c.toInt() - 'a'.toInt()
            if (next[ind] == null) next[ind] = Node()
            return next[ind]!!
        } 
        operator fun get(c: Char) = next[c.toInt() - 'a'.toInt()]
    }
    fun findWords(board: Array<CharArray>, words: Array<String>): List<String> {
        val trie = Node()
        
        words.forEach { w ->
            var t = trie
            w.forEach { t = t(it) }
            t.word = w
        }
        
        val result = mutableSetOf<String>()
        fun dfs(y: Int, x: Int, t: Node?, visited: MutableSet<Int>) {
           if (t == null || y < 0 || x < 0 
               || y >= board.size || x >= board[0].size 
               || !visited.add(100 * y + x)) return
           t[board[y][x]]?.let {
               it.word?.let {  result.add(it)  }
                dfs(y-1, x, it, visited)
                dfs(y+1, x, it, visited)
                dfs(y, x-1, it, visited)
                dfs(y, x+1, it, visited)
           }
           visited.remove(100 * y + x)
        }
        board.forEachIndexed { y, row ->
            row.forEachIndexed { x, c ->
                dfs(y, x, trie, mutableSetOf<Int>())
            }
        }
        return result.toList()
    }

Explanation: Use trie + dfs

  1. Collect all the words into the Trie
  2. Search deeply starting from all the cells and advancing trie nodes
  3. Collect if node is the word
  4. Use set to avoid duplicates

Speed: O(wN + M), w=10, N=10^4, M=12^2 , Memory O(26w + N)

4.11.2022

https://leetcode.com/problems/reverse-vowels-of-a-string/ easy

Solution [kotlin]


    fun reverseVowels(s: String): String {
        val vowels = setOf('a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U')
        var chrs = s.toCharArray()
        var l = 0
        var r = chrs.lastIndex
        while(l < r) {
            while(l<r && chrs[l] !in vowels) l++
            while(l<r && chrs[r] !in vowels) r--
            if (l < r) chrs[l] = chrs[r].also { chrs[r] = chrs[l] }
            r--
            l++
        }
        return String(chrs)
    }

Explanation: Straightforward solution : use two pointers method and scan from the both sides.

Speed: O(N), Memory O(N)

3.11.2022

https://leetcode.com/problems/longest-palindrome-by-concatenating-two-letter-words/ medium

Solution [kotlin]


fun longestPalindrome(words: Array<String>): Int {
        var singles = 0
        var mirrored = 0
        var uneven = 0
        var unevenSum = 0
        val visited = mutableMapOf<String, Int>()
        words.forEach { w ->  visited[w] = 1 + visited.getOrDefault(w, 0) }
        visited.forEach { w, wCount ->
            if (w[0] == w[1]) {
                if (wCount %2 == 0) {
                    singles += wCount*2
                } else {
                    // a b -> a
                    // a b a -> aba 2a + 1b = 2 + 1
                    // a b a b -> abba 2a + 2b = 2+2
                    // a b a b a -> baaab 3a + 2b = 3+2
                    // a b a b a b -> baaab 3a + 3b = 3+2 (-1)
                    // a b a b a b a -> aabbbaa 4a+3b=4+3
                    // a b a b a b a b -> aabbbbaa 4a+4b=4+4
                    // 5a+4b = 2+5+2
                    // 5a+5b = 2+5+2 (-1)
                    // 1c + 2b + 2a = b a c a b
                    // 1c + 3b + 2a =
                    // 1c + 3b + 4a = 2a + 3b + 2a
                    // 5d + 3a + 3b + 3c = a b c 5d c b a = 11 
                    uneven++
                    unevenSum += wCount
                }
            } else {
                val matchingCount = visited[w.reversed()] ?:0
                mirrored += minOf(wCount, matchingCount)*2
            }
        }
        val unevenCount = if (uneven == 0) 0 else 2*(unevenSum - uneven + 1)
        return singles + mirrored + unevenCount
    }

Explanation: This is a counting task, can be solved linearly. There are 3 cases:

  1. First count mirrored elements, “ab” <-> “ba”, they all can be included to the result
  2. Second count doubled letters “aa”, “bb”. Notice, that if count is even, they also can be splitted by half and all included.
  3. The only edge case is uneven part. The law can be derived by looking at the examples

Speed: O(N), Memory O(N)

2.11.2022

https://leetcode.com/problems/minimum-genetic-mutation/ medium

Solution [kotlin]


    fun minMutation(start: String, end: String, bank: Array<String>): Int {
        val wToW = mutableMapOf<Int, MutableList<Int>>()
        fun searchInBank(i1: Int, w1: String) {
            bank.forEachIndexed { i2, w2 ->
                if (w1 != w2) {
                    var diffCount = 0
                    for (i in 0..7) {
                        if (w1[i] != w2[i]) diffCount++
                    }
                    if (diffCount == 1) {
                       wToW.getOrPut(i1, { mutableListOf() }).add(i2)
                       wToW.getOrPut(i2, { mutableListOf() }).add(i1)
                    }
                }
            }
        }
        bank.forEachIndexed { i1, w1 -> searchInBank(i1, w1) }
        searchInBank(-1, start)
        val queue = ArrayDeque<Int>()
        queue.add(-1)
        var steps = 0
        while(queue.isNotEmpty()) {
            repeat(queue.size) {
                val ind = queue.poll()
                val word = if (ind == -1) start else bank[ind]
                if (word == end) return steps
                wToW[ind]?.let { siblings ->
                    siblings.forEach { queue.add(it) }
                }
            }
            steps++
            if (steps > bank.size + 1) return -1
        }
        return -1
    }

Explanation:

  1. make graph
  2. BFS in it
  3. stop search if count > bank, or we can use visited map

Speed: O(wN^2), Memory O(N)

1.11.2022

https://leetcode.com/problems/where-will-the-ball-fall/ medium

Solution [kotlin]


    fun findBall(grid: Array<IntArray>): IntArray {
        var indToBall = IntArray(grid[0].size) { it }
        var ballToInd = IntArray(grid[0].size) { it }
        grid.forEach { row ->
            var nextIndToBall = IntArray(grid[0].size) { -1 }
            var nextBallToInd = IntArray(grid[0].size) { -1 }
            for (i in 0..row.lastIndex) {
                val currBall = indToBall[i]
                if (currBall != -1) {
                    val isCorner = row[i] == 1 
                    &&  i<row.lastIndex 
                    && row[i+1] == -1
                    || row[i] == -1
                    && i > 0
                    && row[i-1] == 1
                    
                    val newInd = i + row[i]
                    if (!isCorner && newInd >= 0 && newInd <= row.lastIndex) {
                        nextIndToBall[newInd] = currBall
                        nextBallToInd[currBall] = newInd
                    } 
                }
            }
            indToBall = nextIndToBall
            ballToInd = nextBallToInd
        }
        return ballToInd
    }

Explanation: This is a geometry problem, but seeing the pattern might help. We can spot that each row is an action sequence: -1 -1 -1 shifts balls left, and 1 1 1 shifts balls to the right. Corners can be formed only with -1 1 sequence.

31.10.2022

https://leetcode.com/problems/toeplitz-matrix/ easy

Solution [kotlin]


    fun isToeplitzMatrix(matrix: Array<IntArray>): Boolean =
        matrix
        .asSequence()
        .windowed(2)
        .all { (prev, curr) -> prev.dropLast(1) == curr.drop(1) }

Explanation: just compare adjacent rows, they must have an equal elements except first and last