[LeetCode] “全排列”問題系列(一) - 用交換元素法生成全排列及其應用,例題: Permutations I 和 II, N-Queens I 和 II,數獨問題


一、開篇

Permutation,排列問題。這篇博文以幾道LeetCode的題目和引用劍指offer上的一道例題入手,小談一下這種類型題目的解法。

二、上手

最典型的permutation題目是這樣的:

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].

class Solution {
public:
    vector<vector<int> > permute(vector<int> &num) {
    }
};

我第一次接觸這類問題是在劍指offer里,見筆記 面試題 28(*),字符串的排列(排列問題的典型解法:采用遞歸,每次交換首元素和剩下元素中某一個的位置) 。

書中對這種問題采用的方法是“交換元素”,這種方法的好處是不需要再新開一個數組存臨時解,從而節省一部分輔助空間。

 交換法的思路是for(i = start to end),循環中: swap (第start個和第i個),遞歸調用(start+1),swap back

根據這個思路,可以輕易寫出這道題的代碼:

class Solution {
public:
    vector<vector<int> > permute(vector<int> &num) {
        if(num.size() == 0) return res;
        permuteCore(num, 0);
        return res;
    }
private:
    vector<vector<int> > res;
    void permuteCore(vector<int> &num, int start){
        if(start == num.size()){
            vector<int> v;
            for(vector<int>::iterator i = num.begin(); i < num.end(); ++i){
                v.push_back(*i);
            }
            res.push_back(v);
        }
        for(int i = start; i < num.size(); ++i){
            swap(num, start, i);
            permuteCore(num, start+1);
            swap(num, start, i);
        }
    }
    void swap(vector<int> &num, int left, int right){
        int tmp = num[left];
        num[left] = num[right];
        num[right] = tmp;
    }
};

 

permutation II 是在上一題的基礎上,增加了“數組元素可能重復”的條件。

這樣,如果用交換法來解,需要定義一個set來存儲已經交換過的元素值。

class Solution {
public:
    vector<vector<int> > permuteUnique(vector<int> &num) {
        if(num.size() <= 0) return res;
        permCore(num, 0);
        return res;
    }
private:
    vector<vector<int> > res;
    void permCore(vector<int> &num, int st){
        if(st == num.size()) res.push_back(num);
        else{
            set<int> swp;
            for(int i = st; i < num.size(); ++i){
                if(swp.find(num[i]) != swp.end()) continue;
                swp.insert(num[i]);
                swap(num, st, i);
                permCore(num, st+1);
                swap(num, st, i);
            }
        }
    }
    
    void swap(vector<int> &num, int left, int right){
        int tmp = num[left];
        num[left] = num[right];
        num[right] = tmp;
    }
};

 

 

題外話:交換法只是解法的一種,其實我們還可以借鑒Next permuation的思路(見這個系列的第二篇)來解這一道題,從而省去了使用遞歸。

使用Next permutation的思路來解 Permutation II

class Solution {
public:
    vector<vector<int> > permuteUnique(vector<int> &num) {
        if(num.size() <= 0) return res;
        sort(num.begin(), num.end());
        res.push_back(num);
        int i = 0, j = 0;
        while(1){
            //Calculate next permutation
            for(i = num.size()-2; i >= 0 && num[i] >= num[i+1]; --i);
            if(i < 0) break;
            for(j = num.size()-1; j > i && num[j] <= num[i]; --j);
            swap(num, i, j);
            j = num.size()-1;
            ++i;
            while(i < j)
                swap(num, i++, j--);
            //push next permutation
            res.push_back(num);
        }
        return res;
    }
private:
    vector<vector<int> > res;
    void swap(vector<int> &num, int left, int right){
        int tmp = num[left];
        num[left] = num[right];
        num[right] = tmp;
    }
};

 

 

三、應用

Permutation類問題一個典型的應用就是N皇后問題,以LeetCode上的n-queens題和 n-queens II 為例:

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.

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.."]
]
class Solution {
public:
    vector<vector<string> > solveNQueens(int n) {
        
    }
};

 

