thảo luận [Học Tập] Topic thuật toán

  • Người tạo chủ đề Người tạo chủ đề unknowpc90
  • Ngày bắt đầu Ngày bắt đầu
Phải công nhận 1 điều là học được thuật toán nó tiện lợi vcl mấy bác ạ.
Mới áp dụng thử 1 bài trên leetcode vô dự án của công ty đang làm. Ai ngờ kết quả vượt qua cả sự mong đợi của mình luôn.
Từ 1 đoạn code chạy mấy tới hơn 10p mới xử lý xong. Sau khi chỉnh chu lại thì giờ chạy còn 1p =))
 
#377
Runtime: 0 ms, faster than 100.00% of C++ online submissions for Combination Sum IV.
C++:
class Solution {
public:
    int combinationSum4(vector<int>& nums, int target) {

        vector<unsigned int> dp (1001, 0);
        dp[0] = 1;
        for(int i = 1; i <= target; i++) {
            for(auto c: nums) {
                if(i>=c) dp[i] += dp[i-c];
            }      
        }
  
        return dp[target];
  
    }
};
Viết loop vầy cache destroyer chết :beat_brick: :beat_brick:
Rất nhiều anh trong này chỉ có tư duy làm cho xong mà k (thể) optimize đc, cùng O(1) insert nhưng performance của vector và linkedlist khác nhau rất nhiều đó
 
Viết loop vầy cache destroyer chết :beat_brick: :beat_brick:
Rất nhiều anh trong này chỉ có tư duy làm cho xong mà k (thể) optimize đc, cùng O(1) insert nhưng performance của vector và linkedlist khác nhau rất nhiều đó

Là sao thím, thím viết lại cho mình xem thử nào. Mình code có insert chỗ nào đâu nhỉ :-/
 
Viết loop vầy cache destroyer chết :beat_brick: :beat_brick:
Rất nhiều anh trong này chỉ có tư duy làm cho xong mà k (thể) optimize đc, cùng O(1) insert nhưng performance của vector và linkedlist khác nhau rất nhiều đó
LC 3K, CF 2K5 là đi làm cho FAANG được chưa bác.


Trư newbie không biết tí tẹo gì về lập trình, giờ muốn luyện thuật toán để tương lai xán lạn thì nên bắt đầu với ngôn ngữ nào đây các bác.
 
Tiếp tục chuyên mục mỗi ngày một leetcode. 3 bài mình làm ngày hôm nay:

#375. (Medium) https://leetcode.com/problems/guess-number-higher-or-lower-ii/
#376. (Medium) https://leetcode.com/problems/wiggle-subsequence/
#377. (Medium) https://leetcode.com/problems/combination-sum-iv/

Mình sẽ share về bài Combination Sum IV:
#377. (Medium) https://leetcode.com/problems/combination-sum-iv/

Phân tích bài toán:
  • 1 <= nums.length <= 200: Với limit nhỏ như thế này thì có 2 khả năng:
    • Hoặc là độ phức tạp để giải rất cao O(2^n) hoặc thậm chí là O(n!)
    • Hoặc là kết quả output rất lớn. Nên nếu để limit quá cao sẽ bị tràn số
  • Để biết là dạng nào thì ta có thể thử chạy test với input lớn một tí:
    • Nếu chạy lâu và ra kết quả là một số nhỏ => Trường hợp 1. Lúc này giải pháp tối ưu có lẽ là vét cạn. Các thuật toán phù hợp thường là liệt kê hoặc quay lui.
    • Nếu chạy nhanh nhưng ra kết quả là một số rất lớn => Trường hợp 2. Lúc này hướng giải thì hên xui, nhưng hay gặp nhất chắc là quy hoạch động.
  • Quay lại bài này, ta thấy chỉ cần tăng input lên hơi to một chút là gặp ngay lỗi:

  • => Có vẻ là hướng thứ 2, vậy ta thử giải bài này bằng QHĐ

