[算法專題] 深度優先搜索&回溯剪枝


1. Palindrome Partitioning

https://leetcode.com/problems/palindrome-partitioning/

Given a string s, partition s such that every substring of the partition is a palindrome.

Return all possible palindrome partitioning of s.

For example, given s = "aab",
Return

  [
    ["aa","b"],
    ["a","a","b"]
  ]
/**
 * author : Jianxin Zhou
 * email:zhoujx0219@163.com
 * 
 * 該題dfs函數原型如下:
 * void partitionHelper(const string &s, vector<vector<string>> &result, vector<string> &path, int pos)
 * 
 * 以aaba舉例。
 * 1. 首先a為回文,然后對aba進行dfs
 * 2. 之后回溯到a時,以aa為回文,然后對ba做dfs
 * 3. 回溯到aa,試圖以aab為回文,失敗;試圖以aaba為回文失敗;結束。
 * 
 * 注意:如果能順利的找到一組回文,那么pos最終會等於s.size(),此時可以push到result。
 *       如果找不到,例如之前的aaba不是回文,那么就會直接退出循環,沒有機會執行下一步遞歸,也就沒有pos等於s.size了。
 * 
 * 實際上,此類題與真正的dfs的差別在於,dfs在回溯時,不會進行剪枝操作。而此類題,由於需要求出所有方案,所以需要剪枝。
 *
 */


class Solution {
public:
    vector<vector<string>> partition(string s) {
        vector<vector<string>> result;
        vector<string> path;
        partitionHelper(s, result, path, 0);
        return result;
    }
    
private:
    void partitionHelper(const string &s, vector<vector<string>> &result, vector<string> &path, int pos) {
        // base case
        if (pos == s.size()) {
            result.push_back(path);
            return;
        }
        
        for (int i = pos; i < s.size(); i++) {
            if (isPalindrome(s, pos, i)) {
                path.push_back(s.substr(pos, i - pos + 1));
                partitionHelper(s, result, path, i + 1);
                path.pop_back();
            }
        }
    }
    
    bool isPalindrome(const string &s, int start, int end) {
        while (start < end) {
            if (s[start] == s[end]) {
                start++;
                end--;
            } else {
                break;
            }
        }
        
        return start >= end;
    }
};

2. Permutations

https://leetcode.com/problems/permutations/

Given a collection of numbers, return all possible permutations.

For example,
[1,2,3] have the following permutations:
[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], and [3,2,1].

具體可參加我之前寫的文章:[LintCode] Permutations

/**
 * 思路:dfs。
 * 
 * 以123舉例,
 * 1. 首先以1作為head,然后對23做dfs
 * 2. 回溯到1, 以2作為head,對13做dfs
 * 3. 最后回溯到2,以3作為head,對12做dfs
 * 
 * 注意:例如以2為head,對其余元素做dfs時,那么2不能再取,因此在進行下一輪dfs時,需要標記2為以訪問過
 * 
 */


class Solution {
public:
    vector<vector<int>> permute(vector<int>& nums) {
        vector<vector<int>> result;
        vector<int> path;
        
        bool visited[nums.size()];
        for(int i = 0; i < nums.size(); i++) {
            visited[i] = false;
        }
        
        sort(nums.begin(), nums.end());
        dfs(nums, result, path, visited);
        return result;
    }
    
private:
    void dfs(const vector<int> &nums, vector<vector<int>> &result, vector<int> &path, bool visited[]) {
        // base case
        if (path.size() == nums.size()) {
            result.push_back(path);
            return;
        }

        for (int i = 0; i < nums.size(); i++) {
            if (visited[i] == false) {
                path.push_back(nums[i]);
                visited[i] = true;
                dfs(nums, result, path, visited);
                path.pop_back();
                visited[i] = false;
            }
            
        }
    }
};

3. Permutations II

https://leetcode.com/problems/permutations-ii/

Given a collection of numbers that might contain duplicates, return all possible unique permutations.

For example,
[1,1,2] have the following unique permutations:
[1,1,2], [1,2,1], and [2,1,1].

要點在於保證相同的數不在同一位置出現兩次以上,可以參見我寫的這篇文章:[LintCode] Permutations II