上面是題 n-queens 的內容,題 n-queens II 其實反而更容易,它要求不變,只是不需要返回所有解,只要返回解的個數。

有了上面的思路,如果用A[i] = j 表示第i 行的皇后放在第j列上,N-queen也是一個全排列問題,只是排列時需要加上一個額外判斷,就是兩個皇后是否在一條斜線上。

真正實現的時候我犯了一個錯誤。

如上所說,交換法的思路是for(i = start to end),循環中: switch(第start個和第i個),遞歸調用(start+1),switch back

我錯誤的認為N皇后不需要switch back,其實 switch back是必須要做的步驟,因為這種解法的本質是還是深搜,子遞歸會層層調用下去,不及時swtich back的話,當前層的下一次遞歸調用會把重復的值switch過來,從而出現重復,結果是漏掉了一些正確的排列方法。因此,使用交換法解全排列問題時,不可打亂遞歸調用時的排列。

題N-Queens被AC的代碼:

class Solution {
public:
    vector<vector<string> > solveNQueens(int n) {
        if(n <= 0) return res;
        int* A = new int[n];
        for(int i = 0; i < n; ++i) A[i] = i;
        nqueensCore(A, 0, n);
        return res;
    }
private:
    vector<vector<string> > res;
    void nqueensCore(int A[], int start, int n){
        if((start+1) == n && judgeAttackDiag(A, start))
            output(A, n);
        else{
            for(int i = start; i < n; ++i){
                swtich(A, start, i);
                if(judgeAttackDiag(A, start))
                    nqueensCore(A, start+1, n);
                swtich(A, start, i);
            }
        }
    }
    
    void swtich(int A[], int left, int right){
        int temp = A[left];
        A[left] = A[right];
        A[right] = temp;
    }
    
    bool judgeAttackDiag(int A[], int newPlace){    //everytime a new place is configured out, judge if it can be attacked by the existing queens
        if(newPlace <= 0) return true;
        bool canAttack = false;
        for(int i = 0; i < newPlace; ++i){
            if((newPlace - i) == (A[newPlace] - A[i]) || (i - newPlace) == (A[newPlace] - A[i])) canAttack = true;
        }
        return !canAttack;
    }
    
    void output(int A[], int n){
        vector<string> v;
        for(int i = 0; i < n; ++i){
            string row(n,'.');
            v.push_back(row);
        }
        for(int j = 0; j < n; ++j){
            v[A[j]][j] = 'Q';  
        }
        res.push_back(v);
    }
};

 

N-Queens II

Follow up for N-Queens problem.

Now, instead outputting board configurations, return the total number of distinct solutions.

class Solution {
public:
    int totalNQueens(int n) {
    }
};

 

基本思路依然是使用全排列,這次代碼可以寫得簡潔一些。

class Solution {
public:
    int totalNQueens(int n) {
        if(n <= 1) return n;
        res = 0;
        queens = new int[n];
        for(int i = 0; i < n; queens[i] = i, ++i);
        nQueensCore(queens, n, 0);
        return res;
    }
private:
    int res;
    int* queens;
    void nQueensCore(int* queens, int n, int st){
        if(st == n) ++res;
        int tmp, i, j;
        for(i = st; i < n; ++i){
            tmp = queens[st];
            queens[st] = queens[i];
            queens[i] = tmp;
            
            for(j = 0; j < st; ++j){
                if(abs(queens[st] - queens[j]) == abs(st - j)) break;
            }
            if(j == st) nQueensCore(queens, n, st+1);
            
            tmp = queens[st]; queens[st] = queens[i]; queens[i] = tmp;
        }
    }
};

我第一次提交時依然犯了忘掉switch back的錯誤,第一次提交的代碼中,寫的是“if(abs(queens[st] - queens[j]) == abs(st - j)) return;"

這樣就導致了switch back部分代碼(高亮部分)不會被執行,從而打亂了整個順序。

 

3. 數獨問題

