一、开篇

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() == ) return res;
permuteCore(num, );
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+);
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() <= ) return res;
permCore(num, );
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+);
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() <= ) return res;
sort(num.begin(), num.end());
res.push_back(num);
int i = , j = ;
while(){
//Calculate next permutation
for(i = num.size()-; i >= && num[i] >= num[i+]; --i);
if(i < ) break;
for(j = num.size()-; j > i && num[j] <= num[i]; --j);
swap(num, i, j);
j = num.size()-;
++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 <= ) return res;
int* A = new int[n];
for(int i = ; i < n; ++i) A[i] = i;
nqueensCore(A, , n);
return res;
}
private:
vector<vector<string> > res;
void nqueensCore(int A[], int start, int n){
if((start+) == 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+, 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 <= ) return true;
bool canAttack = false;
for(int i = ; 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 = ; i < n; ++i){
string row(n,'.');
v.push_back(row);
}
for(int j = ; 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 <= ) return n;
res = ;
queens = new int[n];
for(int i = ; i < n; queens[i] = i, ++i);
nQueensCore(queens, n, );
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 = ; j < st; ++j){
if(abs(queens[st] - queens[j]) == abs(st - j)) break;
}
if(j == st) nQueensCore(queens, n, st+); 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*[]; //flag[i][j] == false means value on board[i][j] is decided or originally given.
digits = new bool[]; //digits is used to check whether one digit (1-9) is duplicated in sub 3*3 square
int i = , j = ;
for(; i < ; ++i){
flag[i] = new bool[];
for(j = ; j < ; ++j){
if(board[i][j] == '.') flag[i][j] = true;
else flag[i][j] = false;
}
}
initialBoard(board, ); //初始化Board,先把所有的空缺填满,填的时候先保证每一行没有重复数字。
solveSudokuCore(board, );
}
private:
bool **flag;
bool *digits;
void initialBoard(vector<vector<char> > &board, int N){
int i, j, k;
bool *op = new bool[N+];
for(i = ; i < N; ++i){
for(j = ; j <= N; ++j) op[j] = false;
for(j = ; j < N; ++j){
if(board[i][j] != '.') op[board[i][j] - ''] = true;
}
for(j = , k = ; j < N; ++j){
if(board[i][j] == '.'){
while(op[k++]);
board[i][j] = ((k-) + '');
}
}
}
delete op;
} bool check(vector<vector<char> > &board, int index){
int col = index%, row = index/;
int i = ;
for(i = ; i < ; ++i){
if(i != row && !flag[i][col] && board[i][col] == board[row][col])
return false;
} if((col+)% == && (row+)% == ){
for(i = ; i < ; ++i) digits[i] = false;
for(int j = (row/)*; j < (row/+)*; ++j){
for(int k = (col/)*; k < (col/+)*; ++k){
if(digits[board[j][k] - '']) return false;
digits[board[j][k] - ''] = true;
}
}
}
return true;
} bool solveSudokuCore(vector<vector<char> > &board, int index){
if(index == ) return true;
if(!flag[index/][index%]){ //如果当前位置是不可更改的,那么只要check一下是否正确就可以了
if(check(board, index) && solveSudokuCore(board, index+))
return true;
}else{ //如果当前位置是可更改的,那么需要通过交换不停替换当前位,看哪一个数字放在当前位上是正确的。
flag[index/][index%] = false;
for(int i = index; i < (index/+)*; ++i){
if(flag[i/][i%] || i == index){
int tmp = board[i/][i%];
board[i/][i%] = board[index/][index%];
board[index/][index%] = tmp; if(check(board, index) && solveSudokuCore(board, index+))
return true; //如果当前位上放这个数字正确,那么继续计算下一位上该放哪个数字。 tmp = board[i/][i%];
board[i/][i%] = board[index/][index%];
board[index/][index%] = tmp;
}
}
flag[index/][index%] = 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() == ) return res;
permuteCore(num, );
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+);
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() == ) return res;
permuteCore(num, );
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+);
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部分的代码必须被执行到。