Một chút về Quy Hoạch Động:
  • Mình biết với nhiều người thuật toán QHĐ là một cái gì đó cao siêu, và dường như chỉ có các bậc thánh nhân chuyên tin mới nghĩ ra cách giải.
  • Cách đây 1 năm mình cũng nghĩ vậy. Nhưng sau một thời gian cày cuốc và làm quen với dạng bài này, thì mình thấy nó cũng k có gì quá cao siêu.
  • Chỉ cần nắm vững hướng tư duy và pattern của nó thì có thể vận dụng trong hầu hết các trường hợp. (Dĩ nhiên trừ những bài quá khó - mình cũng bó tay và vào discuss để xem như mn thôi. Kakaka)
  • Với bạn nào chưa quen thì mình suggest xem clip này về QHĐ. Đây cũng là clip giúp mình làm quen với thuật toán này:
  • Về cơ bản, pattern của thuật toán QHĐ tương tự như đệ quy, hay chia để trị. Đó là kết quả của bàn toán lớn được tính từ kết quả của các bài toán con, và tiếp tục đệ quy như thế cho đến trường hợp cơ bản đã biết trước.
  • Cần chú ý là một bài toán giải bằng QHĐ cần có 2 tính chất:
    • Các bài toán con phải gối nhau: Thì việc lưu lại kết quả của các bài toán con mới có ý nghĩa tối ưu. Không thì nó chỉ là chia để trị thôi.
    • Cấu trúc con tối ưu: Kết quả tối ưu của bài toán lớn phải tính được từ kết quả tối ưu của các bài toán con. Nếu không thì có lưu lại kết quả của bài toán con cũng vô nghĩa.

Quay lại bài toán, ta thử áp dụng cách tiếp cận QHĐ để giải bài này:
  • Để đếm số lượng các combinations có tổng bằng target, ta có thể tính nó từ kết quả của bài toán con hay không ?
  • Vậy bài toán con ở đây là gì ? Hiểu nôm na đó là bài toán tương tự như bài toán gốc, nhưng với input "nhỏ" hơn
  • "Nhỏ" ở đây không nhất thiết là nhỏ hơn về mặt toán học. Mà là input đó "gần" với "trường hợp cơ bản" hơn.
  • "Trường hợp cơ bản" có nghĩa là các trường hợp mà từ đó ta có thể suy ra kết quả ngay lập tức, không cần phải thực hiện việc đưa về bài toán nhỏ hơn nữa.
  • Quay lại, trong trường hợp này, bài toán con có thể là:
    • Cũng với mảng input đó, nhưng target là một số nhỏ hơn
    • Vẫn target đó, nhưng input ít đi một / một vài phần tử
  • Ở đây ta thấy một phần tử có thể được dùng lại nhiều lần, nên xem ra hướng thứ hai khó khả thi. Nếu follow theo hướng thứ nhất, "target thấp hơn" thì "trường hợp cơ bản" sẽ là gì. Ta cứ nghĩ đến các case đơn giản nhất ?

  • Đào sâu thêm một chút thì ta có thể tổng quá hóa thành như thế này:

  • Vậy đưa về bài toán nhỏ hơn như thế nào ? Cái này gọi là "công thức truy hồi" và theo mình cũng là phần khó nhất của một bài QHĐ. Làm được hay không là ở chỗ bạn phải nhìn ra công thức này.
  • Với mình, cách đơn giản là cứ execute một ví dụ và cố gắng thử tính bài toán lớn từ kết quả bài toán nhỏ. Ví dụ nums = [1, 2, 3]; target = 4:
  • Giả sử ta biết số cách tạo ra "target = 3" đi, liệu ta có thể áp dụng nó để tính cho "target = 4" ?
  • Đương nhiên có thể, dễ thấy giả sử có n cách để tạo ra "target = 3", ta chỉ việc cộng 1 vào mỗi cách đó để tạo ra "target = 4". Vì có số 1 nằm trong mảng input.
  • Ồ. Vậy tổng quát hóa lên, nếu ta biết được có n cách tạo ra "target = x" thì cũng sẽ có n cách để tạo ra "target = k", nếu như "k - x" có nằm trong mảng input.
  • Nghĩ ngược lại có vẻ sẽ hay hơn. Ta tính luôn với mỗi số x nằm trong mảng input thì sẽ có thêm combinationSum(k - x) cách để tạo ra số k.
  • Nhưng cũng cần chú ý thêm edge case, nếu x = k luôn thì sao, khi đó vẫn có 1 cách để tạo ra số k, là chọn chính nó. Nhưng combinationSum(k - x) sẽ trả về 0 (vì 0 < nums[0]). Thôi thì ta thêm exception cho case này vậy.
  • Vậy kết quả là tổng các combinationSum(k - x) với mỗi x trong mảng input.
