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

Mấy bài linkedlist này tôi hay dùng stack với queue cho nhẹ đầu. Khi nào phỏng vấn họ không chịu thì tính sau :beat_brick: Như bài sáng nay tôi dùng 2 queue + 1 stack https://leetcode.com/submissions/detail/752503120/
Tui mà phỏng vấn là tui loại ngay nha.
Cái khó của mấy cái bài LinkedList này là không được dùng thêm memory (bởi nếu dùng thêm memory người ta tạo Array luôn rồi làm việc trên array thì bài toán thành trivial)

Chú tạo thêm queue và đặc biệt stack (stack là array backed) là biến cái bài này thành trivial rồi
 
Tui mà phỏng vấn là tui loại ngay nha.
Cái khó của mấy cái bài LinkedList này là không được dùng thêm memory (bởi nếu dùng thêm memory người ta tạo Array luôn rồi làm việc trên array thì bài toán thành trivial)

Chú tạo thêm queue và đặc biệt stack (stack là array backed) là biến cái bài này thành trivial rồi

Dùng đệ quy thì vẫn là tốn thêm O(n * sizeof(stackframe)) memory rồi, có gì khác nhau đâu.
 
Tui mà phỏng vấn là tui loại ngay nha.
Cái khó của mấy cái bài LinkedList này là không được dùng thêm memory (bởi nếu dùng thêm memory người ta tạo Array luôn rồi làm việc trên array thì bài toán thành trivial)

Chú tạo thêm queue và đặc biệt stack (stack là array backed) là biến cái bài này thành trivial rồi

Thím nóng thế. Tôi thấy đề bài không yêu cầu phải dùng constant memory, cũng không đòi 1 pass nên cứ tìm cách dễ nhất mà làm thôi. Khi nào đề bài bắt buộc thì tính sau :angry:
 
Dùng đệ quy thì vẫn là tốn thêm O(n * sizeof(stackframe)) memory rồi, có gì khác nhau đâu.
Đa số đệ quy đơn giản có thể được tối ưu hóa bằng tail-call recursion bởi compiler nhé.
Những trường hợp này không increase stack size tí nào đâu. Memory vẫn constant O(1) thôi
Bác có thể tìm hiểu tail call recursion optimization.
 
Thím nóng thế. Tôi thấy đề bài không yêu cầu phải dùng constant memory, cũng không đòi 1 pass nên cứ tìm cách dễ nhất mà làm thôi. Khi nào đề bài bắt buộc thì tính sau :angry:
Tui không nóng mà.
Chỉ là ý tui là nếu không ép như vậy thì bài toán sẽ trở nên dễ quá thôi. (Đưa về array xong rồi swap các pair trên array là xong)

Cheer and peace...
 
Đa số đệ quy đơn giản có thể được tối ưu hóa bằng tail-call recursion bởi compiler nhé.
Những trường hợp này không increase stack size tí nào đâu. Memory vẫn constant O(1) thôi
Bác có thể tìm hiểu tail call recursion optimization.

Kiểu chương trình duy áp dụng được tail call optimization là:

Code:
function f() {
     do something
    return do another thing with f()
}

Kể cả kiểu này cũng không phải tail call.
Code:
function f() {
    do some thing
   return f() - 2
}

Thường những bài toán hơi phức tạp chút là hầu như không có cách để đưa về dạng tail call.
Nên không nên dựa vào nó quá nhiều.
 
Last edited:
Kiểu chương trình duy áp dụng được tail call optimization là:

Code:
function f() {
     do something
    return do another thing with f()
}

Kể cả kiểu này cũng không phải tail call.
Code:
function f() {
    do some thing
   return f() - 2
}
Mặc dù có thể sửa lại code thành (-2 + f()), nhưng nếu đoạn đó là cái gì đó khác (bình phương chẳng hạn) thì chưa chắc làm được.