數獨和N 皇后一樣,都是需要不停地計算當前位置上所擺放的數字是否滿足條件,不滿足就回溯,擺放另一個數字,基於這個新數字再計算。

選擇新數字的過程,就是全排列的過程。

以LeetCode上的例題為例:

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.

void solveSudoku(vector<vector<char> > &board) {}

 

關於數獨的規則,請參見這里:Sudoku Puzzles - The Rules. 必須保證每行,每列,和9個3X3方塊中1-9各自都只出現一次。

我們依然可以用交換法來解,思路依然是:

  for(i = start to end),循環中: swap (第start個和第i個);如果當前排列正確,遞歸調用(start+1);swap back

這里需要額外考慮的是:數獨陣列中有一些固有數字,這些數字是一開始就不能動。因此,我用flag[][]來標記一個位置上的數字是否可替換。flag[i][j] == true表示Board[i][j]上的數字可替換,false表示不可替換。因此思路稍加變更,成了:

Func(start){

a. 如果 flag上start對應的位置 == false,說明當前位不能改動,因此只需判斷當前排列是否正確,正確則遞歸調用(start+1)

b. flag上start對應的位置 = false

c. for(i = start 到當前行末尾),循環中: swap (第start個和第i個);如果當前排列正確,遞歸調用(start+1);swap back

d. flag上start對應的位置 = true

}

代碼: 

class Solution {
public:
    void solveSudoku(vector<vector<char> > &board) {
        flag = new bool*[10];    //flag[i][j] == false means value on board[i][j] is decided or originally given.
        digits = new bool[10];  //digits is used to check whether one digit (1-9) is duplicated in sub 3*3 square
        int i = 0, j = 0;
        for(; i < 9; ++i){
            flag[i] = new bool[9];
            for(j = 0; j < 9; ++j){
                if(board[i][j] == '.') flag[i][j] = true;
                else flag[i][j] = false;
            }
        }
        initialBoard(board, 9); //初始化Board,先把所有的空缺填滿,填的時候先保證每一行沒有重復數字。
        solveSudokuCore(board, 0);
    }
private:
    bool **flag;
    bool *digits;
    void initialBoard(vector<vector<char> > &board, int N){
        int i, j, k;
        bool *op = new bool[N+1];
        for(i = 0; i < N; ++i){
            for(j = 0; j <= N; ++j) op[j] = false;
            for(j = 0; j < N; ++j){
                if(board[i][j] != '.') op[board[i][j] - '0'] = true;
            }
            for(j = 0, k = 1; j < N; ++j){
                if(board[i][j] == '.'){
                    while(op[k++]);
                    board[i][j] = ((k-1) + '0');
                }
            }
        }
        delete op;
    }
    
    bool check(vector<vector<char> > &board, int index){
        int col = index%9, row = index/9;
        int i = 0;
        for(i = 0; i < 9; ++i){
            if(i != row && !flag[i][col] && board[i][col] == board[row][col])
                return false;
        }
        
        if((col+1)%3 == 0 && (row+1)%3 == 0){
            for(i = 0; i < 10; ++i) digits[i] = false;
            for(int j = (row/3)*3; j < (row/3+1)*3; ++j){
                for(int k = (col/3)*3; k < (col/3+1)*3; ++k){
                    if(digits[board[j][k] - '0']) return false;
                    digits[board[j][k] - '0'] = true;
                }
            }
        }
        return true;
    }
    
    bool solveSudokuCore(vector<vector<char> > &board, int index){
        if(index == 81) return true;
        if(!flag[index/9][index%9]){ //如果當前位置是不可更改的,那么只要check一下是否正確就可以了
            if(check(board, index) && solveSudokuCore(board, index+1))
                return true;
        }else{ //如果當前位置是可更改的,那么需要通過交換不停替換當前位,看哪一個數字放在當前位上是正確的。
            flag[index/9][index%9] = false;
            for(int i = index; i < (index/9+1)*9; ++i){
                if(flag[i/9][i%9] || i == index){
                    int tmp = board[i/9][i%9];
                    board[i/9][i%9] = board[index/9][index%9];
                    board[index/9][index%9] = tmp;

                    if(check(board, index) && solveSudokuCore(board, index+1))
                        return true; //如果當前位上放這個數字正確,那么繼續計算下一位上該放哪個數字。
                    
                    tmp = board[i/9][i%9];
                    board[i/9][i%9] = board[index/9][index%9];
                    board[index/9][index%9] = tmp;
                }
            }
            flag[index/9][index%9] = true;
        }
        return false;
    }
};

 

 