Solution:
  • Mình thích sử dụ Memoization, vì thấy nó gần hơn với suy nghĩ tự nhiên của con người
  • Sort lại nums, lưu mảng nums đã sort lại
  • Viết hàm đệ quy countCombination để tính
  • Nếu target == 0: return 1
  • Nếu target < nums[0]: return 0
  • Nếu k == nums[0]: return 1
  • Nếu target này đã được tính trước đó (nằm trong bảng kết quả), trả kết quả đã tính từ trước
  • Tính kết quả bằng tổng các countCombination(k - x) với mỗi x trong mảng input.
  • Lưu kết quả lại vào bảng kết quả, chú ý đây là bước quan trọng và thể hiện bản chất của quy hoạch động
  • Trả về kết quả đã tính toán

Python:
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        nums.sort()
        self.nums = nums
        return self.countCombination(target, {})
 
    def countCombination(self, target, memo):
        if target == 0:
            return 1
        if target < self.nums[0]:
            return 0
        if target == self.nums[0]:
            return 1
   
        if target in memo:
            return memo[target]
   
        memo[target] = sum(self.countCombination(target - x, memo) for x in self.nums)
        return memo[target]

p/s: Bài này thật ra khá kinh điển. Mình cố ý chọn nó và viết chi tiết để các bạn mới có thể học được cách tư duy và hướng phát triển vấn đề, thay vì chỉ đưa solution. Đồng thời solution bên trên chỉ đạt mức "faster than 11%". Nghĩa là còn có nhiều điểm có thể tối ưu cũng như viết gọn lại. Các bạn cứ đóng góp thoải mái. Welcome.
Góp ý với thím một chút:
  • 1 <= nums.length <= 200: với giới hạn như này thì đpt thường là n^2 đến n^3 chứ không phải là 2^n hay n!
  • Với bài #377 trên còn phải chú ý thêm giới hạn về target để phán đoán phương pháp, vì đề bài cho khá nhỏ nên khả năng đpt có liên quan đến giới hạn target này nữa.
  • Các bước phân tích của thím khá hay, nhưng cố gắng tiến thêm chút nữa là khái quát bằng toán học công thức truy hồi, như vậy quá trình code sẽ dễ dàng hơn.
  • QHĐ cài bằng đệ quy cũng ổn, nếu có thể thì nên cài thêm cách khử đệ quy.
  • 11% là khá chậm, thím xem có thể bị chậm chỗ nào, hoặc đọc cách tối ưu hơn cũng là một cách nâng cao kỹ năng.
Vài dòng góp ý, hi vọng thím tiếp tục chia sẻ cho ae học tập :big_smile:
 
Là sao thím, thím viết lại cho mình xem thử nào. Mình code có insert chỗ nào đâu nhỉ :-/
:beat_brick: :beat_brick: nhầm sang bài coin change, bài này k swap 2 loop cho nhau được :v đại để là nếu loop cái vòng trong trước thì sẽ cache friendly, but not an option in this case :whistle::whistle:
còn vụ kia chỉ là nói metaphor cho việc cùng độ phức tạp nhưng cách implement khác nhau sẽ dẫn đến perf khác nhau
 