class Solution {
public:
    /**
     * @param nums: A list of integers.
     * @return: A list of unique permutations.
     */
    vector<vector<int> > permuteUnique(vector<int> &nums) {
        // write your code here
        vector<vector<int>> paths;
        if (nums.empty()) {
            return paths;
        }
         
        sort(nums.begin(), nums.end());
        bool *visited = new bool[nums.size()]();
        vector<int> path;
        permuteUniqueHelper(nums, visited, path, paths);
        return paths;
    }
     
private:
    void permuteUniqueHelper(const vector<int> &nums,
                             bool visited[],
                             vector<int> &path,
                             vector<vector<int>> &paths) {
        if (path.size() == nums.size()) {
            paths.push_back(path);
            return;
        } 
         
        for (int ix = 0; ix < nums.size(); ix++) {
            if (visited[ix] == true || ix > 0 && nums[ix - 1] == nums[ix] && visited[ix - 1] == false) {
                continue;    
            }
             
            visited[ix] = true;
            path.push_back(nums[ix]);
            permuteUniqueHelper(nums, visited, path, paths);
            visited[ix] = false;
            path.pop_back();
        }
    }
};

4 Subsets

https://leetcode.com/problems/subsets/

Given a set of distinct integers, nums, return all possible subsets.

Note:

  • Elements in a subset must be in non-descending order.
  • The solution set must not contain duplicate subsets.

For example,
If nums = [1,2,3], a solution is:

[
  [3],
  [1],
  [2],
  [1,2,3],
  [1,3],
  [2,3],
  [1,2],
  []
]
/**
 * 思路:找方案,一般都是使用搜索。
 * 
 * 以123為例,在遞歸還沒有開始前,先把空集push到result中,之后:
 * 1. 以1位head,對23做dfs,所以pos需要加1,用於分支限界(1 12 13 123)
 * 2. 回溯到1,以2為head,對3做dfs (2 23)
 * 3. 回溯到3,以3為head,之后循環結束。 (3)
 * 
 * 
 */



class Solution {
public:
    vector<vector<int>> subsets(vector<int>& nums) {
        // ensure that elements in a subset must be in non-descending order.
        sort(nums.begin(), nums.end());
        
        vector<vector<int>> res;
        vector<int> path;
        dfs(nums, res, path, 0);
        return res;
    }
    
private:
    void dfs(const vector<int> &nums, vector<vector<int>> &res, vector<int> &path, int pos) {
        res.push_back(path);
        
        for (int i = pos; i < nums.size(); i++) {
            path.push_back(nums[i]);
            dfs(nums, res, path, i + 1);
            path.pop_back();
        }
    }
};

5. Subsets II

https://leetcode.com/problems/subsets-ii/

Given a collection of integers that might contain duplicates, nums, return all possible subsets.

Note:

  • Elements in a subset must be in non-descending order.
  • The solution set must not contain duplicate subsets.

For example,
If nums = [1,2,2], a solution is:

[
  [2],
  [1],
  [1,2,2],
  [2,2],
  [1,2],
  []
]

同一位置上,前面取過的數,后面就不要再重復取了,當然當i = pos時,這個數必然是第一次取。

class Solution {
public:
    vector<vector<int>> subsetsWithDup(vector<int> &nums) {
        sort(nums.begin(), nums.end());
        vector<vector<int>> res;
        vector<int> path;
        dfs(nums, res, path, 0);
        return res;
    }
    
private:
    void dfs(const vector<int> &nums, vector<vector<int>> &res, vector<int> &path, int pos) {
        res.push_back(path);
        
        for (int i = pos; i < nums.size(); i++) {
            if (i != pos && nums[i] == nums[i - 1]) {
                continue;
            }
            
            path.push_back(nums[i]);
            dfs(nums, res, path, i + 1);
            path.pop_back();
        }
    }
};

6 Restore IP Addresses

https://leetcode.com/problems/restore-ip-addresses/

Given a string containing only digits, restore it by returning all possible valid IP address combinations.

For example:
Given "25525511135",

return ["255.255.11.135", "255.255.111.35"]. (Order does not matter)

/**
 * 該題思路與求回文划分相似
 */

