前言
全排列问题大部分都是一个模板的。 以前一直以为是通过一层递归一次添加一个元素这样,但是写起来没有位置互换这样方便,要考虑的情况也太多。 记录一下这种调换式的解题模板一、剑指offer-38:字符的全排列问题
题目描述:输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。
示例:
输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
思路:
从1到n,每次固定第一个位置的字母。
从下一个字母到最后一个字母,与第一个字母分别互换.
固定完后,下标后移一位,递归的对剩下n-1的字母进行同样的操作。
需要处理的特殊情况:
- 重复字母
如abb
固定a后,第二位与a互换和第三位与a互换实际上是等价的,为了防止递归进入,需要进行剪枝。
使用的秘诀是通过HashSet将每次已经互换过的字符添加进入set,当轮到下一个字母时,看看HashSet中有没有这个字符,若有,直接跳过。
- 递归后还原处理
防止这次的迭代造成数组元素变动,需要将数组元素返还
char[] p = null;
List<String> result = null;
public String[] permutation(String s) {
if (s == null || s.length() == 0) return new String[]{""};
p = s.toCharArray();
result = new ArrayList<String>();
combine(0);
String[] res = new String[result.size()];
result.toArray(res);
return res;
}
public void combine(int index) {
//后移到只有一个字母的时候,就不需要再互换了,直接将字符数组组成的字符串返回
if (index == p.length - 1) {
result.add(String.valueOf(p));
return;
}
Set<Character> set = new HashSet<>();
for (int i = index; i < p.length; i++) {
//若当前字母与之前已经互换过的字母相等,不再互换
if (set.contains(p[i])) continue;
//存储下当前交换的位置的字母
set.add(p[i]);
swap(index, i);
//递归地进行剩下 n - index的字母的排列
combine(index + 1);
// 还原数组,防止分支污染
swap(i, index);
}
}
public void swap(int i, int j) {
char temp = p[j];
p[j] = p[i];
p[i] = temp;
}
二、LeetCode-51:八皇后问题
题目表述:
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。
分析:
先不计较输出。
将皇后的位置抽象为一维数组:q[n]
-
第一点,每个皇后的位置不在同一行。
因为直接使用数组的下标作为行号,下标自然不可能重合。 -
第二,要保证列号不一样。
q[i] != q[j] -
第三点要求不在同一个斜线
设 i < j,则满足等式
i - j != abs(q[i] - q[j])
将这个等式作为分支的剪枝条件。
可以专门设置一个函数,去判断当前插入的元素合不合法。若不合法,直接跳过。
好吧,还有一个沙雕的输出问题,这个应该不是什么问题吧?
其实真挺难得。
当迭代到第n + 1个位置后,将一维数组的信息转化为结果。
观察结果,使用List<List<String>>.
外层的List可以认为是方案,每次n + 1次位置只需要添加一次。
内层的List为当前每一行的信息转为字符串的效果。
我们的设定是:
将一维数组q的下标作为行号,将q的数值作为列号。
因此可设计两层循环,外层遍历行,一共n次。【因为每行元素是一个String,所以在外层循环开始时设置一个StringBuilder】
内存循环遍历列,我们可以这样:
对列的每个可能值遍历一遍,当列标遍历到刚好是q[i]中所存的那个列标时,添加一个“Q”
代码就很简单了
int[] q = null;
List<List<String>> result = null;
public List<List<String>> solveNQueens(int n) {
//舍弃下标为0的单元
q = new int[n + 1];
System.out.println(Arrays.toString(q));
result = new ArrayList<>();
dfs(1);
System.out.println(result);
return result;
}
public void dfs(int x) {
if (x == q.length) {
// 运行到棋盘外了
// 此时应该收集战果,将之前的结果变成字符串返回
// 观察返回值的样子,发现是一个字符串的列表的列表,因此要用双重循环
/*
外层表示每一行;内层表示每一列。当数组元素的该列有值时,添加为Q
[
[".Q..","...Q","Q...","..Q."],
["..Q.","Q...","...Q",".Q.."]
]
* */
List<String> res = new ArrayList<>();
for (int i = 1; i < q.length; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 1; j < q.length; j++) {
if (q[i] == j) sb.append("Q");
else sb.append(".");
}
res.add(sb.toString());
}
result.add(res);
return;
}
for (int i = 1; i < q.length; i++) {
q[x] = i; //将当前行x行的皇后摆放在第i列
if (check(x)) {
//为true,表示当前的插入方法不会导致失败,可以插入[x, i]这一格皇后,继续向下查看
dfs(x + 1);
} else {
//为false,不能插入,应该跳出循环
//写不写无所谓
continue;
}
}
}
/**
* 当方法运行到第i行时,当前的插入方法是否可以保证与前面的i-1行即不重合列,又不在同一个斜边
* @param i
*/
public boolean check(int i) {
for (int j = 1; j < i; j++) {
if (q[i] == q[j] || Math.abs(i - j) == Math.abs(q[i] - q[j])) {
return false;
}
}
return true;
}
后话
开篇说是回溯调换法,第二题八皇后还是没用上。我试了一下,发现异常的复杂,最后我自己都糊涂了,还没有这种添加元素式的简单。看来什么套路也不是万能的。