Góp ý với thím một chút:

- 1 <= nums.length <= 200: với giới hạn như này thì đpt thường là n^2 đến n^3 chứ không phải là 2^n hay n!
  • Với bài #377 trên còn phải chú ý thêm giới hạn về target để phán đoán phương pháp, vì đề bài cho khá nhỏ nên khả năng đpt có liên quan đến giới hạn target này nữa.
  • Các bước phân tích của thím khá hay, nhưng cố gắng tiến thêm chút nữa là khái quát thành công thức truy hồi, như vậy quá trình code sẽ dễ dàng hơn.
  • QHĐ cài bằng đệ quy cũng ổn, nếu có thể thì nên cài thêm cách khử đệ quy.
  • 11% là khá chậm, thím xem có thể bị chậm chỗ nào, hoặc đọc cách tối ưu hơn cũng là một cách nâng cao kỹ năng.
Vài dòng góp ý, hi vọng thím tiếp tục chia sẻ cho ae học tập :big_smile:
Thank góp ý của bạn nha.
  • Đúng, 200 thì thường là n^3. 2^n hoặc n! thì khoảng vài chục là tắt điện. Tuy nhiên thường các bài 2^n và n! trên leetcode sẽ buộc phải dùng kèm một vài technique để optimize nên vẫn hay gặp giới hạn 200.
  • Việc để ý về giới hạn của target nữa cũng là một cách hay. Mà thật ra là phải để ý tất tần tật các constraint. Chỉ là bài dài quá rồi, đưa thêm đủ thứ vào mình k biết làm sao cho đỡ rối.
  • Mình thích làm đệ quy hơn, thấy nó intuitive hơn. Làm tabulation nói thật đôi khi mình k hiểu nổi.
  • 11% là khá chậm: Sure. Chỉ là mình cố cài đặt thuật toán follow theo các idea mình viết ở trên nên còn nhiều điểm chưa tối ưu. Thôi quan trọng là share được ý tưởng. Còn detail optimization thì mỗi ng làm họ sẽ tự khám phá thêm cách để tối ưu.
 
:beat_brick: nhầm sang bài coin change, bài này k swap 2 loop cho nhau được :v đại để là nếu loop cái vòng trong trước thì sẽ cache friendly, but not an option in this case :whistle::whistle:

:beat_brick:

Tôi đọc lại tưởng thím nhìn nhầm code của ai, vì vector nếu insert liên tục thì đúng là perf giảm, nhưng code của mình thì tạo sẵn ngay từ đầu với fix size luôn rồi :)
 
Bác có thể viết 1 bài về cái học và hiểu QHĐ được ko bác?
E là không. Mình chỉ cố gắng share theo các ví dụ mình làm hằng ngày, hy vọng là chia sẻ được cách mình suy nghĩ, tiếp cận và phát triển vấn đề để tìm ra solution thôi. Chứ chắc k đủ trình để viết chuyên sâu. Vả lại mình có viết thì cũng k tốt được bằng các bài mà bạn có thể search ra trên mạng.
 
Tiếp tục chuyên mục mỗi ngày một leetcode. 3 bài mình làm ngày hôm nay:

#375. (Medium) https://leetcode.com/problems/guess-number-higher-or-lower-ii/
#376. (Medium) https://leetcode.com/problems/wiggle-subsequence/
#377. (Medium) https://leetcode.com/problems/combination-sum-iv/

Mình sẽ share về bài Combination Sum IV:
#377. (Medium) https://leetcode.com/problems/combination-sum-iv/