四、引申

給定一個包含重復元素的序列,生成其全排列

如果要生成全排列的序列中包含重復元素,該如何做呢?以LeetCode上的題 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].

class Solution {
public:
    vector<vector<int> > permuteUnique(vector<int> &num) {
    }
};

 思路:

比如[1, 1, 2, 2],我們交換過一次 位置1上的"1"和 位置3上的"2",就不再需要交換 位置1上的"1" 和 位置4上的"2"了。

因此,在傳統的交換法的基礎上,需要加一個過濾:比如當前我們 需要挨個將位置 2-4的元素和位置1上的"1" 交換,此時,如果2-4上的元素有重復值,我們只需要用第一次出現的那個值和位置1做交換即可。

我開始的思路是:先將位置2-4的元素sort一下,然后定義pre存放上次交換的元素的值,如果當前值和pre不同,則交換當前值和位置1上的值。

按照這種方式實現的代碼是:

class Solution {
public:
    vector<vector<int> > permuteUnique(vector<int> &num) {
        if(num.size() == 0) return res;
        permuteCore(num, 0);
        return res;
    }
private:
    vector<vector<int> > res;
    void permuteCore(vector<int> &num, int start){
        if(start == num.size()){
            vector<int> v;
            for(vector<int>::iterator i = num.begin(); i < num.end(); ++i){
                v.push_back(*i);
            }
            res.push_back(v);
        }
        sort(num.begin()+start, num.end()); int pre; for(int i = start; i < num.size(); ++i){ if(i == start || pre != num[i]){ swap(num, start, i); permuteCore(num, start+1); swap(num, start, i); pre = num[i]; } }
    }
    void swap(vector<int> &num, int left, int right){
        int tmp = num[left];
        num[left] = num[right];
        num[right] = tmp;
    }
};

 然而判定結果是 Output Limit Exceeded,分析了一下原因,在於Sort破壞了當前子排列,導致出現了重復解。正如我上一節中所說,使用交換法解全排列問題時,不可打亂遞歸調用時的排列,不然可能導致重復解。

不用sort來做判斷的話,那就使用set 來去重吧。將上面代碼的高亮部分換成下面代碼的高亮部分,這次就AC了。

class Solution {
public:
    vector<vector<int> > permuteUnique(vector<int> &num) {
        if(num.size() == 0) return res;
        permuteCore(num, 0);
        return res;
    }
private:
    vector<vector<int> > res;
    
    void permuteCore(vector<int> &num, int start){
        if(start == num.size()){
            vector<int> v;
            for(vector<int>::iterator i = num.begin(); i < num.end(); ++i){
                v.push_back(*i);
            }
            res.push_back(v);
        }
        set<int> used; for(int i = start; i < num.size(); ++i){ if(used.find(num[i]) == used.end()){ swap(num, start, i); permuteCore(num, start+1); swap(num, start, i); used.insert(num[i]); } }
    }
    void swap(vector<int> &num, int left, int right){
        int tmp = num[left];
        num[left] = num[right];
        num[right] = tmp;
    }
};

但這種解法的缺點在於比較費空間,set 需要定義在局部變量區,這樣才能保證遞歸函數不混用set。

 

五、總結:

對於全排列問題,交換法是一種比較基本的方法,其優點就在於不需要額外的空間

使用時需要注意

a. 不要打亂子問題的序列順序。

b. 記得換回來,回溯才能正確進行,也就是說,負責switch back部分的代碼必須被執行到。


免責聲明!

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



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