T78-子集
见LeetCode第78题子集问题
题目描述
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
我的思路
-
不能重复:代表需要进行排序
-
根节点的选择:数组中所有的元素
-
剪枝逻辑[题目给出元素不重复,无需剪枝]:当前节点的值和前一个兄弟节点的值相同,则直接continue -
如何确定递归的返回条件?不需要额外判定返回条件,遍历子节点的时候注意边界即可start == nums.length - 1
我的题解
private List<List<Integer>> resList = new ArrayList<>();
/**
* 子集问题
* @param nums
* @return
*/
public List<List<Integer>> subsets(int[] nums) {
resList.add(new ArrayList<>());
if (nums == null || nums.length == 0) return resList;
List<Integer> path = new ArrayList<>();
Arrays.sort(nums);
// 遍历根节点
for (int i = 0; i < nums.length; i++) {
// 剪枝逻辑
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
backtrace(i, nums, path);
}
return resList;
}
/**
* 递归求解
* @param start 用来判断当前子节点的范围
* @param i 当前子节点的索引
* @param nums
* @param path
*/
private void backtrace(int i, int[] nums, List<Integer> path) {
// 将当前的值添加到路径中
path.add(nums[i]);
resList.add(new ArrayList<>(path));
// 遍历子节点
for (int j = i + 1; j < nums.length; j++) {
if (j > i + 1 && nums[j] == nums[j - 1]) continue;
backtrace(j, nums, path);
}
// 将当前节点移除
path.remove(path.size() - 1);
}
T90-子集II
见LeetCode第90题子集II
题目描述
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
我的思路
见上一题T78-子集
我的题解
见上。
T491-非递减子序列
见LeetCode第491题非递减子序列
题目描述
给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。
数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。
示例 1:
输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]
我的思路
-
如何选择根节点?理论上区间
[0, nums.length - 1)的节点都可以做根节点 -
什么时候往结果集中添加元素?
- 每当路径中新增了一个元素,就直接将路径添加到结果集合
- 需要在循环中保证递归中每个节点都是合法的
-
是否需要剪枝?
- 根节点需要进行去重剪枝
- 子节点需要进行去重,并且保证递增
我的实现
private List<List<Integer>> resList = new ArrayList<>();
/**
* 非递增子序列
* @param nums
* @return
*/
public List<List<Integer>> findSubsequences(int[] nums) {
if (nums == null || nums.length <= 1) return resList;
List<Integer> path = new ArrayList<>();
boolean[] isVisited = new boolean[201];
// 根节点的选取
for (int i = 0; i < nums.length - 1; i++) {
// 剪枝
if (isVisited[nums[i] + 100]) {
// 当前值已经访问过
continue;
}
backtrace(i, path, nums);
isVisited[nums[i] + 100] = true;
}
return resList;
}
private void backtrace(int i, List<Integer> path, int[] nums) {
// 将当前节点添加到路径中
path.add(nums[i]);
// 如果元素数量大于 1 ,可以添加到结果集合中
if (path.size() > 1) {
resList.add(new ArrayList<>(path));
}
boolean[] isVisited = new boolean[201];
// 遍历孩子节点,孩子节点的区间为 [i + 1, nums.length - 1],是一个闭区间,可以到达最后一个,和根节点不一样
for (int j = i + 1; j < nums.length; j++) {
// 首先是剪枝: 当前节点已经被访问过 或者 不是递增子序列
if (isVisited[nums[j] + 100] || nums[j] < path.get(path.size() - 1)) {
continue;
}
backtrace(j, path, nums);
isVisited[nums[j] + 100] = true;
}
// 将当前节点移除
path.remove(path.size() - 1);
}
T46-全排列
见LeetCode第46题全排列
题目描述
给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。
示例 1:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
我的思路
-
全排列的结果集中,相同元素但是排列顺序不同仍然为有效的输出
-
在递归决策树的时候,应当记录已经访问的节点坐标
-
递归返回条件?
path.size() == nums.length,即为一个正确的结果
-
如果遍历子节点?
- 子节点的遍历区间为:
[0, numg.length - 1] - 要根据是否被访问过来决定剪枝
- 记录访问的数组
isVisited应当是一个实例变量
- 子节点的遍历区间为:
我的题解
private List<List<Integer>> resList = new ArrayList<>();
private boolean[] isVisited;
/**
* 全排列
* @param nums
* @return
*/
public List<List<Integer>> permute(int[] nums) {
if (nums == null || nums.length == 0) {
return resList;
}
isVisited = new boolean[nums.length];
List<Integer> path = new ArrayList<>();
// 遍历根节点,谁都可以当根节点,并且根节点不用进行剪枝
for (int i = 0; i < nums.length; i++) {
backtrace(i, nums, path);
}
return resList;
}
/**
* 递归求解
* @param i
* @param nums
* @param path
*/
private void backtrace(int i, int[] nums, List<Integer> path) {
// 添加当前的值,for 循环控制好 i 不会数组越界或者重复访问
path.add(nums[i]);
isVisited[i] = true;
// 判断返回条件
if (path.size() == nums.length) {
resList.add(new ArrayList<>(path));
// 将当前元素擦除
path.remove(path.size() - 1);
isVisited[i] = false;
return;
}
// 遍历子节点
for (int j = 0; j < nums.length; j++) {
// 剪枝
if (!isVisited[j]) {
backtrace(j, nums, path);
}
}
// 擦除当前节点
path.remove(path.size() - 1);
isVisited[i] = false;
}
T47-全排列II
见LeetCode第47题全排列
题目描述
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
示例 1:
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
我的思路
-
如果数组中存在重复数字,并且要求结果集中不能含有重复的全排列,需要在子节点和根节点中剪枝
-
如何进行剪枝呢?或者说,怎么确定当前节点和兄弟节点是相同的呢?
- 对于根节点,可以使用额外的
boolean[] isUsed来判断兄弟节点是否被访问过 - 对于子节点,一方面要通过
isVisited确定合法的兄弟节点,另一方面又要确定合法的兄弟节点和自己不一样,需要另个变量boolean[] isUsed来记录兄弟节点的访问?
- 对于根节点,可以使用额外的
我的题解
private final List<List<Integer>> resList = new ArrayList<>();
private boolean[] isVisited;
/**
* 没有重复的全排列
* @param nums
* @return
*/
public List<List<Integer>> permuteUnique(int[] nums) {
if (nums == null || nums.length == 0) return resList;
isVisited = new boolean[nums.length];
// 用来防止出现同一个数字被使用多次,数字的范围为[-10, 10]
boolean[] isUsed = new boolean[21];
List<Integer> path = new ArrayList<>();
// 遍历根节点
for (int i = 0; i < nums.length; i++) {
// 根节点的剪枝,判断当前根节点是否被使用过
if (!isUsed[nums[i] + 10]) {
backtrace(i, nums, path);
isUsed[nums[i] + 10] = true;
}
}
return resList;
}
/**
* 递归求解
* @param i
* @param nums
* @param path
*/
private void backtrace(int i, int[] nums, List<Integer> path) {
// 将当前的合法节点添加到路径中
path.add(nums[i]);
isVisited[i] = true;
// 判断返回条件
if (path.size() == nums.length) {
resList.add(new ArrayList<>(path));
// 移除当前节点
path.remove(path.size() - 1);
isVisited[i] = false;
return;
}
// 记录当前节点是否被本层节点使用过
boolean[] isUsed = new boolean[21];
// 遍历子节点
for (int j = 0; j < nums.length; j++) {
// 子节点有两个约束条件:当前的节点没有被上层节点使用 && 当前节点没有被本层节点使用
if (!isVisited[j] && !isUsed[nums[j] + 10]) {
backtrace(j, nums, path);
// 标记当前兄弟节点被访问
isUsed[nums[j] + 10] = true;
}
}
// 将当前节点从路径中移除
path.remove(path.size() - 1);
isVisited[i] = false;
}
T332-重新安排行程[未解决]
见LeetCode第332题重新安排行程
题目描述
给你一份航线列表 tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。
所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。
- 例如,行程
["JFK", "LGA"]与["JFK", "LGB"]相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。
我的思路【超时】
-
如何选择根节点?
- 根节点只能是
"JKF"为始发站的行程,遍历所有的票,如果from.equals(JFK)则开始回溯寻找可行的机票
- 根节点只能是
-
什么时候递归结束呢?
- 递归结束条件是所有的机票被使用了,所以需要有一个全局
boolean[]记录使用的情况
- 递归结束条件是所有的机票被使用了,所以需要有一个全局
-
如果有多个解,如何选择最优的呢?
- 满足递归结束条件之后,判断两种解的字典顺序,选择较小的
-
路径使用什么来记录?
List<String>:这个StringBuilder
T51-N皇后
见LeetCode第51题N皇后
题目描述
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
示例
输入:n = 4
输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释:如上图所示,4 皇后问题存在两个不同的解法。
我的思路
-
使用实例变量board记录已经落子的棋盘
-
每次落子的时候,函数
isValid判断是否可以落子 -
最重要的是如何判断该位置是否合法?
- 使用一个数组
List<int[]>记录已经落子的位置 - 在
isValid方法里判断,已经落子的位置是否和当前位置相关
- 使用一个数组
我的题解
private char[][] board;
List<List<String>> resList = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 棋盘初始化
board = new char[n][n];
for (char[] rows : board) {
Arrays.fill(rows, '.');
}
// 如果落子成功,将坐标放进此集合中
List<int[]> coordinates = new ArrayList<>();
int row = 0;
for (int col = 0; col < n; col++) {
backtrace(row, col, coordinates);
}
return resList;
}
/**
*
* @param row 行坐标
* @param col 列坐标
* @param coordinates 已经落子的坐标
*/
private void backtrace(int row, int col, List<int[]> coordinates) {
// 将当前坐标落子
board[row][col] = 'Q';
coordinates.add(new int[]{row, col});
// 判断是否为解
if (coordinates.size() == board.length) {
// 将当前棋盘加入到结果集
List<String> temp = new ArrayList<>();
for (char[] rowStr : board) {
temp.add(new String(rowStr));
}
resList.add(temp);
// 移除当前棋子
board[row][col] = '.';
coordinates.remove(coordinates.size() - 1);
return;
}
// 遍历下一行
for (int i = 0; i < board.length; i++) {
if (row + 1 < board.length && isValid(row + 1, i, coordinates)) {
backtrace(row + 1, i, coordinates);
}
}
// 将当前的位置擦除
board[row][col] = '.';
coordinates.remove(coordinates.size() - 1);
}
/**
* 判断当前位置是否合法
* @param row
* @param col
* @param coordinates 已经有的位置
* @return
*/
private boolean isValid(int row, int col, List<int[]> coordinates) {
for (int[] coordinate : coordinates) {
// 在同一列或者同一斜线
if (coordinate[1] == col // 同一列
|| row - coordinate[0] == col - coordinate[1] // 斜率为-1的直线
|| row - coordinate[0] == coordinate[1] - col // 斜率为1的直线
) {
return false;
}
}
return true;
}
T37-解数独
见LeetCode第37题解数独
题目描述
编写一个程序,通过填充空格来解决数独问题。
数独的解法需 遵循如下规则:
- 数字
1-9在每一行只能出现一次。 - 数字
1-9在每一列只能出现一次。 - 数字
1-9在每一个以粗实线分隔的3x3宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 '.' 表示。
我的思路
- 解数独和N皇后的思路相似,需要遍历当前位置可以选择的数
- 注意,有一点不同的是,数独仅有一个解
- 仅有一个解代表需要回溯方法有一个返回值,这样可以在找到解的时候,直接返回,
- 整体上的回溯顺序是,先列后行
/**
* 解数独
* @param board
*/
public void solveSudoku(char[][] board) {
int col = 0;
int row = 0;
backtrace(row, col, board);
}
/**
* 回溯解决数独
* @param row 当前行
* @param col 当前列
* @param board 棋盘
* @return
*/
private boolean backtrace(int row, int col, char[][] board) {
// 第 9 行,已经成功了
if (row == 9) return true;
if (col >= 9) return backtrace(row + 1, 0, board);
// 如果当前位置有数字,直接跳过
if (board[row][col] != '.') return backtrace(row, col + 1, board);
// 对当前位置进行遍历
for (char i = '1'; i <= '9'; i++) {
if (isValid(board, row, col, i)) {
// 将当前值写入棋盘
board[row][col] = i;
// 递归下一个节点
if (backtrace(row, col + 1, board)) return true;
// 当前位置擦除
board[row][col] = '.';
}
}
return false;
}
/**
* 判断当前位置是否可行
* @param board
* @param row
* @param col
* @param c
* @return
*/
private boolean isValid(char[][] board, int row, int col, char c) {
// 判断当前行
for (int j = 0; j < 9; j++) {
if (board[row][j] == c) return false;
}
// 判断列
for (int i = 0; i < 9; i++) {
if (board[i][col] == c) return false;
}
// 判断块
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
if (board[row / 3 * 3 + i][col / 3 * 3 + j] == c) return false;
}
}
return true;
}