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

21/06/2022​

#1642. (Medium) https://leetcode.com/problems/furthest-building-you-can-reach/

Phân tích bài toán:
  • 1 <= n <= 10^5: Có thể chạy từ O(nlogn) trở xuống
  • Đầu tiên, vì ta bắt buộc phải đi qua từng tòa nhà một theo thứ tự, nên chỉ cần quan tâm độ cao chênh lệch giữa 2 tòa nhà liên tiếp nhau
  • Hay nói cách khác, giá trị đáng quan tâm là h = heights - heights[i - 1] với i = 1 → n
  • Nếu h ≤ 0 thì ta được free 1 tòa nhà, ngược lại, ta sẽ lựa chọn dùng gạch hoặc thang để vượt qua tòa nhà
  • Dễ thấy thang hiệu quả hơn dùng gạch rất nhiều vì có thể vượt qua bất kì độ cao nào, trong khi gạch thì càng cao chi phí càng lớn
  • Suy ra, bài toán chính là tối ưu khi nào nên dùng thang khi nào nên dùng gạch. Hay cách khác, chính là dùng thang sao cho tối ưu nhất có thể

Vậy dùng thang sao cho tối ưu ?
  • Giả sử chỉ có 1 thang:
    • Rõ ràng ta chỉ nên dùng nó ở vị trí có h cao nhất, các chỗ khác thì dùng gạch cho tiết kiệm
    • Nhưng nếu vị trí đó nằm ở cuối mảng, mà ta không có đủ gạch để đi đến đó ? Vậy thì phải chọn vị trí có h cao nhất nhưng trước khi hết gạch
  • Mở rộng ra, nếu có 2 thang thì sao ? Ta sẽ dùng 2 thang ở vị trí có h lớn nhất và nhì trước khi hết gạch
  • Tương tự với n thang, ta sẽ dùng nó cho n vị trí lớn nhất có thể trước khi hết gạch

Vậy ta đã có phương án cơ bản, vấn đề trở thành làm thế nào để tìm được n vị trí lớn nhất này trước khi hết gạch ?
  • Trước tiên cứ keep in mind, khi nào gặp bài toán top k phần tử lớn nhất / bé nhất thì khả năng cao ta sẽ dùng heap (Priority Queue). Vì đây là CDTL tối ưu cho bài toán này
  • Nhưng dùng heap như thế nào ? Ta phân tích tiếp. Nếu giả sử chỉ có 1 thang, ta sẽ:
    • Dùng gạch hết mức có thể
    • Lưu lại trước đó ta đã đi qua tòa nhà có h cao nhất là hmax
    • Rõ ràng để tối ưu ta sẽ dùng thang tại vị trí hmax, nếu thế ta tiết kiệm đc hmax viên gạch
    • Vì hết thang rồi, nên ta tiếp tục xài số gạch còn lại (+ hmax tiết kiệm được) để đi tiếp
    • Đi hết gạch luôn thì thôi, tòa nhà cuối cùng đang đứng chính là đáp án
  • Nếu có 2 thang?
    • Làm tương tự như khi có 1 thang
    • Nhưng khi đến hết gạch lần đầu tiên, vì ta còn thừa 1 thang, nên ta lại lặp lại như trước:
    • Tìm tòa nhà có h lớn nhất đã qua (nhưng không bao gồm cái đã chọn lần trước)
    • Dùng thang chỗ đó, tiết kiệm đc số gạch tương ứng
    • Xài hết gạch rồi nghỉ
  • Đến đây có lẽ đáp án đã rõ ràng, với n thang
    • Xài hết gạch có thể, lưu lại các vị trí đã qua
    • Tìm tòa nhà có h lớn nhất trong các tòa đã qua (mà chưa chọn trước đó)
    • Nếu còn thang, dùng 1 thang tại vị trí đó, tiết kiệm đc số gạch tương ứng, lặp lại bước trên
    • Nếu hết thang, nghỉ khỏe
  • Câu hỏi cuối cùng là làm sao để tìm được tòa nhà có h lớn nhất trong các tòa đã qua ? Đáp án nằm ở ngay ý đầu tiên của phần này ?

