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

Solution dùng trie beat 97%. Đang cố optimize thêm:

Một số trick đã dùng:
- Allocate trước memory cho tất cả các test case bằng biến static. Vì kích thước đầu vào là cố định nên có thể tính trước được tối đa lượng memory sẽ dùng. Lúc đó mỗi lần cần tạo một TrieNode mới thì chỉ cần dịch offset đi 26*8 byte. Cách này giải quyết được các nhược điểm cố hữu của Trie là phải allocate nhiều và bộ nhớ bị phân mảnh. Lúc này toàn bộ Trie đều nằm trên một khoảng liền, tổng dung lượng không quá 1.8 MB, có thể vừa khít trên L3 cache.

Cứ mỗi test case thì memset lại phần bộ nhớ tối đa có thể dùng. Con số này ước lượng dựa trên số từ đầu vào của test case đó.

- Tạm hoãn việc tạo TrieNode khi gặp một ký tự mới, thay vào đó gán bằng giá trị 0x1 để phân biệt với trường hợp bằng null (vì chắc chắn không con trỏ nào có giá trị 0x1). Chỉ khi nào xét đến con của node đó thì mới tạo.
Với cách allocate trước thì cách này có thể không cần thiết lắm, mà lại làm phức tạp code. Để thử bỏ đi làm theo kiểu bình thường xem có khá hơn không.

C++:
char ** offset;
class Solution {

    static std::vector<char*> buffer;
    
    #define NOTNULL(c) (long)c > Placeholder
    #define ISNULL(c) (long)c <= Placeholder
    struct TrieNode {
        static constexpr long Placeholder = 1;
        TrieNode * children = nullptr;

        int addWord(std::string &s) {
            auto *node = this;
            int res = 0;
            
            for (auto it = s.rbegin(); it != s.rend(); ++it) {
                int first_time = 0;
                int divergence = 0;
                if (ISNULL(node->children)) {
                    node->children = (TrieNode*) offset;
                    offset += 26;
                    //std::memset(children, 0, 26 * sizeof(TrieNode*));
                    first_time = 1;
                }

                auto &pc = node->children[*it - 'a'];
                if (pc.children == nullptr) {
                    pc.children = (TrieNode*) Placeholder;
                    divergence = first_time ? 0 : (it - s.rbegin() + 2);
                }
                res += first_time + divergence;
                node = &pc;
            }
            return res;
        }
        
        
        
    };
    
public:
    Solution() {
        if (buffer.size() == 0) {
            int sz = 2000;
            buffer.resize(26 * (1 + min(sz, 26) + min(sz, 26*26) + sz * 4));
        }
    }
    
    int minimumLengthEncoding(vector<string>& words) {
        int sz = words.size();
        offset = buffer.data();
        
        std::memset(offset, 0, sizeof(char*) * 26 * (1 + min(sz, 26) 
                            + min(sz, 26*26) + sz * 4));
        //buffer.resize(26 * (1 + min(sz, 26) 
        //                    + min(sz, 26*26) + sz * 4));
        
        int res = 1;
        TrieNode root;
        for (auto &w : words) {
            res += root.addWord(w);
        }
        return res;
    }
};

std::vector<char*> Solution::buffer;
SNIaFn2.jpg



Sent from HUAWEI DBY-W09 using vozFApp
 
