thảo luận Leetcode mỗi ngày

Bỏ algo lâu quá rồi nay ngồi implement lại DSU TLE lên TLE xuống :adore:

Lại thêm 1 bài có tư tưởng greedy nữa, ý tưởng là tìm thành phần liên thông sử dụng cạnh type 3 trước vì những cạnh này đều có thể được dùng bởi cả Alice và Bob -> Sau đó chạy lại thuật toán cho type 1 và 2 -> cuối cùng là check xem có thằng nào đang chưa tạo được cây khung không
Python:
class Solution:
    def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
        parents = [i for i in range(n)]
        count = [1] * n

        def find(parents, u):
            if parents[u] != u:
                parents[u] = find(parents, parents[u])
            return parents[u]

        def dsu(parents, count, u, v):
            p_u = find(parents, u)
            p_v = find(parents, v)
            if p_u == p_v:
                return 1
            if p_u < p_v:
                parents[p_v] = p_u
                count[p_u] += count[p_v]
                count[p_v] = 0
            else:
                parents[p_u] = p_v
                count[p_v] += count[p_u]
                count[p_u] = 0
            return 0
        
        removed_edges_count = 0
        for edge in edges:
            [t, u, v] = edge
            if t == 3:
                removed_edges_count += dsu(parents, count, u-1, v-1)
        
        parents_A = copy.deepcopy(parents)
        parents_B = copy.deepcopy(parents)
        count_A = copy.deepcopy(count)
        count_B = copy.deepcopy(count)

        for edge in edges:
            [t, u, v] = edge
            if t == 1:
                removed_edges_count += dsu(parents_A, count_A, u-1, v-1)
            elif t == 2:
                removed_edges_count += dsu(parents_B, count_B, u-1, v-1)
        
        if count_A[0] != n or count_B[0] != n:
            return -1
        
        return removed_edges_count
 
Cách cài đặt này không thỏa mãn ràng buộc O(1) space, ngoài ra còn quá phức tạp (sử dụng fancy functions không cần thiết).

Bài này thì tư tưởng tail recursion quá rõ ràng nên một cách khác đơn giản hơn như sau:
Code:
let oddEvenList xs =
    let rec pick (os, es) xs p =
        match xs, p with
        | x :: xs', true -> pick (x :: os, es) xs' false
        | x :: xs', false -> pick (os, x :: es) xs' true
        | _ -> es @ os |> List.rev

    pick ([], []) xs true
Chỉ cần thêm memory space cho đúng một biến bool (là p), nên SC = O(1). Cách cài đặt trên có thể chuyển thành single-line function bằng cách dùng List.fold, như sau:
Code:
let oddEventList<'t> =
    List.fold (fun (os, es, p) x ->
        if p then (x :: os, es, not p)
        else (os, x :: es, not p)) ([], [], true)
    >> (fun (a, b, _) -> b @ a |> List.rev)
Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ? :adore:
 
Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ? :adore:
Cài đặt của mình sử dụng tail recursion nên tất cả các lời gọi đều sử dụng chung stack frame.
 
Thím cho e hỏi phát recursion thì sao tính là O(1) SC được tím nhỉ? Vì bộ nhớ của stack space sẽ tăng theo n nên SC đúng của recursion implementation phải là O(n) chứ nhỉ? :adore:
nhìn có vẻ như là tail recursion, và MS nói rằng F# optimize để không có stack growth:

The difference is that in countDown1 the returned value of the recursive call was used to construct the final value of the function by adding 1 to it. In contrast, in countDown2 the returned value of the recursive call is also the value of the function itself. Meaning, the recursive call is the last instruction in the function definition – a style known as tail recursion. This style allowed the compiler to transform the function into a loop, thus eliminating the need to create new stack frames.
 
Java:
class EdgeComparator implements Comparator<int[]> {
    @Override
    public int compare(int[] a, int[] b) {
        if (a[0] == b[0]) {
            if (a[1] == b[1]) {
                return a[2] - b[2];
            }

            return a[1] - b[1];
        }
        return - (a[0] - b[0]);
    }
}
class Solution {
    public static int TYPE = 0;
    public static int U = 1;
    public static int V = 2;
    public static int ALICE = 1;
    public static int BOB = 2;
    public static int BOTH = 3;

    public int maxNumEdgesToRemove(int n, int[][] edges) {
        List<int[]> bobGraph = getEdgesByType(BOB, edges);
        List<int[]> aliceGraph = getEdgesByType(ALICE, edges);

        List<int[]> bobSPT = getSPTFromGraph(bobGraph, n);
        List<int[]> aliceSPT = getSPTFromGraph(aliceGraph, n);
        if (bobSPT == null || aliceSPT == null) return -1;

        Set<int[]> distincEdge = new TreeSet<>(new EdgeComparator());
        distincEdge.addAll(bobSPT);
        distincEdge.addAll(aliceSPT);
        System.out.println(distincEdge);

        return edges.length - distincEdge.size();
    }