Solution
Mã giả:

Code:
pq = new <Heap Max>

for <i từ 1 đến heights.length> {
    h = heights[i] - heights[i - 1]
    if (h <= 0) continue // bỏ qua

    // Dùng gạch
    bricks -= h
    pq.push(h)
    if (bricks >= 0) continue; // vẫn còn đủ gạch thì đi tiếp

    // hết gạch thì dùng thang
    if (ladders <= 0) return i - 1 // Hết thang luôn thì nghỉ khỏe

    ladders -= 1 // dùng 1 thang
    bricks += pq.pop() // tiết kiệm số gạch lớn nhất có thể
}

return heights.length - 1

Nhận xét:
  • Solution này có độ phức tạp O(nlogn) cho các thao tác trên heap
  • Đây là một giải pháp greedy vì với mỗi bước ta chỉ quan tâm đến việc vượt qua tòa nhà trước mắt chứ không tính toán cho các trường hợp lâu dài
 
Last edited:
Tự dưng dạo này lại có hứng thú lướt voz làm LC. Nay không có nhiều tgian như trước nên mỗi ngày làm một bài daily của LC thôi. Ae cùng tham gia vào chém gió hằng ngày.

Leetcode Daily là gì ?
Đơn giản là mỗi ngày trên LC sẽ random ra một bài đặt lên đầu trang, cả thế giới đều thấy, đc xem như một challenge nho nhỏ của ngày hôm đấy. Topic này sẽ thảo luận về cách giải của bài đấy. Ví dụ hôm nay (20/06/2022):

View attachment 1220198

Bài hôm nay ?
https://leetcode.com/problems/short-encoding-of-words/

View attachment 1220208
View attachment 1220209

Phân tích chút chút:
  • Constraint này chắc là O(n^2)
  • Bài này có vẻ là bài search string thôi, nếu không có vấn đề gì đặc biệt thì sẽ khá đễ vì search string bth = O(n), lặp qua hết thì O(n^2) vừa đủ pass.
  • Nhưng nó có nhiều dislike nên có khi có nhiều testcase khốn nạn, cần verify kĩ trước khi submit
Thế thôi, mình chưa làm đc nên chưa có solution để post, anh em vào thảo luận nào.

Update:
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.

p/s: Trước mình toàn đăng bài trong topic thuật toán bên kia nhưng h nó nhiều trang quá rồi, lội mệt. Với bên đó nhiều chủ đề quá đăng cái này vào sợ loãng.
Screen Shot 2022-06-21 at 16.26.57.png