bài hôm nay, 1 cái thang = 1 lần dùng gạch vô tận (vượt qua bất kì khoảng cách), gọi số thang là a. Ý tưởng greedy khá đơn giản, vì 1 cái thang xây được 1 dãy gạch vô tận, nên tại vị trí x bất kì giả sử có k vị trí mà ta phải sử dụng gạch hoặc thang để vượt qua, thì ta sẽ sử dụng thang để vượt qua a các vị trí chênh lệch cao nhất, k-a vị trí còn lại sử dụng gạch.
Cách 1: sử dụng binary search trên kết quả, TC O(n*logn*logn).
C++:
class Solution {
public:
    bool check(int mid, vector<int>& heights, int bricks, int ladders){
        vector<int>a;
        for(int i=1;i<=mid;i++){
            if(heights[i]-heights[i-1]>0) a.push_back(heights[i]-heights[i-1]);
        }
        if(ladders>=a.size()) return true;
        sort(a.begin(),a.end());
        int z=a.size()-ladders;
        for(int i=0;i<z;i++){
            bricks-=a[i];
        }
        return bricks>=0;
    }
    int furthestBuilding(vector<int>& heights, int bricks, int ladders) {
        int n=heights.size();
        int l=0,r=n-1;
        int res=0;
        while(l<=r){
            int mid=(l+r)>>1;
            if(check(mid,heights,bricks,ladders)){
                l=mid+1;
                res=mid;
            }
            else r=mid-1;
        }
        return res;
    }
};
Cách 2: multiset, TC O(n*logn)
C++:
class Solution {
public:
    int furthestBuilding(vector<int>& heights, int bricks, int ladders) {
        multiset<int>s;
        int res=0;
        for(int i=1;i<heights.size();i++){
            if(heights[i]-heights[i-1]<=0){
                res=i;
                continue;
            }
            s.insert(heights[i]-heights[i-1]);
            if(s.size()<=ladders){
                res=i;
                continue;
            }
            int z=*s.begin();
            if(bricks-z>=0){
                bricks-=z;
                res=i;
                s.erase(s.find(z));
                continue;
            }
            break;
        }
        return res;
    }
};
 
