🔄 Đệ quy nâng cao (Advanced Recursion)
📖 Giới thiệu
Đệ quy nâng cao là kỹ thuật powerful để giải quyết các vấn đề phức tạp bằng cách chia nhỏ thành các subproblems tương tự. Từ Tower of Hanoi classical đến modern backtracking algorithms, đệ quy là tool không thể thiếu trong competitive programming và algorithm design.
Tại sao đệ quy nâng cao quan trọng?
- Problem decomposition: Chia vấn đề phức tạp thành parts đơn giản hơn
- Tree/Graph traversal: DFS, binary tree operations
- Backtracking: N-Queens, Sudoku, maze solving
- Mathematical problems: Combinatorics, number theory
Các pattern đệ quy chính:
- Linear recursion: Fibonacci, factorial
- Tree recursion: Binary tree traversal
- Tail recursion: Optimized recursive calls
- Backtracking: Trial and error với undo
- Divide & Conquer: Merge sort, quick sort
🔧 Cú pháp
Classic Recursive Problems
#include <iostream>
#include <vector>
#include <string>
using namespace std;
// Tower of Hanoi - Classic recursion problem
void towerOfHanoi(int n, char from, char to, char aux) {
if (n == 1) {
cout << "Move disk 1 from " << from << " to " << to << endl;
return;
}
// Move n-1 disks from source to auxiliary
towerOfHanoi(n - 1, from, aux, to);
// Move the largest disk from source to destination
cout << "Move disk " << n << " from " << from << " to " << to << endl;
// Move n-1 disks from auxiliary to destination
towerOfHanoi(n - 1, aux, to, from);
}
// N-Queens Problem - Backtracking
class NQueens {
private:
vector<vector<string>> solutions;
vector<string> board;
int n;
bool isSafe(int row, int col) {
// Check column
for (int i = 0; i < row; i++) {
if (board[i][col] == 'Q') return false;
}
// Check diagonal (top-left)
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j] == 'Q') return false;
}
// Check diagonal (top-right)
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j] == 'Q') return false;
}
return true;
}
void backtrack(int row) {
if (row == n) {
solutions.push_back(board);
return;
}
for (int col = 0; col < n; col++) {
if (isSafe(row, col)) {
board[row][col] = 'Q'; // Place queen
backtrack(row + 1); // Recursive call
board[row][col] = '.'; // Backtrack
}
}
}
public:
vector<vector<string>> solveNQueens(int size) {
n = size;
board = vector<string>(n, string(n, '.'));
solutions.clear();
backtrack(0);
return solutions;
}
};
// Permutations using backtracking
void generatePermutations(vector<int>& nums, vector<int>& current,
vector<bool>& used, vector<vector<int>>& result) {
if (current.size() == nums.size()) {
result.push_back(current);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (!used[i]) {
used[i] = true;
current.push_back(nums[i]);
generatePermutations(nums, current, used, result);
current.pop_back(); // Backtrack
used[i] = false;
}
}
}Tree Recursion
// Binary Tree Node
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
// Tree traversals using recursion
void inorderTraversal(TreeNode* root, vector<int>& result) {
if (root) {
inorderTraversal(root->left, result); // Left
result.push_back(root->val); // Root
inorderTraversal(root->right, result); // Right
}
}
void preorderTraversal(TreeNode* root, vector<int>& result) {
if (root) {
result.push_back(root->val); // Root
preorderTraversal(root->left, result); // Left
preorderTraversal(root->right, result); // Right
}
}
void postorderTraversal(TreeNode* root, vector<int>& result) {
if (root) {
postorderTraversal(root->left, result); // Left
postorderTraversal(root->right, result); // Right
result.push_back(root->val); // Root
}
}
// Tree properties using recursion
int maxDepth(TreeNode* root) {
if (!root) return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
bool isSymmetric(TreeNode* root) {
if (!root) return true;
function<bool(TreeNode*, TreeNode*)> helper = [&](TreeNode* left, TreeNode* right) {
if (!left && !right) return true;
if (!left || !right) return false;
return (left->val == right->val) &&
helper(left->left, right->right) &&
helper(left->right, right->left);
};
return helper(root->left, root->right);
}
// Path sum problems
bool hasPathSum(TreeNode* root, int targetSum) {
if (!root) return false;
if (!root->left && !root->right) {
return root->val == targetSum;
}
return hasPathSum(root->left, targetSum - root->val) ||
hasPathSum(root->right, targetSum - root->val);
}Advanced Backtracking
// Sudoku Solver
class SudokuSolver {
private:
bool isValid(vector<vector<char>>& board, int row, int col, char num) {
// Check row
for (int j = 0; j < 9; j++) {
if (board[row][j] == num) return false;
}
// Check column
for (int i = 0; i < 9; i++) {
if (board[i][col] == num) return false;
}
// Check 3x3 box
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++) {
for (int j = startCol; j < startCol + 3; j++) {
if (board[i][j] == num) return false;
}
}
return true;
}
bool solve(vector<vector<char>>& board) {
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
if (board[i][j] == '.') {
for (char num = '1'; num <= '9'; num++) {
if (isValid(board, i, j, num)) {
board[i][j] = num;
if (solve(board)) {
return true;
}
board[i][j] = '.'; // Backtrack
}
}
return false; // No valid number found
}
}
}
return true; // All cells filled
}
public:
void solveSudoku(vector<vector<char>>& board) {
solve(board);
}
};
// Maze pathfinding with backtracking
class MazeSolver {
private:
vector<vector<int>> directions = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
bool isValid(vector<vector<int>>& maze, int x, int y, vector<vector<bool>>& visited) {
int m = maze.size(), n = maze[0].size();
return x >= 0 && x < m && y >= 0 && y < n &&
maze[x][y] == 0 && !visited[x][y];
}
bool findPath(vector<vector<int>>& maze, int x, int y, int destX, int destY,
vector<vector<bool>>& visited, vector<pair<int, int>>& path) {
if (x == destX && y == destY) {
path.push_back({x, y});
return true;
}
visited[x][y] = true;
path.push_back({x, y});
for (auto& dir : directions) {
int newX = x + dir[0];
int newY = y + dir[1];
if (isValid(maze, newX, newY, visited)) {
if (findPath(maze, newX, newY, destX, destY, visited, path)) {
return true;
}
}
}
// Backtrack
path.pop_back();
visited[x][y] = false;
return false;
}
public:
vector<pair<int, int>> solveMaze(vector<vector<int>>& maze,
pair<int, int> start, pair<int, int> end) {
vector<pair<int, int>> path;
vector<vector<bool>> visited(maze.size(),
vector<bool>(maze[0].size(), false));
if (findPath(maze, start.first, start.second,
end.first, end.second, visited, path)) {
return path;
}
return {}; // No path found
}
};🔬 Phân tích & Giải thích chi tiết
Recursion vs Iteration Trade-offs
Recursion Advantages:
- Elegant code: More readable for tree/graph problems
- Natural fit: Some problems are inherently recursive
- Divide & conquer: Easy to implement
- Backtracking: Natural undo mechanism
Recursion Disadvantages:
- Stack overflow: Deep recursion can exceed stack space
- Performance: Function call overhead
- Memory usage: Each call uses stack frame
- Debugging: Harder to trace execution
Optimization Techniques
Tail Recursion Optimization:
// Non-tail recursive (not optimizable)
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1); // Operation after recursive call
}
// Tail recursive (optimizable)
int factorialTail(int n, int acc = 1) {
if (n <= 1) return acc;
return factorialTail(n - 1, n * acc); // Recursive call is last operation
}
// Iterative equivalent
int factorialIterative(int n) {
int result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}Memoization for Recursive Problems:
// Fibonacci with memoization
unordered_map<int, long long> memo;
long long fibonacciMemo(int n) {
if (n <= 1) return n;
if (memo.find(n) != memo.end()) {
return memo[n];
}
memo[n] = fibonacciMemo(n - 1) + fibonacciMemo(n - 2);
return memo[n];
}💻 Ví dụ minh họa
Ví dụ 1: Advanced Combinatorial Problems
#include <iostream>
#include <vector>
#include <string>
#include <algorithm>
using namespace std;
class CombinatoricsRecursion {
public:
// Generate all subsets (Power Set)
void generateSubsets(vector<int>& nums, int index, vector<int>& current,
vector<vector<int>>& result) {
if (index == nums.size()) {
result.push_back(current);
return;
}
// Don't include current element
generateSubsets(nums, index + 1, current, result);
// Include current element
current.push_back(nums[index]);
generateSubsets(nums, index + 1, current, result);
current.pop_back(); // Backtrack
}
// Generate combinations of size k
void generateCombinations(vector<int>& nums, int start, int k,
vector<int>& current, vector<vector<int>>& result) {
if (current.size() == k) {
result.push_back(current);
return;
}
for (int i = start; i < nums.size(); i++) {
current.push_back(nums[i]);
generateCombinations(nums, i + 1, k, current, result);
current.pop_back(); // Backtrack
}
}
// Generate all valid parentheses combinations
void generateParentheses(int n, int open, int close, string current,
vector<string>& result) {
if (current.length() == 2 * n) {
result.push_back(current);
return;
}
if (open < n) {
generateParentheses(n, open + 1, close, current + "(", result);
}
if (close < open) {
generateParentheses(n, open, close + 1, current + ")", result);
}
}
// Word Break problem using recursion with memoization
bool wordBreakHelper(string s, vector<string>& wordDict, int start,
unordered_set<string>& wordSet, vector<int>& memo) {
if (start == s.length()) {
return true;
}
if (memo[start] != -1) {
return memo[start];
}
for (int end = start + 1; end <= s.length(); end++) {
string substr = s.substr(start, end - start);
if (wordSet.find(substr) != wordSet.end() &&
wordBreakHelper(s, wordDict, end, wordSet, memo)) {
memo[start] = 1;
return true;
}
}
memo[start] = 0;
return false;
}
bool wordBreak(string s, vector<string>& wordDict) {
unordered_set<string> wordSet(wordDict.begin(), wordDict.end());
vector<int> memo(s.length(), -1);
return wordBreakHelper(s, wordDict, 0, wordSet, memo);
}
void demonstrateRecursion() {
cout << "=== ADVANCED RECURSION DEMONSTRATIONS ===" << endl;
// Subsets demo
cout << "\n🔢 Generating all subsets of [1, 2, 3]:" << endl;
vector<int> nums = {1, 2, 3};
vector<vector<int>> subsets;
vector<int> current;
generateSubsets(nums, 0, current, subsets);
for (const auto& subset : subsets) {
cout << "[";
for (int i = 0; i < subset.size(); i++) {
cout << subset[i];
if (i < subset.size() - 1) cout << ", ";
}
cout << "] ";
}
cout << endl;
// Combinations demo
cout << "\n🔢 Generating combinations of size 2 from [1, 2, 3, 4]:" << endl;
vector<int> nums2 = {1, 2, 3, 4};
vector<vector<int>> combinations;
current.clear();
generateCombinations(nums2, 0, 2, current, combinations);
for (const auto& combo : combinations) {
cout << "[" << combo[0] << ", " << combo[1] << "] ";
}
cout << endl;
// Parentheses demo
cout << "\n📋 Valid parentheses for n=3:" << endl;
vector<string> parentheses;
generateParentheses(3, 0, 0, "", parentheses);
for (const string& p : parentheses) {
cout << p << " ";
}
cout << endl;
// Word break demo
cout << "\n📝 Word break example:" << endl;
string s = "leetcode";
vector<string> wordDict = {"leet", "code"};
bool canBreak = wordBreak(s, wordDict);
cout << "Can break '" << s << "': " << (canBreak ? "Yes" : "No") << endl;
}
};
int main() {
CombinatoricsRecursion demo;
demo.demonstrateRecursion();
return 0;
}🏋️ Thực hành
Bài tập 1: Knight's Tour Problem
Implement Knight's Tour sử dụng backtracking:
- 8x8 chessboard
- Knight phải visit mỗi square exactly once
- Return valid tour path
Bài tập 2: Expression Evaluation
Build recursive expression evaluator:
- Support +, -, *, /, parentheses
- Handle operator precedence
- Error handling cho invalid expressions
Bài tập 3: Tree Serialization
Implement binary tree serialization/deserialization:
- Convert tree to string representation
- Reconstruct tree from string
- Handle null nodes properly
Bài tập 4: Palindrome Partitioning
Find all possible palindrome partitions của string:
- Each partition phải là palindrome
- Return all valid partitioning ways
- Optimize với memoization
📋 Tóm tắt
Các điểm quan trọng
Recursion Patterns:
- Linear: Simple chain of calls
- Tree: Multiple recursive calls
- Backtracking: Try and undo approach
- Tail: Last operation is recursive call
Common Applications:
- Tree/Graph traversal: DFS, tree operations
- Combinatorics: Permutations, combinations
- Puzzle solving: N-Queens, Sudoku
- String processing: Parsing, pattern matching
Optimization Strategies:
- Memoization: Cache results
- Tail recursion: Enable compiler optimization
- Iterative conversion: When stack space is concern
- Early termination: Prune unnecessary branches
Best Practices
Design:
- Define clear base case
- Ensure progress toward base case
- Minimize parameter passing
- Use appropriate data structures
Performance:
- Consider iterative alternatives
- Use memoization cho overlapping subproblems
- Monitor stack depth
- Profile recursive vs iterative performance
Chuẩn bị cho bài tiếp theo
Bài học tiếp theo sẽ tìm hiểu về Dynamic Programming:
- Memoization: Top-down approach
- Tabulation: Bottom-up approach
- Classic Problems: Fibonacci, Knapsack, LIS
- Optimization: Space complexity reduction
- Advanced DP: 2D, tree DP, digit DP
Advanced recursion là foundation cho many algorithmic techniques. Master recursion để solve complex problems elegantly!