[LeetCode] “全排列”问题系列(一) - 用交换元素法生成全排列及其应用,例题: Permutations I 和 II, N-Queens I 和 II,数独问题的更多相关文章

  1. “全排列”问题系列(一)[LeetCode] - 用交换元素法生成全排列及其应用,例题: Permutations I 和 II, N-Queens I 和 II,数独问题

    转:http://www.cnblogs.com/felixfang/p/3705754.html 一.开篇 Permutation,排列问题.这篇博文以几道LeetCode的题目和引用剑指offer ...

  2. [LeetCode] “全排列”问题系列(二) - 基于全排列本身的问题,例题: Next Permutation , Permutation Sequence

    一.开篇 既上一篇<交换法生成全排列及其应用> 后,这里讲的是基于全排列 (Permutation)本身的一些问题,包括:求下一个全排列(Next Permutation):求指定位置的全 ...

  3. LeetCode 31:递归、回溯、八皇后、全排列一篇文章全讲清楚

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天我们讲的是LeetCode的31题,这是一道非常经典的问题,经常会在面试当中遇到.在今天的文章当中除了关于题目的分析和解答之外,我们还会 ...

  4. LeetCode47, 全排列进阶,如果有重复元素怎么办?

    本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是LeetCode第28篇,依然是全排列的问题. 如果对全排列不熟悉或者是最近关注的同学可以看一下上一篇文章: LeetCode46 回 ...

  5. C# 刷遍 Leetcode 面试题系列连载(3): No.728 - 自除数

    前文传送门: C#刷遍Leetcode面试题系列连载(1) - 入门与工具简介 C#刷遍Leetcode面试题系列连载(2): No.38 - 报数 系列教程索引 传送门:https://enjoy2 ...

  6. 图解Leetcode组合总和系列——回溯(剪枝优化)+动态规划

    Leetcode组合总和系列--回溯(剪枝优化)+动态规划 组合总和 I 给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 ...

  7. LeetCode:最少移动次数使得数组元素相等||【462】

    LeetCode:最少移动次数使得数组元素相等||[462] 题目描述 给定一个非空整数数组,找到使所有数组元素相等所需的最小移动数,其中每次移动可将选定的一个元素加1或减1. 您可以假设数组的长度最 ...

  8. C#刷遍Leetcode面试题系列连载(1) - 入门与工具简介

    目录 为什么要刷LeetCode 刷LeetCode有哪些好处? LeetCode vs 传统的 OJ LeetCode刷题时的心态建设 C#如何刷遍LeetCode 选项1: VS本地Debug + ...

  9. C#刷遍Leetcode面试题系列连载(2): No.38 - 报数

    目录 前言 题目描述 相关话题 相似题目 解题思路: 运行结果: 代码要点: 参考资料: 文末彩蛋 前言 前文传送门: C# 刷遍 Leetcode 面试题系列连载(1) - 入门与工具简介 上篇文章 ...

随机推荐

  1. Android:有关菜单的学习(供自己参考)

    Android:有关==菜单==的学习 上下文菜单 上下文菜单就是手机中对某一项进行==点击一定时间==后弹出的针对该项处理的菜单. context_menu.xml: <?xml versio ...

  2. java调试器

    javac.exe是编译.java文件 java.exe是执行编译好的.class文件 javadoc.exe是生成Java说明文档 jdb.exe是Java调试器 javaprof.exe是剖析工具 ...

  3. 记录 C++ STL 中 一些好用的函数--持续更新 (for_each,transform,count_if,find_if)

    在日常的编程中,有这么几种操作还是比较常见的: 把一组数据都赋值成一个数,在一组数据中查找一个数,统计一组数据中符合条件的数等等. 一般的写法可以用循环,没有什么是循环不能搞定的.假如在这里怎么用介绍 ...

  4. struts2--文件上传类型3

    拦截器栈在<package>标签内 <action>标签外配置 如上我们如果把它定义成默认拦截器的话就不需要在 <action>标签中引入,没有的话需要引入拦截器 ...

  5. ZOJ 1842 Prime Distance(素数筛选法2次使用)

    Prime Distance Time Limit: 2 Seconds      Memory Limit: 65536 KB The branch of mathematics called nu ...

  6. 删除多余的自编译的内核、mysql连接不了的问题

    1.删除多余的自编译的内核 每次Debian发布内核更新,总是有某些内核选项跟自己的硬件不配套,要自己编译内核.编译多了,多余的内核就占用了多余的硬盘空间.我就试过因为/boot分区满了,而导致编译内 ...

  7. 1st 结对编程:简易四则运算

    结对编程:简易四则运算 功能:进行简易的四则运算,并根据给出的结果判断正误. 实现:使用java的图形化界面实现. 导入包库 package six; import javax.swing.*; im ...

  8. 【leetcode】215. Kth Largest Element in an Array

    Find the kth largest element in an unsorted array. Note that it is the kth largest element in the so ...

  9. HDU 4767——Bell

    昨天比赛被虐的这个题目. 今天听斌牛讲过他的思路后就A掉了. 题目的意思是要你求出bell数的第n项对95041567取模. 首先,95041567=31*37*41*43*47: 然后取模就是先分别 ...

  10. ZOJ3113_John

    这个题目是一个典型的Anti_Sg.我也不知道为什么这么叫,呵呵,反正大家都这么叫,而且我也是听别人说,看别人的日志自己才知道的. 题目的意思是给你不同颜色的石子,每次可以去一种颜色的石子若干个(至少 ...