Phân tích bài toán:
  • 1 <= nums.length <= 200: Với limit nhỏ như thế này thì có 2 khả năng:
    • Hoặc là độ phức tạp để giải rất cao O(2^n) hoặc thậm chí là O(n!)
    • Hoặc là kết quả output rất lớn. Nên nếu để limit quá cao sẽ bị tràn số
  • Để biết là dạng nào thì ta có thể thử chạy test với input lớn một tí:
    • Nếu chạy lâu và ra kết quả là một số nhỏ => Trường hợp 1. Lúc này giải pháp tối ưu có lẽ là vét cạn. Các thuật toán phù hợp thường là liệt kê hoặc quay lui.
    • Nếu chạy nhanh nhưng ra kết quả là một số rất lớn => Trường hợp 2. Lúc này hướng giải thì hên xui, nhưng hay gặp nhất chắc là quy hoạch động.
  • Quay lại bài này, ta thấy chỉ cần tăng input lên hơi to một chút là gặp ngay lỗi:

  • => Có vẻ là hướng thứ 2, vậy ta thử giải bài này bằng QHĐ

Một chút về Quy Hoạch Động:
  • Mình biết với nhiều người thuật toán QHĐ là một cái gì đó cao siêu, và dường như chỉ có các bậc thánh nhân chuyên tin mới nghĩ ra cách giải.
  • Cách đây 1 năm mình cũng nghĩ vậy. Nhưng sau một thời gian cày cuốc và làm quen với dạng bài này, thì mình thấy nó cũng k có gì quá cao siêu.
  • Chỉ cần nắm vững hướng tư duy và pattern của nó thì có thể vận dụng trong hầu hết các trường hợp. (Dĩ nhiên trừ những bài quá khó - mình cũng bó tay và vào discuss để xem như mn thôi. Kakaka)
  • Với bạn nào chưa quen thì mình suggest xem clip này về QHĐ. Đây cũng là clip giúp mình làm quen với thuật toán này:
  • Về cơ bản, pattern của thuật toán QHĐ tương tự như đệ quy, hay chia để trị. Đó là kết quả của bàn toán lớn được tính từ kết quả của các bài toán con, và tiếp tục đệ quy như thế cho đến trường hợp cơ bản đã biết trước.
  • Cần chú ý là một bài toán giải bằng QHĐ cần có 2 tính chất:
    • Các bài toán con phải gối nhau: Thì việc lưu lại kết quả của các bài toán con mới có ý nghĩa tối ưu. Không thì nó chỉ là chia để trị thôi.
    • Cấu trúc con tối ưu: Kết quả tối ưu của bài toán lớn phải tính được từ kết quả tối ưu của các bài toán con. Nếu không thì có lưu lại kết quả của bài toán con cũng vô nghĩa.