class Solution {
public:
    vector<string> restoreIpAddresses(string s) {
        vector<string> res;
        
        size_t len = s.size();
        if (len < 4 || len > 12) {
            return res;
        }
        

        vector<string> path;
        dfs(s, res, path, 0);
        return res;
    }
    
private:
    void dfs(const string &s, vector<string> &res, vector<string> &path, int pos) {
        // base case
        if (path.size() == 4) {
            if (pos != s.size()) {
                return;
            }
            
            string returnElem;
            for (const auto &elem : path) {
                returnElem += elem;
                returnElem += ".";
            }
            returnElem.erase(returnElem.end() - 1);
            
            res.push_back(returnElem);
            return;
        }
        
        for (int i = pos; i < s.size() && i < pos + 3; i++) {
            string tmp = s.substr(pos, i - pos + 1);
            if (isValid(tmp)) {
                path.push_back(tmp);
                dfs(s, res, path, i + 1);
                path.pop_back();
            }
        }
    }
    
    bool isValid(const string &s) {
        // 排除 055 之類的數字
        if (s[0] == '0' && s.size() > 1) {
            return false;
        }
        
        int digit = atoi(s.c_str());
        return 0 <= digit && digit <= 255;
    }
};

7 N-Queens

http://www.lintcode.com/en/problem/n-queens/#

The n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other.

image

Given an integer n, return all distinct solutions to the n-queens puzzle.

Each solution contains a distinct board configuration of the n-queens' placement, where 'Q' and '.' both indicate a queen and an empty space respectively.

For example,
There exist two distinct solutions to the 4-queens puzzle:

[
 [".Q..",  // Solution 1
  "...Q",
  "Q...",
  "..Q."],

 ["..Q.",  // Solution 2
  "Q...",
  "...Q",
  ".Q.."]
]
/**
 * 思路:一行一行的取數,例如第一行的皇后放在第1個位置,第二行的皇后放在第3個位置,
 * 以此類推,直到最后一行的皇后放在正確的位置,如此視為一個方案,push到result中
 * 
 * 顯然,本題使用dfs,每一行可取的位置從0-N-1,
 * 需要注意的是,每一行在取位置的時候,需要判斷有效性(是否可以相互攻擊)。
 */



class Solution {
public:
    /**
     * Get all distinct N-Queen solutions
     * @param n: The number of queens
     * @return: All distinct solutions
     * For example, A string '...Q' shows a queen on forth position
     */
    vector<vector<string> > solveNQueens(int n) {
        vector<vector<string>> res;
        vector<int> visitedCol;
        
        if (n <= 0) {
            return res;
        }
        
        dfs(n, res, visitedCol);
        return res; 
    }
    
private:
    void dfs(const int n, vector<vector<string>> &res, vector<int> &visitedCol) {
        // base case
        if (visitedCol.size() == n) {
            res.push_back(draw(visitedCol));
            return;
        }
        
        for (int i = 0; i < n; i++) {
            if (!isValid(visitedCol, i)) {
                continue;
            }
            
            visitedCol.push_back(i);
            dfs(n, res, visitedCol);
            visitedCol.pop_back();
        }
        
        
    }
    
    bool isValid(const vector<int> &visitedCol, const int currentCol) {
        size_t currentRow = visitedCol.size();
        
        for (int rowIndex = 0; rowIndex < visitedCol.size(); rowIndex++) {
            if (currentCol == visitedCol[rowIndex]) {
                return false;
            }
            
            if (currentRow + currentCol == rowIndex + visitedCol[rowIndex]) {
                return false;
            }
            
            if (currentRow - currentCol == rowIndex - visitedCol[rowIndex]) {
                return false;
            }
        }
        
        return true;
    }
    
    vector<string> draw(const vector<int> &visitedCol) {
        
        vector<string> ret;
        string row;
        for (const auto &elem : visitedCol) {
            row.clear();
            
            for (int i = 0; i < visitedCol.size(); i++) {
                if (i == elem) {
                    row += "Q";
                } else {
                    row += ".";
                }
            }
            
            ret.push_back(row);
        }
        
        return ret;
    }
};

8 Sudoku Solver

https://leetcode.com/problems/sudoku-solver/

Write a program to solve a Sudoku puzzle by filling the empty cells.

Empty cells are indicated by the character '.'.

You may assume that there will be only one unique solution.

A sudoku puzzle...

...and its solution numbers marked in red.

