listC++ Data Structure & Algorithm
ตัวอย่างโจทย์ Leet Code
สารบัญ
ตัวอย่างโจทย์ Backtracking
Problem 78 - Subset
https://leetcode.com/problems/subsets/description/
โจทย์แบบสั้นๆ: มี array nums ที่เก็บตัวเลขไม่ซ้ำกัน จงคืน “ทุก subset” ที่เป็นไปได้ของตัวเลข nums ออกมา
สำหรับข้อนี้ สามารถการแก้ปัญหานี้ด้วยการใช้ Backtracking (เนื่องจากต้องแตะทุก case) เราจะสร้างชุดย่อยทีละชุดโดยการเพิ่มหรือไม่เพิ่มองค์ประกอบในแต่ละขั้นตอน จากนั้นเรียกใช้ function backtrack เองอีกครั้งเพื่อสร้างชุดย่อยถัดไปออกมา
class Solution {
public:
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> result;
vector<int> subset;
backtrack(result, subset, nums, 0);
return result;
}
private:
void backtrack(vector<vector<int>> &result, vector<int> &subset,
vector<int> &nums, int start) {
// เพิ่มชุดย่อยปัจจุบันลงในผลลัพธ์
result.push_back(subset);
// สร้างชุดย่อยถัดไปโดยเพิ่มหรือไม่เพิ่มองค์ประกอบ
for (int i = start; i < nums.size(); i++) {
subset.push_back(nums[i]);
backtrack(result, subset, nums, i + 1);
subset.pop_back();
}
}
};ในการทำงาน เราจะเริ่มต้นด้วย subset ว่างเปล่า subset และส่งมันไปยัง function backtrack พร้อมกับตำแหน่งเริ่มต้นที่ 0
ภายใน function backtrack
- เราเพิ่ม subset ปัจจุบัน
subsetลงในผลลัพธ์result - จากนั้นเราวน loop ผ่านตัวที่เหลือใน Array
numsโดยเริ่มจากตำแหน่งstart- สำหรับแต่ละตัว เราเพิ่มมันลงใน
subset - เรียกใช้ function
backtrackอีกครั้งด้วยตำแหน่งเริ่มต้นถัดไป (i + 1) เพื่อสร้าง subset ถัดไป - หลังจากนั้น เราลบองค์ประกอบที่เพิ่มเข้าไปออกจาก
subsetเพื่อเตรียมสร้างชุดย่อยถัดไป
- สำหรับแต่ละตัว เราเพิ่มมันลงใน
ตัวอย่างการทำงาน เช่น
nums = [1][2][3]- เริ่มต้นด้วย subset ว่าง
[]และเพิ่มลงในผลลัพธ์ - เพิ่ม 1 ลงใน subset
[1]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 1- เพิ่ม
[1]ลงในผลลัพธ์ - เพิ่ม 2 ลงใน subset
[1][2]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 2- เพิ่ม
[1][2]ลงในผลลัพธ์ - เพิ่ม 3 ลงใน subset
[1][2][3]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 3- เพิ่ม
[1][2][3]ลงในผลลัพธ์
- เพิ่ม
- ลบ 3 ออกจาก subset
[1][2]
- เพิ่ม
- ลบ 2 ออกจาก subset
[1]
- เพิ่ม
- ไม่เพิ่มอะไรลงใน subset
[]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 1- เพิ่ม
[]ลงในผลลัพธ์ - เพิ่ม 2 ลงใน subset
[2]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 2- เพิ่ม
[2]ลงในผลลัพธ์ - เพิ่ม 3 ลงใน subset
[2][3]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 3- เพิ่ม
[2][3]ลงในผลลัพธ์
- เพิ่ม
- ลบ 3 ออกจาก subset
[2]
- เพิ่ม
- ลบ 2 ออกจาก subset
[]
- เพิ่ม
- ไม่เพิ่มอะไรลงใน subset
[]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 2- เพิ่ม
[]ลงในผลลัพธ์ - เพิ่ม 3 ลงใน subset
[3]และเรียกใช้backtrackกับตำแหน่งเริ่มต้น 3- เพิ่ม
[3]ลงในผลลัพธ์
- เพิ่ม
- ลบ 3 ออกจาก subset
[]
- เพิ่ม
ผลลัพธ์ที่ได้คือ [[],[1],[2],[1][2],[3],[1][3],[2][3],[1][2][3]] ซึ่งเป็น subset ทั้งหมดของ [1][2][3]
การแก้ปัญหานี้มีความซับซ้อนเวลาทำงานในกรณีเลวร้ายสุดอยู่ที่ O(N * 2^N) เนื่องจากเราต้องสร้าง subset ทั้งหมด 2^N ชุด และสำหรับแต่ละ subset เราต้องผ่านองค์ประกอบทั้งหมด N องค์ประกอบ ดังนั้น เวลาก็จะคำนวนออกมาได้เป็น N * 2^N ออกมานั่นเอง
Problem 46 - Permutations
https://leetcode.com/problems/permutations/description/
โจทย์แบบสั้นๆ: มี array nums ที่เก็บตัวเลขไม่ซ้ำกัน จงเรียงตัวเลขชุดนี้ “ทุกรูปแบบ” ที่เป็นไปได้ออกมา (all permutation)
สำหรับข้อนี้ เราจะสร้างการเรียงสับเปลี่ยนทีละการเรียงสับเปลี่ยน โดยการสลับตำแหน่งของตัวเลขในแต่ละขั้นตอน จากนั้นเรียกใช้ function backtrack เองอีกครั้งเพื่อสร้างการเรียงสับเปลี่ยนถัดไป
class Solution {
public:
vector<vector<int>> permute(vector<int> &nums) {
vector<vector<int>> result;
vector<int> permutation;
backtrack(result, permutation, nums);
return result;
}
private:
void backtrack(vector<vector<int>> &result, vector<int> &permutation,
vector<int> &nums) {
// ถ้าขนาดของ permutation เท่ากับขนาดของ Array nums
// แสดงว่าสร้าง permutation เสร็จแล้ว
if (permutation.size() == nums.size()) {
result.push_back(permutation);
return;
}
// สร้าง permutation ถัดไปโดยสลับตำแหน่งของตัวเลข
for (int i = 0; i < nums.size(); i++) {
// ข้ามตัวเลขที่ถูกใช้ไปแล้วใน permutation ปัจจุบัน
if (find(permutation.begin(), permutation.end(), nums[i]) !=
permutation.end()) {
continue;
}
// เพิ่มตัวเลขลงใน permutation ปัจจุบัน
permutation.push_back(nums[i]);
// สร้าง permutation ถัดไป
backtrack(result, permutation, nums);
// ย้อนกลับ (backtrack) โดยลบตัวเลขออกจาก permutation ปัจจุบัน
permutation.pop_back();
}
}
};ในการทำงาน เราจะเริ่มต้นด้วยการ สร้าง permutation ว่างเปล่าขึ้นมา permutation และส่งมันไปยัง function backtrack พร้อมกับ Array nums
ภายใน function backtrack:
- ถ้าขนาดของ permutation ปัจจุบัน
permutationเท่ากับขนาดของ Arraynumsแสดงว่าเราสร้าง permutation เสร็จแล้ว เราจะเพิ่ม permutation นี้ลงในผลลัพธ์result - มิฉะนั้น เราจะวนลูปผ่านตัวเลขทั้งหมดใน Array
nums- ถ้าตัวเลขนั้นยังไม่ถูกใช้ใน permutation ปัจจุบัน
permutationเราจะเพิ่มตัวเลขนั้นลงใน permutation - จากนั้นเรียกใช้ function
backtrackอีกครั้งเพื่อสร้าง permutation ถัดไป - หลังจากนั้น เราลบตัวเลขที่เพิ่มเข้าไปออกจาก permutation
permutationเพื่อเตรียมสร้าง permutation ถัดไป
- ถ้าตัวเลขนั้นยังไม่ถูกใช้ใน permutation ปัจจุบัน
ตัวอย่างการทำงาน:
nums = [1][2][3]- เริ่มต้นด้วย permutation ว่าง
[] - เพิ่ม 1 ลงใน permutation
[1]และเรียกใช้backtrack- เพิ่ม 2 ลงใน permutation
[1][2]และเรียกใช้backtrack- เพิ่ม 3 ลงใน permutation
[1][2][3]และเพิ่มลงในผลลัพธ์ - ลบ 3 ออกจาก permutation
[1][2]
- เพิ่ม 3 ลงใน permutation
- ลบ 2 ออกจาก permutation
[1] - เพิ่ม 3 ลงใน permutation
[1][3]และเรียกใช้backtrack- เพิ่ม 2 ลงใน permutation
[1][3][2]และเพิ่มลงในผลลัพธ์ - ลบ 2 ออกจาก permutation
[1][3]
- เพิ่ม 2 ลงใน permutation
- ลบ 3 ออกจาก permutation
[1]
- เพิ่ม 2 ลงใน permutation
- เพิ่ม 2 ลงใน permutation
[2]และเรียกใช้backtrack- เพิ่ม 1 ลงใน permutation
[2][1]และเรียกใช้backtrack- เพิ่ม 3 ลงใน permutation
[2][1][3]และเพิ่มลงในผลลัพธ์ - ลบ 3 ออกจาก permutation
[2][1]
- เพิ่ม 3 ลงใน permutation
- ลบ 1 ออกจาก permutation
[2] - เพิ่ม 3 ลงใน permutation
[2][3]และเรียกใช้backtrack- เพิ่ม 1 ลงใน permutation
[2][3][1]และเพิ่มลงในผลลัพธ์ - ลบ 1 ออกจาก permutation
[2][3]
- เพิ่ม 1 ลงใน permutation
- ลบ 3 ออกจาก permutation
[2]
- เพิ่ม 1 ลงใน permutation
- เพิ่ม 3 ลงใน permutation
[3]และเรียกใช้backtrack- เพิ่ม 1 ลงใน permutation
[3][1]และเรียกใช้backtrack- เพิ่ม 2 ลงใน permutation
[3][1][2]และเพิ่มลงในผลลัพธ์ - ลบ 2 ออกจาก permutation
[3][1]
- เพิ่ม 2 ลงใน permutation
- ลบ 1 ออกจาก permutation
[3] - เพิ่ม 2 ลงใน permutation
[3][2]และเรียกใช้backtrack- เพิ่ม 1 ลงใน permutation
[3][2][1]และเพิ่มลงในผลลัพธ์ - ลบ 1 ออกจาก permutation
[3][2]
- เพิ่ม 1 ลงใน permutation
- ลบ 2 ออกจาก permutation
[3]
- เพิ่ม 1 ลงใน permutation
ผลลัพธ์ที่ได้คือ [[1][2][3], [1][3][2], [2][1][3], [2][3][1], [3][1][2], [3][2][1]] ซึ่งเป็น permutation ทั้งหมดของ [1][2][3]
การแก้ปัญหานี้มีความซับซ้อนเวลาทำงานในกรณีเลวร้ายสุดอยู่ที่ O(N * N!) เนื่องจากเราต้องสร้าง permutation ทั้งหมด N! permutation และสำหรับ
Problem 39 - Combination Sum
https://leetcode.com/problems/combination-sum/description/
โจทย์แบบสั้นๆ: ให้ตัวเลข array candidates มา พร้อมกับตัวเลข target จงคืน array ของ **combinations ของ candidates ที่ สามารถรวมกันได้เท่ากับ target โดย “สามารถใช้ตัวเลขใน candidate ซ้ำกี่รอบก็ได้”
การแก้ปัญหานี้ด้วย Backtracking เราจะสร้างผลรวมของตัวเลขที่เป็นไปได้ทีละผลรวม โดยการเพิ่มหรือไม่เพิ่มตัวเลขในแต่ละขั้นตอน จากนั้นเรียกใช้ function backtrack เองอีกครั้งเพื่อสร้างผลรวมถัดไป
class Solution {
public:
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
vector<vector<int>> result;
vector<int> curr;
backtrack(candidates, target, 0, curr, result);
return result;
}
private:
void backtrack(vector<int> &candidates, int target, int start,
vector<int> &curr, vector<vector<int>> &result) {
if (target < 0) {
return;
} else if (target == 0) {
result.push_back(curr);
} else {
for (int i = start; i < candidates.size(); i++) {
curr.push_back(candidates[i]);
backtrack(candidates, target - candidates[i], i, curr, result); // Reuse the same index i
curr.pop_back();
}
}
}
};การทำงาน
- function หลัก เรามี function หลัก
combinationSumที่รับ Array ของตัวเลขcandidatesและเป้าหมายผลรวมtargetเป็น input และส่งกลับ Array ของ Array ที่เป็นผลรวมทั้งหมดที่เป็นไปได้ - function ย่อย backtrack ภายใน function หลัก เราจะเรียกใช้ function ย่อย
backtrackที่จะทำการค้นหาผลรวมทั้งหมดที่เป็นไปได้ - ตรวจสอบเงื่อนไขการหยุด ภายใน function
backtrackเราจะตรวจสอบเงื่อนไขการหยุดก่อน- ถ้า
targetเท่ากับ 0 แสดงว่าเราพบผลรวมที่ถูกต้อง เราจะเพิ่มผลรวมปัจจุบันลงในผลลัพธ์result - ถ้า
targetน้อยกว่า 0 หรือเราวนลูปจนถึงท้าย Array แล้ว แสดงว่าไม่สามารถสร้างผลรวมได้อีก เราจะหยุดการทำงานของ function นี้
- ถ้า
- สร้างผลรวมถัดไป ถ้ายังไม่ถึงเงื่อนไขการหยุด เราจะวนลูปผ่านตัวเลขทั้งหมดใน Array
candidatesโดยเริ่มจากตำแหน่งstart- เพิ่มตัวเลขปัจจุบันลงในผลรวมชั่วคราว
combination - เรียกใช้ function
backtrackอีกครั้งด้วยเป้าหมายผลรวมที่เหลือtarget - candidates[i]และตำแหน่งเริ่มต้นเดิมstart(เนื่องจากสามารถใช้ตัวเลขซ้ำได้) - หลังจากนั้น เราลบตัวเลขที่เพิ่มเข้าไปออกจากผลรวมชั่วคราว
combinationเพื่อเตรียมสร้างผลรวมถัดไป
- เพิ่มตัวเลขปัจจุบันลงในผลรวมชั่วคราว
ตัวอย่างการทำงานสำหรับ candidates = [2][3][6][7] และ target = 7:
- เริ่มต้นด้วยผลรวมว่าง
[]และเป้าหมาย 7 - เพิ่ม 2 ลงในผลรวม
[2]และเรียกใช้backtrackกับเป้าหมาย 5- เพิ่ม 3 ลงในผลรวม
[2][3]และเรียกใช้backtrackกับเป้าหมาย 2- ไม่สามารถเพิ่มตัวเลขใดลงในผลรวมได้อีก เนื่องจากทุกตัวเลขมากกว่าเป้าหมาย
- ลบ 3 ออกจากผลรวม
[2] - เพิ่ม 2 ลงในผลรวม
[2][2]และเรียกใช้backtrackกับเป้าหมาย 3- เพิ่ม 3 ลงในผลรวม
[2][2][3]และเพิ่มลงในผลลัพธ์ - ลบ 3 ออกจากผลรวม
[2][2]
- เพิ่ม 3 ลงในผลรวม
- ลบ 2 ออกจากผลรวม
[2]
- เพิ่ม 3 ลงในผลรวม
- ไม่เพิ่มอะไรลงในผลรวม
[]และเรียกใช้backtrackกับเป้าหมาย 7- เพิ่ม 3 ลงในผลรวม
[3]และเรียกใช้backtrackกับเป้าหมาย 4- เพิ่ม 2 ลงในผลรวม
[3][2]และเรียกใช้backtrackกับเป้าหมาย 2- เพิ่ม 2 ลงในผลรวม
[3][2][2]แต่ผลรวมเกิน 7 จึงไม่ได้เพิ่มลงในผลลัพธ์
- เพิ่ม 2 ลงในผลรวม
- ลบ 2 ออกจากผลรวม
[3][2] - ลบ 3 ออกจากผลรวม
[]
- เพิ่ม 2 ลงในผลรวม
- เพิ่ม 6 ลงในผลรวม
[6]และเรียกใช้backtrackกับเป้าหมาย 1- ไม่สามารถเพิ่มตัวเลขใดลงในผลรวมได้อีก เนื่องจากทุกตัวเลขมากกว่าเป้าหมาย
- เพิ่ม 7 ลงในผลรวม
[7]และเรียกใช้backtrackกับเป้าหมาย 0- เพิ่ม
[7]ลงในผลลัพธ์
- เพิ่ม
- เพิ่ม 3 ลงในผลรวม
ผลลัพธ์ที่ได้คือ [[2][2][3], [7]]
ปัญหาอื่นๆสำหรับ Backtrack
https://leetcode.com/problems/sudoku-solver/description/
สรุป
Backtrack Algorithm เป็นเทคนิคการแก้ปัญหาที่ใช้วิธีการแบบ Brute Force ในการค้นหาคำตอบที่ต้องการ โดยจะลองทุกทางเลือกที่เป็นไปได้และเลือกคำตอบที่ดีที่สุด
คำว่า "ย้อนกลับ" (Backtrack) หมายถึงหากทางเลือกปัจจุบันไม่เหมาะสม ก็จะย้อนกลับไปทางเลือกก่อนหน้าและลองทางเลือกอื่นแทน ดังนั้นจึงมีการใช้เทคนิคเรียกซ้ำ (Recursion) ในการทำงาน
เมื่อใดควรพิจารณาใช้ algorithm การย้อนกลับ
Backtrack algorithm มักถูกนำมาใช้ในกรณีดังต่อไปนี้
- ปัญหาที่มีคำตอบได้หลายคำตอบ เช่น การหาเส้นทางทั้งหมดใน graph, การจัดเรียงตัวอักษรทั้งหมด, การแก้ปริศนาเช่น N-Queens หรือ Sudoku เป็นต้น
- ปัญหาที่สามารถแบ่งเป็นปัญหาย่อยๆ ที่คล้ายกัน เนื่องจาก Backtrack algorithm จะแก้ปัญหาโดยการสร้างคำตอบแบบค่อยเป็นค่อยไป จึงเหมาะกับปัญหาที่สามารถแบ่งเป็นปัญหาย่อยๆ ที่มีลักษณะคล้ายกัน
- ปัญหาที่มีข้อจำกัดหรือเงื่อนไขที่คำตอบต้องสนองตอบ เช่น ในปัญหา N-Queens ต้องวางตำแหน่ง Queen ไม่ให้ Queen ตัวใดสามารถรุกได้ เป็นต้น
โดยสรุป Backtrack algorithm เหมาะสำหรับปัญหาที่ต้องการหาคำตอบทั้งหมดหรือคำตอบที่ดีที่สุด โดยมีข้อจำกัดหรือเงื่อนไขบางประการ และสามารถแบ่งปัญหาออกเป็นปัญหาย่อยๆ ที่คล้ายกันได้