Quay lại bài toán, ta thử áp dụng cách tiếp cận QHĐ để giải bài này:
  • Để đếm số lượng các combinations có tổng bằng target, ta có thể tính nó từ kết quả của bài toán con hay không ?
  • Vậy bài toán con ở đây là gì ? Hiểu nôm na đó là bài toán tương tự như bài toán gốc, nhưng với input "nhỏ" hơn
  • "Nhỏ" ở đây không nhất thiết là nhỏ hơn về mặt toán học. Mà là input đó "gần" với "trường hợp cơ bản" hơn.
  • "Trường hợp cơ bản" có nghĩa là các trường hợp mà từ đó ta có thể suy ra kết quả ngay lập tức, không cần phải thực hiện việc đưa về bài toán nhỏ hơn nữa.
  • Quay lại, trong trường hợp này, bài toán con có thể là:
    • Cũng với mảng input đó, nhưng target là một số nhỏ hơn
    • Vẫn target đó, nhưng input ít đi một / một vài phần tử
  • Ở đây ta thấy một phần tử có thể được dùng lại nhiều lần, nên xem ra hướng thứ hai khó khả thi. Nếu follow theo hướng thứ nhất, "target thấp hơn" thì "trường hợp cơ bản" sẽ là gì. Ta cứ nghĩ đến các case đơn giản nhất ?

  • Đào sâu thêm một chút thì ta có thể tổng quá hóa thành như thế này:

  • Vậy đưa về bài toán nhỏ hơn như thế nào ? Cái này gọi là "công thức truy hồi" và theo mình cũng là phần khó nhất của một bài QHĐ. Làm được hay không là ở chỗ bạn phải nhìn ra công thức này.
  • Với mình, cách đơn giản là cứ execute một ví dụ và cố gắng thử tính bài toán lớn từ kết quả bài toán nhỏ. Ví dụ nums = [1, 2, 3]; target = 4:
  • Giả sử ta biết số cách tạo ra "target = 3" đi, liệu ta có thể áp dụng nó để tính cho "target = 4" ?
  • Đương nhiên có thể, dễ thấy giả sử có n cách để tạo ra "target = 3", ta chỉ việc cộng 1 vào mỗi cách đó để tạo ra "target = 4". Vì có số 1 nằm trong mảng input.
  • Ồ. Vậy tổng quát hóa lên, nếu ta biết được có n cách tạo ra "target = x" thì cũng sẽ có n cách để tạo ra "target = k", nếu như "k - x" có nằm trong mảng input.
  • Nghĩ ngược lại có vẻ sẽ hay hơn. Ta tính luôn với mỗi số x nằm trong mảng input thì sẽ có thêm combinationSum(k - x) cách để tạo ra số k.
  • Nhưng cũng cần chú ý thêm edge case, nếu x = k luôn thì sao, khi đó vẫn có 1 cách để tạo ra số k, là chọn chính nó. Nhưng combinationSum(k - x) sẽ trả về 0 (vì 0 < nums[0]). Thôi thì ta thêm exception cho case này vậy.
  • Vậy kết quả là tổng các combinationSum(k - x) với mỗi x trong mảng input.
Solution:
  • Mình thích sử dụ Memoization, vì thấy nó gần hơn với suy nghĩ tự nhiên của con người
  • Sort lại nums, lưu mảng nums đã sort lại
  • Viết hàm đệ quy countCombination để tính
  • Nếu target == 0: return 1
  • Nếu target < nums[0]: return 0
  • Nếu k == nums[0]: return 1
  • Nếu target này đã được tính trước đó (nằm trong bảng kết quả), trả kết quả đã tính từ trước
  • Tính kết quả bằng tổng các countCombination(k - x) với mỗi x trong mảng input.
  • Lưu kết quả lại vào bảng kết quả, chú ý đây là bước quan trọng và thể hiện bản chất của quy hoạch động
  • Trả về kết quả đã tính toán

Python:
class Solution:
    def combinationSum4(self, nums: List[int], target: int) -> int:
        nums.sort()
        self.nums = nums
        return self.countCombination(target, {})
 
    def countCombination(self, target, memo):
        if target == 0:
            return 1
        if target < self.nums[0]:
            return 0
        if target == self.nums[0]:
            return 1
     
        if target in memo:
            return memo[target]
     
        memo[target] = sum(self.countCombination(target - x, memo) for x in self.nums)
        return memo[target]

p/s: Bài này thật ra khá kinh điển. Mình cố ý chọn nó và viết chi tiết để các bạn mới có thể học được cách tư duy và hướng phát triển vấn đề, thay vì chỉ đưa solution. Đồng thời solution bên trên chỉ đạt mức "faster than 11%". Nghĩa là còn có nhiều điểm có thể tối ưu cũng như viết gọn lại. Các bạn cứ đóng góp thoải mái. Welcome.

#377 Đề có vẻ sai nhỉ hay do mình có sai sót:
(1, 1, 2)
(1, 2, 1)
2 cái phương án này thì khác gì nhau ? Đề tìm tổ hợp mà ??????
 

Thống kê chủ đề

Ngày tạo
unknowpc90,
Người trả lời cuối
Spaghetti Code,
Trả lời
1.460
Lượt xem
154.126
Quay lại
Lên đầu trang