Hành trình giải bài leetcode hôm 21 / 6 / 2021:
  • Thoạt nhìn qua, liên tưởng ngay tới greedy vì có cái thang sẽ đc skip 1 lần
  • Nhìn xuống thấy cái size của heights max 10^6 nên Time complexity chỉ có thể là O(n) hoặc O(nlog(n)), nói chung là phải nhỏ hơn O(n^2)
  • Sau 1 hồi suy nghĩ, đã nghĩ đến dp, monotonic stack thì nhận ra cái key của bài này là giữa hai phần tử i - 1 và i sao cho heights của i - 1 < heights của i => chỉ cần quan tâm cái khoảng cách giữa heights của i - 1 và heights của i. //ps: DM cái edit voz
  • Từ điều đó với cả cái thang sẽ skip 1 lần => hình thành ý tưởng
  • Ý tưởng là dùng 1 biến totalSum sẽ là tổng tất cả các khoảng cách giữa mỗi lần nhảy
  • Vì thang sẽ skip 1 lần nên phải cho những lần nhảy đó là lớn nhất có thể => sử dụng priority queue để lưu những lần nhảy có giá trị lớn nhất, mỗi khi cái priority queue có size > cái ladders thì pop nó ra, dùng thêm biến sumPq để lưu tổng pq.
  • Khi totalSum > bricks + sumPq thì có nghĩa ko thể nhảy nữa, return index lúc đó trừ cho 1
  • Time complexity: O(n log(n))
  • Space complexity: O(log(n)
  • Code lần 1: https://leetcode.com/submissions/detail/727216774/
  • Sau khi xem discuss thì thấy cách mình đã tối ưu nhất thì lên voz chém gió
 
Last edited:
1655778495012.png


Đã beat 100% bài hôm qua.

So với lời giải ở: https://voz.vn/t/leetcode-moi-ngay.568742/post-18448221 thì có cải tiến ở chỗ giảm được 1/2 Memory cho Trie, theo đó tăng cơ hội Trie được fit vào cache L3, thâm chí L2.

Cụ thể là: số node của Trie không bao giờ vượt quá 200000, cho nên hoàn toàn có thể dùng 4 byte để mô tả mỗi Node, thay vì phải dùng đầy đủ 8 byte cho một Pointer như trước đây.
 
Hành trình giải bài leetcode hôm 21 / 6 / 2021:
  • Thoạt nhìn qua, liên tưởng ngay tới greedy vì có cái thang sẽ đc skip 1 lần
  • Nhìn xuống thấy cái size của heights max 10^6 nên Time complexity chỉ có thể là O(n) hoặc O(nlog(n)), nói chung là phải nhỏ hơn O(n^2)
  • Sau 1 hồi suy nghĩ, đã nghĩ đến dp, monotonic stack thì nhận ra cái key của bài này là giữa hai phần tử i - 1 và i sao cho heights của i - 1 < heights của i => chỉ cần quan tâm cái khoảng cách giữa heights của i - 1 và heights của i. //ps: DM cái edit voz
  • Từ điều đó với cả cái thang sẽ skip 1 lần => hình thành ý tưởng
  • Ý tưởng là dùng 1 biến totalSum sẽ là tổng tất cả các khoảng cách giữa mỗi lần nhảy
  • Vì thang sẽ skip 1 lần nên phải cho những lần nhảy đó là lớn nhất có thể => sử dụng priority queue để lưu những lần nhảy có giá trị lớn nhất, mỗi khi cái priority queue có size > cái ladders thì pop nó ra, dùng thêm biến sumPq để lưu tổng pq.
  • Khi totalSum > bricks + sumPq thì có nghĩa ko thể nhảy nữa, return index lúc đó trừ cho 1
  • Time complexity: O(n log(n))
  • Space complexity: O(log(n)
  • Code lần 1: https://leetcode.com/submissions/detail/727216774/
  • Sau khi xem discuss thì thấy cách mình đã tối ưu nhất thì lên voz chém gió
Ý tưởng của t cũng giống y chang, dùng greedy. Sử dụng max heap để lưu lại những vị trí dùng bricks để nhảy, mục đích là để tối ưu cái ladder, chỉ sử dụng ladder tại những chỗ tốn nhiều bricks nhất.
https://leetcode.com/submissions/detail/727248157/
 
Update tí:
Vì thớt này bàn về thuật toán nên sẽ show code khá nhiều khiến việc lội trang của ae bất tiện. Nên anh em chú ý phần nào có code thì cần để vào SPOILER.

Về phần ý tưởng thì không cần, tuy là có hơi spoil cho ng khác nhưng topic này bàn chính là ý tưởng, cái nào cũng để vào spoiler thì bất tiện quá.
 
Em implement ý tưởng giống các bác trên C# mà sao runtime nó x2 luôn :cry:
C#:
public class Solution {
    public int FurthestBuilding(int[] heights, int bricks, int ladders) {
        int pos = 0;
        const int MAX = 1000001;
        PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>();
        for(int i = 0; i < heights.Length - 1; i++){
            if(heights[i] < heights[i+1]){
                int diff = heights[i + 1] - heights[i];
                if(bricks >= diff){
                    bricks -= diff;     
                    maxHeap.Enqueue(diff, MAX - diff);
                }
                else if(ladders > 0){
                    ladders--;
                    if(maxHeap.Count > 0 && diff < maxHeap.Peek()){
                        bricks += maxHeap.Dequeue() - diff;
                        maxHeap.Enqueue(diff, MAX - diff);
                    }               
                }
                else
                    break;
            }
            pos += 1;
        }
        return pos;
    }
}
 
Em implement ý tưởng giống các bác trên C# mà sao runtime nó x2 luôn :cry:
C#:
public class Solution {
    public int FurthestBuilding(int[] heights, int bricks, int ladders) {
        int pos = 0;
        const int MAX = 1000001;
        PriorityQueue<int, int> maxHeap = new PriorityQueue<int, int>();
        for(int i = 0; i < heights.Length - 1; i++){
            if(heights[i] < heights[i+1]){
                int diff = heights[i + 1] - heights[i];
                if(bricks >= diff){
                    bricks -= diff;   
                    maxHeap.Enqueue(diff, MAX - diff);
                }
                else if(ladders > 0){
                    ladders--;
                    if(maxHeap.Count > 0 && diff < maxHeap.Peek()){
                        bricks += maxHeap.Dequeue() - diff;
                        maxHeap.Enqueue(diff, MAX - diff);
                    }             
                }
                else
                    break;
            }
            pos += 1;
        }
        return pos;
    }
}

Mấy cái time này nó hên xui lắm, thím resubmit cùng 1 code thì sẽ có runtime khác nhau, chưa kể C#/Java thì luôn chậm hơn C++ rồi.

Tôi cũng dùng C# gần giống thím thì đc 272ms (beat 67.5%) https://leetcode.com/submissions/detail/727204520/
 
Sử dụng PriorityQueue TC O(n*logn). Runtime: 20 ms (beat 90.85%)

Java:
class Solution {
   
public int furthestBuilding(int[] a, int bricks, int ladders) {
        int n = a.length;
       
        Queue<Integer> queue = new PriorityQueue<Integer>();
       
        int res = 1;
       
        while (res < n) {
            int offset = a[res] - a[res-1];
            if (offset>0) {
                if (ladders > 0) {
                    ladders--;
                    queue.offer(offset);
                } else {
                    if (!queue.isEmpty()&&queue.peek() < offset) {
                        bricks-=queue.poll();
                        queue.offer(offset);
                    } else {
                        bricks-=offset;
                    }
                    if (bricks<0) {
                        break;
                    };
                }
            }
            res++;
        }
       
        return res-1;
       
    }
};
 
Last edited:
Beat 98.82%. Cách làm thì vẫn là max heap như các bài trên, chỉ có cái là dùng lại trick allocate trước 100000 phần tử cho cái heap để đỡ phải tạo xóa lại khi đổi test case.

https://leetcode.com/submissions/detail/727329008/


Mấy cái time này nó hên xui lắm, thím resubmit cùng 1 code thì sẽ có runtime khác nhau, chưa kể C#/Java thì luôn chậm hơn C++ rồi.

Tôi cũng dùng C# gần giống thím thì đc 272ms (beat 67.5%) https://leetcode.com/submissions/detail/727204520/
Mình quan sát từ trước đến giờ thì Java toàn nhanh hơn C++ trên leetcode.
Khả năng là bọn leetcode này chạy trên máy ảo kiểu gì đó, code C++ tối ưu cực kém và thời gian không ổn định. Cũng không biết compile option là như nào.
 
Beat 98.82%. Cách làm thì vẫn là max heap như các bài trên, chỉ có cái là dùng lại trick allocate trước 100000 phần tử cho cái heap để đỡ phải tạo xóa lại khi đổi test case.

https://leetcode.com/submissions/detail/727329008/



Mình quan sát từ trước đến giờ thì Java toàn nhanh hơn C++ trên leetcode.
Khả năng là bọn leetcode này chạy trên máy ảo kiểu gì đó, code C++ tối ưu cực kém và thời gian không ổn định. Cũng không biết compile option là như nào.
Đúng r bác, em làm mấy câu Java thời gian chạy chênh lệch ít vl.
 
bài hôm nay, 1 cái thang = 1 lần dùng gạch vô tận (vượt qua bất kì khoảng cách), gọi số thang là a. Ý tưởng greedy khá đơn giản, vì 1 cái thang xây được 1 dãy gạch vô tận, nên tại vị trí x bất kì giả sử có k vị trí mà ta phải sử dụng gạch hoặc thang để vượt qua, thì ta sẽ sử dụng thang để vượt qua a các vị trí chênh lệch cao nhất, k-a vị trí còn lại sử dụng gạch.
Cách 1: sử dụng binary search trên kết quả, TC O(n*logn*logn).
Lúc đầu cũng nghĩ ra mỗi cách 1 mà nghĩ chưa tối ưu nên chưa implement, sau xem hint thì mới nghĩ ra cách dùng min heap :ah:
 
Back
Top