class Solution {
public:
    void solveSudoku(vector<vector<char>>& board) {
        dfs (board, 0, 0);
    }
    
private:
    /**
     * 該題需要對sudoku中每一個以‘.’標記的方格進行dfs,
     * 1. 如果對當前方格的以1-9這9個數字進行遍歷,都不合法,那么不會再往下一個方格進行dfs,直接回溯到上一個方格取下一個數。
     * 2. 如果當前方格所取的數合法,那么繼續對下一個方格進行dfs,依次下去如果一直合法,那么直到走到sudoku中的最后一個需要放數字的方格,
     *    嘗試完它的所有選擇,再往上回溯。
     * 然后,在這邊我們只需要一個可行解即可,因此只要當前方格合法,往下的dfs返回true,那么即為一個解,直接返回。
     * 
     * 
     * 
     */
    bool dfs(vector<vector<char>> &board, int x, int y) {

        for (int i = 0; i < 9; i++) {
            for (int j = 0; j < 9; j++) {
                //dfs
                if (board[i][j] == '.') {
                    // k從0-9走完才算走完,但是此處我們只要有一個解,就可以返回了,因此在以下循環中設置了return語句
                    for (int k = 0; k < 9; k++) {
                        bool flag;
                        
                        if (!isValid(board, i ,j, k)) {
                            continue;
                        }
                        
                        board[i][j] = '1' + k;
                        
                        if (j != 8) {
                            flag = dfs(board, i, j + 1);
                        } else {
                            flag = dfs(board, i + 1, 0);
                        }
                        
                        // 當前合法&&下一輪dfs合法,說明找到解
                        if (flag) {
                            return true;
                        }
                        
                        board[i][j] = '.';
                    }
                    
                    // 遍歷完9個數,仍然找不到合適的解,則返回false
                    return false;
                }
            }
        }
        
        // 當所有各自都走完,自然返回true(注意只有當前合法,才會繼續往下走,繼續往下走的最終結果是越了sudoku的界限)
        return true;
        
    }
    
    bool isValid(const vector<vector<char>> &board, int x, int y, int k) {
        int i, j;
        for (i = 0; i < 9; i++) // 檢查 y 列
            if (i != x && board[i][y] == '1' + k)
                return false;
        
        for (j = 0; j < 9; j++) // 檢查 x 行
            if (j != y && board[x][j] == '1' + k)
                 return false;

        for (i = 3 * (x / 3); i < 3 * (x / 3 + 1); i++)
            for (j = 3 * (y / 3); j < 3 * (y / 3 + 1); j++)
                if ((i != x || j != y) && board[i][j] == '1' + k)
                    return false;

        return true;
    }
};
 

小結1

做搜索的題目,最關鍵的是要知道對什么對象進行dfs,例如,在sudoku中是對每一個以“.”標記的方格進行dfs,在回文划分中,是對每一個划分的位置進行dfs,在8妃問題中,是對每一行妃子可以在的位置進行dfs。

其次,dfs時,我們需要判斷所取的每一個解是否是有效的,最好寫一個函數來專門做這件事情。只要當當前對象dfs的數值有效時,才會繼續往對下一個對象進行dfs,否則就直接向上回溯了(這點可以參見sudoku中的解釋)。

最后,對於每次dfs時,可以對范圍進行分支限界。例如回文划分、subset等。

小結2

值得注意的是:到底要對多少對象進行dfs,有時候是很明顯的,例如8妃和sudoku問題,8妃就是對8行依次dfs,sudoku就是對所有方格進行dfs。但有時,總共要對多少對象進行dfs並不明顯。dfs的遞歸基要處理的就是dfs完多少個對象就一定要返回(不然就無限dfs下去了)。當然,在sudoku問題中,方格的循環走完返回,這是一個隱含的遞歸基。

總結:dfs函數中,遞歸基處理的是dfs多少個對象就要返回。而每次dfs的for循環,往往是每一次dfs的范圍。當遞歸棧最頂層的那個dfs循環走完,搜素就完成了。

小結3

在圖論中,往往是從某一個點開始往下dfs,dfs的范圍是當前node的所有neighbor,與我們通常的搜索問題不同的是,圖論中的dfs在回溯時不會剪枝,總之,找到一條路徑就結束了。

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM