Interesting LeetCode problems.

Siva

Network Delay Time

This problem is a good example of how to use Dijkstra's algorithm to solve a problem.

class Solution:
    def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
        graph = collections.defaultdict(list)
        for f, t, w in times:
            graph[f].append((w, t))

        distances = [math.inf] * n

        min_heap = [(0, k)]
        while min_heap:
            d, n = heapq.heappop(min_heap)

            if distances[n - 1] != math.inf:
                continue

            distances[n - 1] = d
            for w, t in graph[n]:
                heapq.heappush(min_heap, (d + w, t))

        ans = max(distances)
        return ans if ans != math.inf else -1

Redundant Connection

This problem is a good example of how to use Union Find to solve a problem. The problem is to find the redundant connection in a graph.

class DSU:
    def __init__(self, total_node_count) -> None:
        self.parent = [i for i in range(total_node_count + 1)]
        self.rank = collections.defaultdict(int)

    def find(self, x):
        if self.parent[x] != x:
            self.parent[x] = self.find(self.parent[x])
        return self.parent[x]

    def union(self, x, y):
        xp, yp = self.find(x), self.find(y)

        if xp == yp:
            return False
        elif self.rank[xp] < self.rank[yp]:
            self.parent[xp] = yp
        elif self.rank[xp] > self.rank[yp]:
            self.parent[yp] = xp
        else:
            self.parent[yp] = xp
            self.rank[xp] += 1

        return True

class Solution:
    def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
        dsu = DSU(1000)
        for edge in edges:
            if not dsu.union(*edge):
                return edge

K Closest Points to Origin

This problem is quite easy as we can just sort the list using a custom comparator and return the k closest. However, finding the optimal is quite hard.

Usually we do binary search on a sorted list. Sometimes, we do it on a monotonic function. However, we use binary search on the distance here and reduce the search space by half each time. Binary search will give a O(n) time and space solution

Binary Search Solution:

class Solution:
    def distance(self, point: List[int]) -> int:
        return point[0] ** 2 + point[1] ** 2 # we avoid the square root operation here
    def split_distances(self, remaining, distances, mid):
        closer, farther = [], []
        for index in remaining:
            if distances[index] <= mid:
                closer.append(index)
            else:
                farther.append(index)
        return closer, farther
    def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
        distances = [self.distance(point) for point in points]
        remaining = [i for i in range(len(points))]
        low, high = 0, max(distances)
        ans = []
        while k:
            mid = low + (high - low) / 2
            closer, farther = self.split_distances(remaining, distances, mid)
            # if we have more than enough points in the closer half, we can continue to search in the closer half for k closest
            if len(closer) > k:
                remaining = closer
                high = mid
            # else if we have less than k points in the closer half, we have to search for k - len(closer) closest points from the farther half
            else:
                k -= len(closer)
                ans.extend(closer)
                remaining = farther
                low = mid
        return [points[i] for i in ans]

However, the most optimal solution would be to use quickselect. For problems where we are tasked with finding the k (or kth) smallest/largest/etc, we should always consider whether QuickSort can be applied.

Topological sort - Course Schedule II

Topological sort is a graph traversal algorithm that can be used to sort a DAG in topological order.