Thường những bài toán hơi phức tạp chút là hầu như không có cách để đưa về dạng tail call.
Nên không nên dựa vào nó quá nhiều.
Ha, cũng chẳng muốn bình luận thêm gì nữa.
Chỉ hỏi bác câu này để xem bác trả lời thế nào:
 
Ha, cũng chẳng muốn bình luận thêm gì nữa.
Chỉ hỏi bác câu này để xem bác trả lời thế nào:

Riêng bài này page trước có bác làm được loop, 1-pass, constant memory nên mình nghĩ có thể chuyển đáp án recursion thành dạng tail call được.
 
Bài sáng nay tôi không dùng Stack hay Queue nhé :ah: https://leetcode.com/submissions/detail/753321799/
Lâu ko dùng c# hay java giờ e sợ mấy cái ngoặc quá :(
Cách của e đây
Python:
class Solution:
    def partition(self, head: Optional[ListNode], x: int) -> Optional[ListNode]:
        dummy=ListNode(next=head)
        cur=head
        last_s=dummy    #keep track of last small node
        last_b=dummy    #keep track of last big node
        while cur:
            next_node=cur.next
            if cur.val<x:         
                if cur!=last_s.next:    # move current node to next of last small
                    tmp=last_s.next
                    last_s.next=cur
                    last_b.next=cur.next   
                    cur.next=tmp
                last_s=cur
            else:   
                last_b=cur
            cur=next_node
        return dummy.next
 
1 dòng C++

C++:
std::stable_partition<Iterator>(head, nullptr, [x](int n) { return n < x; });
return head;

C++:
// https://www.internalpointers.com/post/writing-custom-iterators-modern-cpp
struct Iterator
{
    using iterator_category = std::forward_iterator_tag;
    using difference_type   = std::ptrdiff_t;
    using value_type        = int;
    using pointer           = int*;
    using reference         = int&;
 
 
    Iterator(ListNode* ptr) : m_ptr(ptr) {}
 
    reference operator*() const { return m_ptr->val; }
    pointer operator->() { return &m_ptr->val; }

    // Prefix increment
    Iterator& operator++() { m_ptr = m_ptr->next; return *this; }

    // Postfix increment
    Iterator operator++(int) { Iterator tmp = *this; ++(*this); return tmp; }

    friend bool operator== (const Iterator& a, const Iterator& b) { return a.m_ptr == b.m_ptr; };
    friend bool operator!= (const Iterator& a, const Iterator& b) { return a.m_ptr != b.m_ptr; };  

    ListNode* m_ptr;
};

class Solution {
public:
    ListNode* partition(ListNode* head, int x) {
        std::stable_partition<Iterator>(head, nullptr, [x](int n) { return n < x; });
        return head;
    }
};
 
Screen Shot 2022-07-22 at 16.41.28.png


Bài hôm nay có 1 solution đơn giản là chỉ cần tạo ra 2 LL lưu thứ tự những thằng < x và >= x, chỉ cần one pass LL input để làm được việc này.
Xong việc tiếp theo là gộp 2 LL đã partition này lại làm 1
 