    public List<int[]> getEdgesByType(int type, int[][] edges) {
        List<int[]> graph = new ArrayList<>();
        for (int[] edge : edges) {
            if (type == edge[TYPE] || BOTH == edge[TYPE]) {
                graph.add(edge);
            }
        }

        Collections.sort(graph, new EdgeComparator());

        return graph;
    }

    int[] parent;
    public List<int[]> getSPTFromGraph(List<int[]> edges, int n) {
        List<int[]> sptEdge = new ArrayList<>();
        parent = new int[n + 1];

        for(int i = 1 ; i <= n ; ++i) makeSet(-i);

        for (int[] edge : edges) {
            int u = Math.abs(findSet(edge[U]));
            int v = Math.abs(findSet(edge[V]));
            if(u != v) {
                parent[u] = v;
                sptEdge.add(new int[]{edge[TYPE], edge[U], edge[V]});
            }
        }

        Set<Integer> hashSet = new HashSet<>();
        for (int i = 1; i <= n; i++) {
            hashSet.add(findSet(i));
        }

        if (hashSet.size() != 1) {
            return null;
        }

        return sptEdge;
    }

    void makeSet(int u){
        u = Math.abs(u);
        parent[u] = u;
    }

    int findSet(int u){
        u = Math.abs(u);
        if(u == parent[u]) return u;
        return parent[u] = findSet(parent[u]);
    }
}
Lâu không code đồ thị nên quên hết Kruskal mất công ngồi mo` lại?
 
Code:
// Lists in F# are implemented as singly linked lists,
// which means that operations that access only
// the head of the list are O(1), and element access is O(n).
// https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/lists

let oddEvenList (list: int list) =
    list
    |> List.indexed
    |> List.partition (fun (i, _) -> i % 2 = 0)
    |> fun (oddList, evenList) ->
        let _, oddList = oddList |> List.unzip
        let _, evenList = evenList |> List.unzip
        oddList @ evenList

// Test with #time directive
[1..10000] |> oddEvenList
Real: 00:00:00.002, CPU: 00:00:00.000, GC gen0: 0, gen1: 0, gen2: 0
val it: int list =
  [1; 3; 5; 7; 9; 11; 13; 15; 17; 19; 21; 23; 25; 27; 29; 31; 33; 35; 37; 39;
   41; 43; 45; 47; 49; 51; 53; 55; 57; 59; 61; 63; 65; 67; 69; 71; 73; 75; 77;
   79; 81; 83; 85; 87; 89; 91; 93; 95; 97; 99; 101; 103; 105; 107; 109; 111;
   113; 115; 117; 119; 121; 123; 125; 127; 129; 131; 133; 135; 137; 139; 141;
   143; 145; 147; 149; 151; 153; 155; 157; 159; 161; 163; 165; 167; 169; 171;
   173; 175; 177; 179; 181; 183; 185; 187; 189; 191; 193; 195; 197; 199; ...]
Nhân tiện ở đây, ngoài cài đặt sử dụng tail recursion đã trình bày ở post trước, mình trình bày một cài đặt sử dụng kỹ thuật continuation rất hay gặp trong functional programming:
Code:
let oddEventList xs =
    let rec pickcps xs k =
        match xs with
        | x :: x' :: xs'' -> pickcps xs'' (fun (os, es) -> k (x :: os, x' :: es))
        | x :: _ -> pickcps [] (fun (os, es) -> k (x :: os, es))
        | _ -> ([], []) |> k |> (fun (a, b) -> a @ b)

    pickcps xs id

Cài đặt này không cần sử dụng bất cứ fancy functions nào tuy nhiên sẽ là khó hiểu nếu chưa quen. Kỹ thuật ở đây là thay accumulator trong cài đặt trước bởi một continuation k.
 
C#:
public class Solution
{
    public int MaxNumEdgesToRemove(int n, int[][] edges)
    {
        UnionFind alice = new(n);
        UnionFind bob = new(n);

        for (int i = 0; i < edges.Length; i++)
        {
            int[] edge = edges[i];
            if (edge[0] == 3)
            {
                alice.Union(edge[1], edge[2]);
                bob.Union(edge[1], edge[2]);
            }
        }
        int result = alice.redundant;
        alice.redundant = 0;
        bob.redundant = 0;

        for (int i = 0; i < edges.Length; i++)
        {
            int[] edge = edges[i];
            int type = edge[0];
            int u = edge[1];
            int v = edge[2];
            if (type == 1)
            {
                alice.Union(u, v);
            }
            if (type == 2)
            {
                bob.Union(u, v);
            }
        }

        if (alice.GetComponentCount() != 1 || bob.GetComponentCount() != 1)
        {
            return -1;
        }
        return result + alice.redundant + bob.redundant;
    }

    public class UnionFind
    {
        int[] parent;
        public int redundant = 0;
        
        public int GetComponentCount()
        {
            int componentCount = 1;
            for (int i = 1; i < parent.Length; i++)
            {
                int parent = Find(i);
                componentCount += parent == 1 ? 0 : 1;
            }

            return componentCount;
        }

        public UnionFind(int n)
        {
            parent = new int[n + 1];
            for (int i = 1; i <= n; i++)
            {
                parent[i] = i;
            }
        }

        public int Find(int a)
        {
            if (parent[a] == a)
            {
                return parent[a];
            }

            int result = Find(parent[a]);
            parent[a] = result;
            return result;
        }

        public void Union(int a, int b)
        {
            int aParent = Find(a);
            int bParent = Find(b);
            if (aParent == bParent)
            {
                redundant++;
                return;
            }
            if (aParent < bParent)
            {
                parent[bParent] = aParent;
                return;
            }

            parent[aParent] = bParent;
        }
    }
}
 
bài hôm nay có phần gần giống với việc dùng thuật toán Kruskal tìm minimum spanning tree, nhưng làm việc trên unweighted graph nên không cần phải sort theo edge weight nữa

hiểu đại khái vậy nhưng vẫn chưa rõ lắm tại sao cách dùng type 3 edge trước rồi add edge có type khác vào sao lại là optimal, có cao nhân nào chỉ giáo không
Đầu bài yêu cầu loại đi tối đa các cạnh nhưng vẫn đảm bảo tính liên thông của hai đồ thị.
Ta có thể hiểu là tìm số cạnh ít nhất để cho 2 đồ thị liên thông.
Cạnh loại 3 là cạnh chung của hai đồ thị, do đó ta sẽ muốn ưu tiên giữ lại cạnh loại 3.
 
Đầu bài yêu cầu loại đi tối đa các cạnh nhưng vẫn đảm bảo tính liên thông của hai đồ thị.
Ta có thể hiểu là tìm số cạnh ít nhất để cho 2 đồ thị liên thông.
Cạnh loại 3 là cạnh chung của hai đồ thị, do đó ta sẽ muốn ưu tiên giữ lại cạnh loại 3.
intuition mình thấy, nhưng giờ muốn chứng minh là nó optimal thì làm ntn?
 
Lâu rồi mới đụng vào Kruskal
Python:
class Solution:
    def maxNumEdgesToRemove(self, n: int, edges: List[List[int]]) -> int:
        # step1: use type3 edges as much as possible to connect vertices for both Bob and Alice
        # step2: use type1, type2 to connect Alice vertices, Bob vertices respectively

        aliceUnions = UnionFind(n + 1)
        bobUnions = UnionFind(n + 1)

        usedEdges = 0
        for _, u, v in filter(lambda edge: edge[0] == 3, edges):
            if aliceUnions.find(u) != aliceUnions.find(v):
                usedEdges += 1
                aliceUnions.union(u, v)
                bobUnions.union(u, v)
        
        for edgeType, u, v in filter(lambda edge: edge[0] != 3, edges):
            targetUnions = aliceUnions if edgeType == 1 else bobUnions
            if targetUnions.find(u) != targetUnions.find(v):
                usedEdges += 1
                targetUnions.union(u, v)
        if not aliceUnions.getUnionCount() == bobUnions.getUnionCount() == 2:
            return -1
        return len(edges) - usedEdges

class UnionFind:
    def __init__(self, dataSize):
        self.par = list(range(dataSize))
        self.size = [1] * dataSize
        self.unionCount = dataSize

    def find(self, u):
        if self.par[u] != u:
            self.par[u] = self.find(self.par[u])
        return self.par[u]
    
    def getSize(self, u):
        return self.size[self.find(u)]
    
    def union(self, u, v):
        u, v = self.find(u), self.find(v)
        if u == v:
            return
        
        if self.size[u] < self.size[v]:
            u, v = v, u
        self.par[v] = u
        self.size[u] += self.size[v]
        self.unionCount -= 1
    
    def getUnionCount(self):
        return self.unionCount
 
intuition mình thấy, nhưng giờ muốn chứng minh là nó optimal thì làm ntn?
Cho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.

Không mất tính tổng quát, giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3. Gọi số cạnh là nghiệm không chứa V là E0.

Mệnh đề A: Pick cạnh loại 3 không phải là phương án tối ưu để E nhỏ nhất => E = E0 + 2 (pick loại 1, 2 để nối tới V).

Tuy nhiên tồn tại phương án pick cạnh loại 3: E0 + 1. Vì E0 + 1 < E0 + 2 nên mệnh đề A sai.

=> pick cạnh loại 3 là phương án tối ưu ở mỗi vertex
=> mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
 
Last edited:
Cho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.

Không mất tính tổng quát, giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3. Gọi số cạnh là nghiệm không chứa V là E0.

Mệnh đề A: Pick cạnh loại 3 không phải là phương án tối ưu để E nhỏ nhất => E = E0 + 2 (pick loại 1, 2 để nối tới V).

Tuy nhiên tồn tại phương án pick cạnh loại 3: E0 + 1. Vì E0 + 1 < E0 + 2 nên mệnh đề A sai.

=> pick cạnh loại 3 là phương án tối ưu ở mỗi vertex
=> mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
giờ cho graph chỉ chứa edge type 3 và graph này là hình tam giác thì cây E có chứa toàn bộ edge trong tập B không?
 
Cho đồ thị G. Gọi E là tập hợp tất cả các cạnh sao cho G liên thông và tổng số cạnh bé nhất. Chứng minh rằng mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.

Không mất tính tổng quát, giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3. Gọi số cạnh là nghiệm không chứa V là E0.

Mệnh đề A: Pick cạnh loại 3 không phải là phương án tối ưu để E nhỏ nhất => E = E0 + 2 (pick loại 1, 2 để nối tới V).

Tuy nhiên tồn tại phương án pick cạnh loại 3: E0 + 1. Vì E0 + 1 < E0 + 2 nên mệnh đề A sai.

=> pick cạnh loại 3 là phương án tối ưu ở mỗi vertex
=> mỗi vertex của G đều ưu tiên pick cạnh loại 3 để E tối ưu.
à edit rồi, mà sao
giả sử tồn tại một vertex V có 3 cạnh mỗi loại 1, 2, 3
thì lại không mất tính tổng quát nhỉ
 
à edit rồi, mà sao

thì lại không mất tính tổng quát nhỉ
Vì mỗi Vertex trên G đều liên thông, nên trường hợp chỉ chứa 1 và 2 hoặc chỉ chứa 3 thì không bàn vì không dùng để chứng minh cho mệnh đề được.

Vì đang chứng minh rằng ưu tiên pick 3, đối với các vertex chứa nhiều 1, 2, 3 thì cũng chỉ cần pick một cặp 1,2 hoặc một 3 để liên thông nên dùng vertex chứa 1, 2, 3 để đại diện vẫn được. Dù số lượng mỗi loại có là bao nhiêu cũng không mất tính tổng quát.
 
Vì mỗi Vertex trên G đều liên thông, nên trường hợp chỉ chứa 1 và 2 hoặc chỉ chứa 3 thì không bàn vì không dùng để chứng minh cho mệnh đề được.

Vì đang chứng minh rằng ưu tiên pick 3, đối với các vertex chứa nhiều 1, 2, 3 thì cũng chỉ cần pick một cặp 1,2 hoặc một 3 để liên thông nên dùng vertex chứa 1, 2, 3 để đại diện vẫn được. Dù số lượng mỗi loại có là bao nhiêu cũng không mất tính tổng quát.
không, cứ cho là ở đây chứng minh được chuyện ưu tiên type 3 edge đúng trong trường hợp có vertex có nhiều edge thuộc cả ba type (gọi là case 1 đi), thì vẫn còn phần còn lại là đi chứng minh là khi không có vertex như vầy thì logic của algo vẫn đúng, tức là khi chỉ có vertex nối edge type 1, 2, v/v, chỉ khi nào cách chứng minh những trường hợp còn lại đó dùng logic giống như case 1 thì mới dùng "không mất tính tổng quát được"

giờ lấy trường hợp chỉ có vertex nối edge type 1, 2 thì chứng minh vầy đâu còn đúng nhỉ 🤔
 
Last edited:
không, cứ cho là ở đây chứng minh được chuyện ưu tiên type 3 edge đúng trong trường hợp có vertex có nhiều edge thuộc cả ba type (gọi lag case 1 đi), thì vẫn còn phần còn lại là đi chứng minh là khi không có vertex như vầy thì logic của algo vẫn đúng, tức là khi chỉ có vertex nối edge type 1, 2, v/v, chỉ khi nào cách chứng minh những trường hợp còn lại đó dùng logic giống như case 1 thì mới dùng "không mất tính tổng quát được"

giờ lấy trường hợp chỉ có vertex nối edge type 1, 2 thì chứng minh vầy đâu còn đúng nhỉ 🖕
Không, vì G liên thông nên trường hợp chỉ có (1,2) hoặc (3) thì kệ nó nhé. Đối với vertex có chứa cả 3 loại dù số lượng tụi nó có bao nhiêu đi nữa thì cũng chỉ pick một {(1,2) OR (3)} để E0 nối với V. Nên dùng (1,2,3) là đủ để đại diện cho toàn bộ case rồi còn gì nữa.
 
Back
Top