Psuedo Code (Kahn's algorithm):

L = Empty list that will contain the sorted elements
S = Stack of all nodes with no incoming edge

while S is non-empty do
    remove a node n from S
    append n to L
    for each node m with an edge e from n to m do
        remove edge e from the graph
        if m has no other incoming edges then
            insert m into S

if graph has edges then
    return error   (graph has at least one cycle)
else
    return L   (a topologically sorted order)

A good leetcode problem that uses topological sort is Course Schedule II.

class Node:
    def __init__(self):
        self.indegree = 0
        self.outgoing_nodes = []

class Solution:
    def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
        graph = defaultdict(Node)
        # make adj list with indegree as extra info
        total_deps = 0
        # add all nodes
        for i in range(numCourses):
            graph[i]
        # populate adj list
        for to_node, from_node in prerequisites:
            graph[from_node].outgoing_nodes.append(to_node)
            graph[to_node].indegree += 1
            total_deps += 1
        # add all nodes with no deps
        no_deps_courses = []
        for key, node in graph.items():
            if node.indegree == 0:
                no_deps_courses.append(key)
        # perform topological sort (Kahn's algo)
        topo_order = []
        removed_edges = 0
        while no_deps_courses:
            course = no_deps_courses.pop()
            topo_order.append(course)
            for to_node in graph[course].outgoing_nodes:
                removed_edges += 1
                graph[to_node].indegree -= 1
                if graph[to_node].indegree == 0:
                    no_deps_courses.append(to_node)
        return topo_order if removed_edges == total_deps else []

LRU Cache

This problem is a good example of how to use a doubly linked list and a hashmap to implement a cache.

class LinkedListNode {
public:
    int key, val;
    LinkedListNode *next, *prev;

    LinkedListNode(int key, int val) {
        this->key = key;
        this->val = val;
    }
};

class DLinkedList {
public:
    LinkedListNode *head, *tail;

    DLinkedList() {
        this->head = new LinkedListNode(0, 0);
        this->tail = new LinkedListNode(0, 0);

        this->head->next = this->tail;
        this->tail->prev = this->head;
    }

    void add(LinkedListNode *node) {
        auto head_next = this->head->next;
        head_next->prev = node;

        head->next = node;

        node->prev = head;
        node->next = head_next;
    }

    void remove(LinkedListNode *node) {
        auto node_prev = node->prev;
        auto node_next = node->next;

        node_prev->next = node_next;
        node_next->prev = node_prev;
    }
};

class LRUCache {
public:
    int capacity;
    unordered_map<int, LinkedListNode*> cache;
    DLinkedList *ll;

    LRUCache(int capacity) {
        this->capacity = capacity;
        ll = new DLinkedList();
    }

    int get(int key) {
        if (cache.find(key) == cache.end()) {
            return -1;
        }

        auto node = cache[key];
        ll->remove(node);
        ll->add(node);

        return node->val;
    }

    void put(int key, int value) {
        if (cache.find(key) != cache.end()) {
            replace_value_for_key(key, value);
            return;
        }

        if (cache.size() >= capacity) {
            remove_lru();
        }

        auto node = new LinkedListNode(key, value);
        cache[key] = node;
        ll->add(node);
    }

private:
    void replace_value_for_key(int key, int value) {
        if (cache.find(key) == cache.end()) {
            return;
        }

        auto node = cache[key];
        ll->remove(node);
        node->val = value;
        ll->add(node);
        cache[key] = node;
    }

    void remove_lru() {
        if (cache.empty()) {
            return;
        }

        auto lru = ll->tail->prev;
        ll->remove(lru);
        cache.erase(lru->key);
    }
};

Here's a solution in python:

class LinkedListNode:
    def __init__(self, key: int, val: int):
        self.key = key
        self.val = val
        self.prev = None
        self.next = None

class DLinkedList:
    def __init__(self):
        # Head and Tail are dummy nodes
        self.head = LinkedListNode(0, 0)
        self.tail = LinkedListNode(0, 0)

        # Link head and tail
        self.head.next = self.tail
        self.tail.prev = self.head

    def add(self, node: LinkedListNode) -> None:
        # Add node in the front after head
        head_next = self.head.next
        head_next.prev = node

        self.head.next = node

        node.prev = self.head
        node.next = head_next

    def remove(self, node: LinkedListNode) -> None:
        # Remove node from DLinkedList
        node_next = node.next
        node_prev = node.prev

        node_next.prev = node_prev
        node_prev.next = node_next

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = {}
        self.ll = DLinkedList()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1

        node = self.cache[key]
        self.ll.remove(node)
        self.ll.add(node)

        return node.val

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.__replace_value_for_key(key, value)
            return

        if len(self.cache) >= self.capacity:
            self.__remove_least_recently_used()

        node = LinkedListNode(key, value)
        self.cache[key] = node
        self.ll.add(node)

    def __replace_value_for_key(self, key: int, value: int) -> None:
        if key not in self.cache:
            return

        node = self.cache[key]
        self.ll.remove(node)
        node.val = value
        self.ll.add(node)
        self.cache[key] = node

    def __remove_least_recently_used(self) -> None:
        if not self.cache:
            return

        lru = self.ll.tail.prev
        self.ll.remove(lru)
        self.cache.pop(lru.key)

In python this problem can be solved using a OrderedDict very easily.

class LRUCache:
    def __init__(self, capacity: int):
        self.capacity = capacity
        self.cache = OrderedDict()

    def get(self, key: int) -> int:
        if key not in self.cache:
            return -1
        self.cache.move_to_end(key)
        return self.cache[key]

    def put(self, key: int, value: int) -> None:
        if key in self.cache:
            self.cache.move_to_end(key)
        self.cache[key] = value
        if len(self.cache) > self.capacity:
            self.cache.popitem(last=False)