Bài hôm nay khó phết, nghĩ mãi không ra nên phải nhìn cái related topics mới tìm ra hướng đi được :sweat:

  • Dễ dàng nhận thấy cách đơn giản nhất là đem từng item của mảng input (từ phải qua trái) chèn vào một mảng sorted. Khi đó vị trí của item trong mảng sorted sẽ là tổng số các item nhỏ hơn hoặc bằng item đó => dùng binary search với thêm 1 chút sửa đổi sẽ ra được output của bài.
  • Tuy nhiên bài này không thể dễ dàng như vậy. Dùng binary search chỉ tốn O(log(n)) nhưng thao tác insert vào mảng sẽ tốn O(n) => tổng chung sẽ là O(n*(log(n)+n)) = O(n*n) => rất có sẽ bị TLE (tôi submit thử lần 1 và y như dự đoán, TLE ngay lập tức :too_sad:)
  • Vì vậy chỉ có thể tìm cách nào có O(n*log(n)) trở xuống. Tìm mãi không ra cách nào vừa có thể search và insert đều là O(log(n)) nên phải xem related topics và thấy Binary Indexed Tree. Tới đây thì mọi thứ ez rồi:
    • Duyệt qua mảng 1 lần để lọc ra các unique items (sorted), đánh index cho các items này rồi tạo cây BIT tương ứng với các value đều bằng 0.
      • Ví dụ với mảng gốc [5,2,6,2,1] sẽ lọc ra được [1,2,5,6] => tạo BIT có 4 items tương ứng với mảng lọc được.
    • Duyệt từng item trong mảng input (phải qua trái) => tìm index tương ứng của item trong BIT => tính sum của index đó để lấy được kết quả, sau đó tăng value của BIT thêm 1 đơn vị.
      • Với item 1 => ứng với index 0 của BIT => sum BIT của các item trước index 0 sẽ là 0 => mảng output sẽ là [_, _, _, _, 0]. Sau đó tăng index 0 của BIT thêm 1 đơn vị.
      • Với item 2 => ứng với index 1 của BIT => sum BIT của các item trước index 1 sẽ là 1 => mảng output sẽ là [_, _, _, 1, 0]. Sau đó tăng index 1 của BIT thêm 1 đơn vị.
      • Với item 6 => ứng với index 3 của BIT => sum BIT của các item trước index 3 sẽ là 2 => mảng output sẽ là [_, _, 2, 1, 0]. Sau đó tăng index 3 của BIT thêm 1 đơn vị.
      • Với item 2 => ứng với index 1 của BIT => sum BIT của các item trước index 1 sẽ là 1 => mảng output sẽ là [_, 1, 2, 1, 0]. Sau đó tăng index 1 của BIT thêm 1 đơn vị.
      • Với item 5 => ứng với index 2 của BIT => sum BIT của các item trước index 2 sẽ là 3 => mảng output sẽ là [3, 1, 2, 1, 0]. Sau đó tăng index 2 của BIT thêm 1 đơn vị.
  • Các thao tác sum và update của BIT đều là O(log(n)) nên tổng phức tạp sẽ là O(nlog(n)) => không bị TLE.

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

C#:
public class Solution {
    public IList<int> CountSmaller(int[] nums) {
        SortedSet<int> set = new ();
        foreach (var num in nums)
            set.Add(num);
      
        Dictionary<int, int> indexes = new ();
        int len = 0;
        foreach (var num in set)
        {
            indexes[num] = len++;
        }
      
        int[] BIT = new int[len+1];
        int[] ret = new int[nums.Length];
        for (var i = nums.Length - 1; i >= 0; i--)
        {
            int index = indexes[nums[i]];
            ret[i] = sum(BIT, index-1);
            update(BIT, index, 1);
        }
      
        return ret;
    }
  
    void update(int[] BIT, int index, int val)
    {
        index = index + 1;
        while (index < BIT.Length)
        {
            BIT[index] += val;
            index += index & (-index);
        }
    }
  
    int sum(int[] BIT, int index)
    {
        int sum = 0;
        index = index + 1;
        while (index > 0)
        {
            sum += BIT[index];
            index -= index & (-index);
        }
        return sum;
    }
}
 
Last edited:
bài link list thì thím kia nói đúng rồi, phải dùng theo kiểu memory space O(1) mới đúng.

Cách giải thì có 2 cách:
  • Dùng 2 pointer cho 2 list, duyệt qua 1 lần rồi sau đó merge 2 list đó lại với nhau.
  • Cách 2 thì tìm 1 thằng đầu tiên >= x trước, sau đó mấy node khác nhỏ hơn x thì cứ thực hiện thao tác move node ra trước thôi. Cách này xử lý rối hơn tí nhưng ko khó.

Mình dùng cách 2 vẫn pass hết mọi test case
 
Back
Top