Dùng HashSet Java, remove hết items với index === item.length() - 1 <=> xoá trong Set, còn lại các giá trị thô của Set đã loại bỏ indices thừa => Tổng length các String item trong Set + độ dài của Set + 1 (vì có append #), mà chắc cách này chuối quá, k biết có cách nào nhanh hơn ko :(
 
View attachment 1222757
Dùng HashSet Java, remove hết items với index === item.length() - 1 <=> xoá trong Set, còn lại các giá trị thô của Set đã loại bỏ indices thừa => Tổng length các String item trong Set + độ dài của Set + 1 (vì có append #), mà chắc cách này chuối quá, k biết có cách nào nhanh hơn ko :(
Hong hiểu lắm fen ơi ? Nhưng bài này ae bàn nhiều rồi, có bác giải beat 100% luôn nên fen có thể lội lạci các page trước. Vài idea để optimize:
1. Trie thay vì Set ?
2. Sort lại mảng ?
 
Hong hiểu lắm fen ơi ? Nhưng bài này ae bàn nhiều rồi, có bác giải beat 100% luôn nên fen có thể lội lạci các page trước. Vài idea để optimize:
1. Trie thay vì Set ?
2. Sort lại mảng ?
Thank bác, vẫn dùng Set, đặt điều kiện, cải thiện dc tí (k cần sort lại vì HashSet constructor input Array.asList nó đã support rồi :love:

Screen Shot 2022-06-21 at 16.45.03.png
 

21/06/2022​

#1642. (Medium) https://leetcode.com/problems/furthest-building-you-can-reach/

Phân tích bài toán:
  • 1 <= n <= 10^5: Có thể chạy từ O(nlogn) trở xuống
  • Đầu tiên, vì ta bắt buộc phải đi qua từng tòa nhà một theo thứ tự, nên chỉ cần quan tâm độ cao chênh lệch giữa 2 tòa nhà liên tiếp nhau
  • Hay nói cách khác, giá trị đáng quan tâm là h = heights - heights[i - 1] với i = 1 → n
  • Nếu h ≤ 0 thì ta được free 1 tòa nhà, ngược lại, ta sẽ lựa chọn dùng gạch hoặc thang để vượt qua tòa nhà
  • Dễ thấy thang hiệu quả hơn dùng gạch rất nhiều vì có thể vượt qua bất kì độ cao nào, trong khi gạch thì càng cao chi phí càng lớn
  • Suy ra, bài toán chính là tối ưu khi nào nên dùng thang khi nào nên dùng gạch. Hay cách khác, chính là dùng thang sao cho tối ưu nhất có thể

Vậy dùng thang sao cho tối ưu ?
  • Giả sử chỉ có 1 thang:
    • Rõ ràng ta chỉ nên dùng nó ở vị trí có h cao nhất, các chỗ khác thì dùng gạch cho tiết kiệm
    • Nhưng nếu vị trí đó nằm ở cuối mảng, mà ta không có đủ gạch để đi đến đó ? Vậy thì phải chọn vị trí có h cao nhất nhưng trước khi hết gạch
  • Mở rộng ra, nếu có 2 thang thì sao ? Ta sẽ dùng 2 thang ở vị trí có h lớn nhất và nhì trước khi hết gạch
  • Tương tự với n thang, ta sẽ dùng nó cho n vị trí lớn nhất có thể trước khi hết gạch

Vậy ta đã có phương án cơ bản, vấn đề trở thành làm thế nào để tìm được n vị trí lớn nhất này trước khi hết gạch ?
  • Trước tiên cứ keep in mind, khi nào gặp bài toán top k phần tử lớn nhất / bé nhất thì khả năng cao ta sẽ dùng heap (Priority Queue). Vì đây là CDTL tối ưu cho bài toán này
  • Nhưng dùng heap như thế nào ? Ta phân tích tiếp. Nếu giả sử chỉ có 1 thang, ta sẽ:
    • Dùng gạch hết mức có thể
    • Lưu lại trước đó ta đã đi qua tòa nhà có h cao nhất là hmax
    • Rõ ràng để tối ưu ta sẽ dùng thang tại vị trí hmax, nếu thế ta tiết kiệm đc hmax viên gạch
    • Vì hết thang rồi, nên ta tiếp tục xài số gạch còn lại (+ hmax tiết kiệm được) để đi tiếp
    • Đi hết gạch luôn thì thôi, tòa nhà cuối cùng đang đứng chính là đáp án
  • Nếu có 2 thang?
    • Làm tương tự như khi có 1 thang
    • Nhưng khi đến hết gạch lần đầu tiên, vì ta còn thừa 1 thang, nên ta lại lặp lại như trước:
    • Tìm tòa nhà có h lớn nhất đã qua (nhưng không bao gồm cái đã chọn lần trước)
    • Dùng thang chỗ đó, tiết kiệm đc số gạch tương ứng
    • Xài hết gạch rồi nghỉ
  • Đến đây có lẽ đáp án đã rõ ràng, với n thang
    • Xài hết gạch có thể, lưu lại các vị trí đã qua
    • Tìm tòa nhà có h lớn nhất trong các tòa đã qua (mà chưa chọn trước đó)
    • Nếu còn thang, dùng 1 thang tại vị trí đó, tiết kiệm đc số gạch tương ứng, lặp lại bước trên
    • Nếu hết thang, nghỉ khỏe
  • Câu hỏi cuối cùng là làm sao để tìm được tòa nhà có h lớn nhất trong các tòa đã qua ? Đáp án nằm ở ngay ý đầu tiên của phần này ?

Solution
Mã giả:

Code:
pq = new <Heap Max>

for <i từ 1 đến heights.length> {
    h = heights[i] - heights[i - 1]
    if (h <= 0) continue // bỏ qua

    // Dùng gạch
    bricks -= h
    pq.push(h)
    if (bricks >= 0) continue; // vẫn còn đủ gạch thì đi tiếp

    // hết gạch thì dùng thang
    if (ladders <= 0) return i - 1 // Hết thang luôn thì nghỉ khỏe

    ladders -= 1 // dùng 1 thang
    bricks += pq.pop() // tiết kiệm số gạch lớn nhất có thể
}

return heights.length - 1

Nhận xét:
  • Solution này có độ phức tạp O(nlogn) cho các thao tác trên heap
  • Đây là một giải pháp greedy vì với mỗi bước ta chỉ quan tâm đến việc vượt qua tòa nhà trước mắt chứ không tính toán cho các trường hợp lâu dài

Sau khi đọc đề thì mình nghĩ bài này có thể xài binary, để try hard xem sao :(
 
Thử thêm cách reserve trước cái priority_queue thì thấy chạy nhanh hơn hẳn.

1655807389047.png


1 lưu ý cái priority_queue trong C++ nó là dạng adaper container và k có method reserve nên để reserve thì khá là tricky. :big_smile: . Nay lại học thêm đc chiêu này.

C++:
class Solution {
public:
    int furthestBuilding(vector<int>& heights, int bricks, int ladders) {
        vector<int> container;
        container.reserve(heights.size());
        priority_queue<int, vector<int>> maxHeap(std::less<int>(),  std::move(container));
        
        int ret = 0;
        for (int i = 0; i < heights.size() - 1; ++i, ++ret){
            auto h = heights[i+1] - heights[i];
            if (h <= 0) continue;
            if (bricks >= h){
                bricks -=  h;
                maxHeap.push(h);
            } else if (ladders > 0) {
                ladders--;
                if (!maxHeap.empty() && h < maxHeap.top()){
                    bricks += maxHeap.top() - h;
                    maxHeap.pop();
                    maxHeap.push(h);
                }
            } else break;
        }
        return ret;
    }
};
 
Mình chia sẻ nhiều lần trên voz này rồi. Nhưng căn bản thím muốn học LC để làm gì ? Mục tiêu khác nhau có cách học khác nhau

via theNEXTvoz for iPhone
đúng Quân Sư ơi, mục tiêu của em là làm chủ được phần interview thuật toán của các công ty lớn ở Việt Nam
 
Mục đích của họ chắc là để dạy dùng heap hoặc thuật toán quick select.
Bài này sẽ hay hơn nếu cho n lớn tí và C++ không có sẵn hàm std::nth_element. Lúc đó ông nào đè sort vào là chết hết.
Thật sự khá khó mà làm test case work với O(n) nhưng lại chết với O(nlogn), vì thật sự 2 thằng này không chênh lệch gì nhiều. Test case vậy chắc n phải tầm 10^8-10^9. Nặng đến vài GB cho 1 test case thì khá phi thực tế.

Thôi nói chung bài hnay đơn giản là ae nào chưa biết Quick Select thì học thêm Quick Select cho biết :D. Lâu lâu có bài dễ xả stress chứ.
 
Bài này tính ra khá là kinh điển. Trước t đi pv bên aws cũng được hỏi bài này. :big_smile:
Ccmnr. Cách đây rất rất lâu có đi pv shopback cũng được hỏi câu này. Mình hì hục ngồi implement quick select xong ông interviewer bảo sao k dùng sort, mình ngớ luôn. Tuổi trẻ ai cũng có sai lầm =((=((
 
Back
Top