Hi folks! I created a list of backtracking problems that can be useful to practice to solve more problems during interview / contest. Backtracking algorithm is straightforward, but when it comes to real problems sometimes it is not obvious how we should tweak the algorithm.
Let's check the basic description and template of backtracking algorithm:
Wikipedia: Backtracking is a general algorithm for finding solutions to some computational problems, notably constraint satisfaction problems, that incrementally builds candidates to the solutions, and abandons a candidate ("backtracks") as soon as it determines that the candidate cannot possibly be completed to a valid solution.
void backtrack(arguments) {
if (condition == true) { // Condition when we should stop our exploration.
result.push_back(current);
return;
}
for (int i = num; i <= last; i++) {
current.push_back(i); // Explore candidate.
backtrack(arguments);
current.pop_back(); // Abandon candidate.
}
}One thing to remember before we can jump to some backtracking problems:
From now let's start to apply this algorithm to solve some backtracking problems.
void backtrack(vector& nums, vector>& res,
vector<int>& cur, unordered_set<int>& used) {
if (cur.size() == nums.size()) { // (1)
res.push_back(cur);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (!used.count(nums[i])) { // (2)
cur.push_back(nums[i]);
used.insert(nums[i]);
backtrack(nums, res, cur, used);
cur.pop_back(); // (3)
used.erase(nums[i]);
}
}
}
// Or we can implement backtrack() without using unordered_set<>.
void backtrack2(vector<int>& nums, int ind,
vector<vector<int>>& res) { // (1)
if (ind == nums.size()) {
res.push_back(nums);
return;
}
for (int i = ind; i < nums.size(); i++) { // (2)
swap(nums[i], nums[ind]);
backtrack(nums, ind + 1, res);
swap(nums[i], nums[ind]); // (3)
}
}You might notice that we change the original template for a bit:
Another variation of the problem is Permutations II.
The only difference between first problem is that we MAY have DUPLICATES in the input array.
void backtrack(vector& nums, vector& cur, vector>& res,
unordered_map& hmap) {
if (cur.size() == nums.size()) { // (1)
res.push_back(cur);
return;
}
for (auto& [num, freq] : hmap) { // (2)
if (freq == 0) continue; // (3)
freq--;
cur.push_back(num);
dfs(nums, cur, res, hmap);
cur.pop_back(); // (4)
freq++;
}
}
// Iterate over the original list, but check if the previous element is the same as current.
// We need to make this check because using the same element will give us the same result as last iteration.
void backtrack2(vector<int>& nums, vector<int>& temp,
vector<vector<int>>& res, unordered_map<int, int>& freq) {
if (temp.size() == nums.size()) {
res.push_back(temp);
return;
}
for (int i = 0; i < nums.size(); i++) {
if (freq[nums[i]] == 0 || (i != 0 && nums[i] == nums[i - 1])) continue;
temp.push_back(nums[i]);
freq[nums[i]]--;
backtrack(nums, temp, res, freq);
freq[nums[i]]++;
temp.pop_back();
}
}Let's check this algorithm and compare to the first problem:
void backtrack(int num, int last, int k, vector& cur,
vector<vector<int>>& res) {
if (cur.size() == k) { // (1)
res.push_back(cur);
return;
}
for (int i = num; i <= last; i++) { // (2)
cur.push_back(i);
backtrack(i + 1, last, k, cur, res);
cur.pop_back();
}
}
// Or we can allocate temp vector in advance and fill the position.
void backtrack2(int ind, int prev, int k, int n, vector<int>& temp,
vector<vector<int>>& res) {
if (ind >= k) {
res.push_back(temp);
return;
}
for (int p = prev + 1; p <= n; p++) {
int saved = temp[ind]; // Given the way how we fill temp array - this is not necessary.
temp[ind] = p;
backtrack2(ind + 1, p, k, n, temp, res);
temp[ind] = saved; // Given the way how we fill temp array - this is not necessary.
}
}Let's check what is the difference compared to earlier examples:
Let's next check another example: Combination Sum II. This time instead of using hash table we will sort th elements before invoking dfs() for the first time.
We are given a collection of candidates and a target number, find ALL UNIQUE combinations in candidates where the candidate numbers sum to target.
void dfs(int ind, vector& cur, vector>& res,
vector& cnd, int target) {
if (target == 0) { // (1)
res.push_back(cur);
return;
}
for (int i = ind; i < cnd.size(); i++) { // (2)
if (i > ind && cnd[i] == cnd[i - 1]) continue; // (3)
if (target - cnd[i] >= 0) {
cur.push_back(cnd[i]);
dfs(i + 1, cur, res, cnd, target - cnd[i]);
cur.pop_back(); // (4)
}
}
}Let's check for is the difference between others examples:
i > ind is needed because we are only allowed to use each number in candidates once. Because of the > and not >= we are allowing ourselves to use the element even if it is the same as previous.void dfs(int ind, vector& nums, vector& cur, vector>& res) {
res.push_back(cur); // (1)
for (int i = ind; i < nums.size(); i++) { // (2)
cur.push_back(nums[i]);
dfs(i + 1, nums, cur, res);
cur.pop_back(); // (3)
}
}Let's check the steps again:
void dfs(int ind, vector& cur, vector>& res,
vector& nums) {
res.push_back(cur);
for (int i = ind; i < nums.size(); i++) {
if (i > ind && nums[i] == nums[i - 1]) continue;
cur.push_back(nums[i]);
dfs(i + 1, cur, res, nums);
cur.pop_back();
}
}For the last problem in this article let's check more complex / interesting problem that can be solved using backtracking: Word Squares. We are given an array of unique strings words, we need to return all the word squares you can build from words. Note: result word square should be symmetrical across its diagonal.
vector> wordSquares(vector& words) {
vector<vector<string>> res;
vector<string> cur;
unordered_map> hmap;
for (auto& w : words) {
for (int i = 0; i < w.size(); i++) hmap[w.substr(0, i)].push_back(w); // (1)
}
dfs(0, words[0].size(), res, cur, hmap);
return res;
}
void dfs(int ind, int sz, vector>& res, vector& cur,
unordered_map>& hmap) {
if (ind == sz) { // (2)
res.push_back(cur);
return;
}
string pref;
for (int i = 0; i < ind; i++) pref += cur[i][ind]; // (3)
for (auto& w : hmap[pref]) { // (4)
cur.push_back(w);
dfs(ind + 1, sz, res, cur, hmap);
cur.pop_back(); // (5)
}
}The dfs() algorithm here is almost the same, but let's check the key points:
Links to the problems above + some more problems to practice: