lass Solution {
List<List<String>> result = new ArrayList<>();
Deque<String> deque = new LinkedList<>();
public List<List<String>> partition(String s) {
backtracking(s,0);
return result;
}
public void backtracking(String s, int startIndex){
if(startIndex == s.length()){
result.add(new ArrayList(deque));
return;
}
for(int i = startIndex; i < s.length(); i++){
if(isPalindrome(s, startIndex, i)){ //找出子串的区间范围,并判断是否为回文子串
String str = s.substring(startIndex, i + 1);//注意左闭右开区间
deque.addLast(str);
}else{
continue;
}
backtracking(s, i+1);
deque.removeLast();
}
}
public boolean isPalindrome(String s, int startIndex, int end){
for(int i = startIndex, j = end; i < j; i++, j--){
if(s.charAt(i) != s.charAt(j)){
return false;
}
}
return true;
}
}
思路:
难点总结:
- 切割问题可以抽象为组合问题
- 如何模拟那些切割线 startindex就相当于分割线
- 切割问题中递归如何终止 当startindex移动到字符串最后的时候就证明分割完毕,递归终止
- 在递归循环中如何截取子串 子串的区间其实是【startindex,i】利用substring把子串分割出来
- 如何判断回文 利用双指针法,从两侧同时开始遍历,出现不相等的字符返回false,否则返回true
class Solution {
List<String> result = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
if(s.length() > 12) return result;
backtrack(s, 0 ,0);
return result;
}
public void backtrack(String s, int startIndex, int pointNum){
if(pointNum == 3){//决定了树深,也是终止条件
if(isValid(s, startIndex, s.length() - 1)){ //左闭右闭区间,判断最后一段是都合法
result.add(s);
}
return;
}
for(int i = startIndex; i < s.length(); i++){
if(isValid(s, startIndex, i)){ //【startindex,i】代表切割的子串
s = s.substring(0, i+1) + "." + s.substring(i+1); //左闭右开区间
pointNum++;
backtrack(s, i+2, pointNum);
pointNum--;
s = s.substring(0,i+1) + s.substring(i+2);
}else{
break;
}
}
}
public Boolean isValid(String s, int start, int end){
if(start > end){
return false;
}
if(s.charAt(start) == '0' && start != end){
return false;
}
int num = 0;
for(int i = start; i <= end; i++){
if(s.charAt(i) > '9' || s.charAt(i) < '0'){
return false;
}
num = num * 10 + (s.charAt(i) - '0');
if(num > 255){
return false;
}
}
return true;
}
}
思路:
难点总结:
1. 如何判断子串合法:实现了一个isvalid方法,需要特别注意的是num = num * 10 + (s.charAt(i) - '0');代码的实现过程是
- 首先,
num
为0。我们首先看到字符'2',所以新的num
值为0*10 + 2 = 2
。 - 接下来,我们看到字符'4',所以新的
num
值为2*10 + 4 = 24
。 - 最后,我们看到字符'8',所以新的
num
值为24*10 + 8 = 248
2. 递归什么时候终止:当逗点数量等于3时,就是递归的终止条件,他控制了树深。并且需要注意的是,需要检查最后一段是否是合法ip字符串
3. 新字符串中的逗点如何加入和处理:【startindex,i】代表切割的子字符串,当插入逗点时,运用substring方法,注意是左闭右开区间,所以s.substring(0, i+1) + "." + s.substring(i+1)
class Solution {
List<List<Integer>> result = new ArrayList<>();
LinkedList<Integer> path = new LinkedList<>();
public List<List<Integer>> subsets(int[] nums) {
backtracking(nums, 0);
return result;
}
private void backtracking(int[] nums, int startIndex){
result.add(new ArrayList<>(path));
if(startIndex >= nums.length){ //终止条件可以不加
return;
}
for(int i = startIndex; i < nums.length; i++){
path.add(nums[i]);
backtracking(nums, i+1);
path.removeLast();
}
}
}
思路:
终止条件:这道题的终止条件可以不加,因为下面的循环也可以控制递归结束,当startindex已经超过nums.length的范围,循环自己就结束了。
注意:为什么收集结果result.add(new ArrayList<>(path));在终止条件上面,因为如果在终止条件里面的话,会将最后一个结果子集漏掉。
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
if (nums.length == 0){
result.add(path);
return result;
}
Arrays.sort(nums);
used = new boolean[nums.length];
subsetsWithDupHelper(nums, 0);
return result;
}
private void subsetsWithDupHelper(int[] nums, int startIndex){
result.add(new ArrayList<>(path));
if (startIndex >= nums.length){
return;
}
for (int i = startIndex; i < nums.length; i++){
if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){
continue;
}
path.add(nums[i]);
used[i] = true;
subsetsWithDupHelper(nums, i + 1);
path.removeLast();
used[i] = false;
}
}
}
思路:
这道题是组合2和子集题目的结合,主要关注于回溯的去重逻辑,分为树层去重和树根去重
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
backTracking(nums, 0);
return result;
}
private void backTracking(int[] nums, int startIndex){
if(path.size() >= 2) //子集必须要有两个元素
result.add(new ArrayList<>(path));
HashSet<Integer> hs = new HashSet<>(); //set在每一树层会刷新
for(int i = startIndex; i < nums.length; i++){
if(!path.isEmpty() && nums[i] < path.get(path.size() -1 ) || hs.contains(nums[i]))
continue;
hs.add(nums[i]);
path.add(nums[i]);
backTracking(nums, i + 1);
path.remove(path.size() - 1);
}
}
}
思路:
需要注意这道题
1. 收获子集必须要至少包含两个元素
2. 当前元素nums[i]必须要比path中末尾元素要小
3. 存在重复元素需要去重,也就是每一层取的元素不能一样,所以把遍历过的元素都放在一个set里面,然后每次遍历一个元素都要在set中检查是否之前已经出现过该元素。
class Solution {
List<List<Integer>> result = new ArrayList<>();// 存放符合条件结果的集合
LinkedList<Integer> path = new LinkedList<>();// 用来存放符合条件结果
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
if (nums.length == 0){
return result;
}
used = new boolean[nums.length];
permuteHelper(nums);
return result;
}
private void permuteHelper(int[] nums){
if (path.size() == nums.length){
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++){
if (used[i]){
continue;
}
used[i] = true;
path.add(nums[i]);
permuteHelper(nums);
path.removeLast();
used[i] = false;
}
}
}
思路:
这题是排列问题,与组合问题关键的不同是允许例如[1,2,3],[3,2,1]这样的结果存在,所以我们不需要startindex来控制循环必须是否从元素的下一个开始,而是直接从i=0开始遍历就可以。还有一点需要注意的是,结果集中不允许有重复的元素出现,例如[1,1,2],所以我们需要利用一个used数组来标记每个元素的使用情况,当遍历完一个元素时,对应的used数组应该标记为1,然后下一层遍历我们就如果当前的元素被使用过,我们就直接跳过continue,遇到没有被标记的元素我们再加入到path里
class Solution {
//存放结果
List<List<Integer>> result = new ArrayList<>();
//暂存结果
List<Integer> path = new ArrayList<>();
public List<List<Integer>> permuteUnique(int[] nums) {
boolean[] used = new boolean[nums.length];
Arrays.fill(used, false);
Arrays.sort(nums);
backTrack(nums, used);
return result;
}
private void backTrack(int[] nums, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过
// used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过
// 如果同⼀树层nums[i - 1]使⽤过则直接跳过
if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) {
continue;
}
//如果同⼀树⽀nums[i]没使⽤过开始处理
if (used[i] == false) {
used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用
path.add(nums[i]);
backTrack(nums, used);
path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复
used[i] = false;//回溯
}
}
}
}
思路:
这道题和上一个题目唯一的区别是,这道题要求输出所有不重复的全排列,所以代码上只多了两行树层去重的逻辑。
class Solution {
List<List<String>> res = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] chessboard = new char[n][n];
for (char[] c : chessboard) {
Arrays.fill(c, '.');
}
backTrack(n, 0, chessboard); //控制行数,也是树深
return res;
}
public void backTrack(int n, int row, char[][] chessboard) {
if (row == n) { //当行数和树深相等时,证明已经到了棋盘的最后一层
res.add(Array2List(chessboard));
return;
}
for (int col = 0;col < n; ++col) {
if (isValid (row, col, n, chessboard)) {
chessboard[row][col] = 'Q';
backTrack(n, row+1, chessboard);
chessboard[row][col] = '.';
}
}
}
public List Array2List(char[][] chessboard) {
List<String> list = new ArrayList<>();
for (char[] c : chessboard) {
list.add(String.copyValueOf(c));
}
return list;
}
public boolean isValid(int row, int col, int n, char[][] chessboard) {
// 检查列
for (int i=0; i<row; ++i) { // 相当于剪枝
if (chessboard[i][col] == 'Q') {
return false;
}
}
// 检查45度对角线
for (int i=row-1, j=col-1; i>=0 && j>=0; i--, j--) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
// 检查135度对角线
for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) {
if (chessboard[i][j] == 'Q') {
return false;
}
}
return true;
}
}
思路:
如何判断棋盘格是否合法
- 不能同行
- 不能同列
- 不能同斜线 (45度和135度角)左上角,右下角
为什么没有在同行进行检查呢?
因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。
递归终止条件:当rows等于棋盘格的长度的时候,表示已经遍历到最后一行,即为终止。
class Solution {
// 主函数,开始解数独问题
public void solveSudoku(char[][] board) {
solveSudokuHelper(board);
}
// 递归解数独辅助函数
private boolean solveSudokuHelper(char[][] board){
// 两层for循环用于遍历棋盘上的每一个格子
for (int i = 0; i < 9; i++){ // 遍历行
for (int j = 0; j < 9; j++){ // 遍历列
// 如果当前格子已经填了数字(不是'.'),则跳过
if (board[i][j] != '.'){
continue;
}
// 对当前格子尝试放入'1'到'9'
for (char k = '1'; k <= '9'; k++){
// 判断数字k是否可以放到当前格子
if (isValidSudoku(i, j, k, board)){
board[i][j] = k; // 放入数字
// 递归尝试填下一个格子
if (solveSudokuHelper(board)){
return true; // 如果下面的填法都对,则这样填当前格子是正确的
}
board[i][j] = '.'; // 如果下面的填法不对,撤回k,尝试下一个数字
}
}
// 如果1-9都尝试过了都不行,说明前面的格子填法有问题,需要回溯
return false;
}
}
// 所有的格子都填满了,说明找到了解答
return true;
}
// 检查数字val放在棋盘的(row, col)处是否是合法的
private boolean isValidSudoku(int row, int col, char val, char[][] board){
// 检查行是否有重复
for (int i = 0; i < 9; i++){
if (board[row][i] == val){
return false;
}
}
// 检查列是否有重复
for (int j = 0; j < 9; j++){
if (board[j][col] == val){
return false;
}
}
// 检查3x3的方格是否有重复
int startRow = (row / 3) * 3;
int startCol = (col / 3) * 3;
for (int i = startRow; i < startRow + 3; i++){
for (int j = startCol; j < startCol + 3; j++){
if (board[i][j] == val){
return false;
}
}
}
return true; // 在行、列和3x3方格都没有重复,所以是合法的
}
}
- 用行号(row)除以3来确定这个位置在哪一个大的行区间(0-2、3-5、或6-8)。
- 用列号(col)除以3来确定这个位置在哪一个大的列区间(0-2、3-5、或6-8)。
代码中的(row / 3) * 3
和(col / 3) * 3
实际上就是在做上述的操作,以便确定3x3九宫格的起始位置。
以startRow
为例:
- 如果
row
是0、1、或2,那么row / 3
的结果是0,乘以3之后还是0,表示这个3x3九宫格的起始行是0。 - 如果
row
是3、4、或5,那么row / 3
的结果是1,乘以3之后变成3,表示这个3x3九宫格的起始行是3。 - 如果
row
是6、7、或8,那么row / 3
的结果是2,乘以3之后变成6,表示这个3x3九宫格的起始行是6。
当我们可以一直从1-9的数字中找到一个数填棋盘格时,我们就一直递归返回true,当我们最终把棋盘格填满时, 我们就退出了循环,所以返回true