https://leetcode.cn/problems/generate-parentheses/
'('
或右括号')'
(受限于当前路径的合法性)。cnt
,算法确保不会放置过多的左括号(不超过n
)。i
与已放置的左括号数量cnt
,算法确保右括号的数量不会超过左括号的数量,从而保持括号组合的合法性。path
被填满时(即i == n * 2
),算法检查该路径是否形成了一个合法的括号组合,并将其添加到结果列表中。class Solution {
private int n; // 括号对的数量
private final List<String> ans = new ArrayList<>(); // 存储最终生成的合法括号组合
private char[] path; // 用于存储当前递归路径中的括号组合
public List<String> generateParenthesis(int n) {
this.n = n; // 初始化括号对的数量
path = new char[n * 2]; // 每组合法括号的长度为 n * 2(n 对括号)
dfs(0, 0); // 从第 0 个字符开始递归,初始左括号计数为 0
return ans; // 返回生成的合法括号组合列表
}
/**
* 深度优先搜索函数
* @param i 当前递归到路径的第几个字符(当前填充的索引位置)
* @param cnt 当前路径中左括号 '(' 的数量
*/
void dfs(int i, int cnt) {
// 递归终止条件:当前路径已经填满 n * 2 个字符
if (i == n * 2) {
ans.add(new String(path)); // 将当前路径的合法括号组合加入结果列表
return; // 返回上层递归
}
// 选择左括号 '(' 的情况
if (cnt < n) { // 如果左括号的数量还没有达到 n
path[i] = '('; // 在当前路径位置填入左括号
dfs(i + 1, cnt + 1); // 递归到下一个字符位置,并增加左括号计数
}
// 选择右括号 ')' 的情况
// 右括号的数量必须小于左括号数量,确保括号组合合法
if (i - cnt < cnt) {
path[i] = ')'; // 在当前路径位置填入右括号
dfs(i + 1, cnt); // 递归到下一个字符位置,左括号计数不变
}
}
}
https://leetcode.cn/problems/word-search/
1.优化一 字母个数够不够
2.头和尾 哪个重复字母数少,哪个在前
class Solution {
char[] word;
char[][] board;
int n, m;
//上下左右四个方向
int[] dx = {-1,0,1,0};
int[] dy = {0,1,0,-1};
int cnt;
public boolean exist(char[][] board, String word) {
this.word = word.toCharArray();
this.board = board;
n = board.length;
m = board[0].length;
// 数组代替哈希表
int[] cnt = new int[128];
for (char[] row : board) {
for (char c : row) {
cnt[c]++;
}
}
// 优化一 字母个数够不够
int[] wordCnt = new int[128];
for (char c : this.word) {
if (++wordCnt[c] > cnt[c]) {
return false;
}
}
// 优化二 头和尾 哪个重复字母数少,哪个在前
if (cnt[this.word[this.word.length - 1]] < cnt[this.word[0]]) {
this.word = new StringBuilder(word).reverse().toString().toCharArray();
}
for (int i = 0; i < n; i++)
for (int j = 0; j < m; j++)
if (dfs(i, j, 0))
return true;
return false;
}
boolean dfs(int i,int j,int cnt){
if (board[i][j] != word[cnt]) { // 匹配失败
return false;
}
if (cnt == word.length - 1) { // 匹配成功!
return true;
}
board[i][j] = '0'; // 标记访问过
//往四个方向
for(int k = 0;k<4;k++){
int xx = i + dx[k];
int yy = j + dy[k];
if(xx>=0 && xx < n && yy >= 0 && yy < m && dfs(xx,yy,cnt+1))
return true;
}
board[i][j] = word[cnt]; // 恢复现场
return false; // 没搜到
}
}
https://leetcode.cn/problems/palindrome-partitioning/
深度优先搜索(DFS)结合回溯法来探索字符串的所有可能回文分割。从字符串的起始位置开始,递归地尝试在每个位置插入分隔符,但仅当当前子串是回文时才真正插入。每次插入分隔符后,递归地处理剩余部分。若无法继续形成有效分割,则回溯,即移除最后一个加入的子串并尝试其他分割点。当遍历完整个字符串时,若形成了一种有效的回文分割,则将其保存。
class Solution {
private final List<List<String>> ans = new ArrayList<>();
private final List<String> path = new ArrayList<>();
private String s;
public List<List<String>> partition(String s) {
this.s = s;
dfs(0, 0);
return ans;
}
//i 当前字符的索引 start 当前子串的起始索引
private void dfs(int i, int start) {
// 如果当前索引已经到达字符串末尾,说明形成了一种分割路径
if (i == s.length()) {
ans.add(new ArrayList<>(path)); // 将当前路径加入结果集(注意这里需要复制 path)
return;
}
// 不选择在 i 和 i+1 之间插入分隔符(继续往后递归)
if (i < s.length() - 1) {
dfs(i + 1, start);
}
// 选择在 i 和 i+1 之间插入分隔符(以 s[i] 结尾作为一个子串)
if (isPalindrome(start, i)) { // 先判断当前子串是否为回文
path.add(s.substring(start, i + 1)); // 如果是回文,将当前子串加入路径
dfs(i + 1, i + 1); // 递归处理剩余部分(下一个子串从 i+1 开始)
path.remove(path.size() - 1); // 恢复现场(回溯,移除最后一个子串)
}
}
//判断是否是回文串
private boolean isPalindrome(int left, int right) {
while (left < right) {
if (s.charAt(left++) != s.charAt(right--))
return false;
}
return true;
}
}
https://leetcode.cn/problems/n-queens/
最核心的思路就是怎么进行标记。
正斜线:用直线方程表示: y-x+n=0 (加n是为了有偏移量,因为坐标必须>0,y-x有可能<0)
反斜线:y+x=0
class Solution {
List<List<String>> ans = new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
// 创建棋盘,初始化为 '.'
char[][] g = new char[n][n];
int[] co = new int[n]; // 列标记数组,co[i] 表示第 i 列是否被占用
int[] zx = new int[n * 2]; // 左斜对角线标记数组,zx[i] 表示是否被占用
int[] fx = new int[n * 2]; // 右斜对角线标记数组,fx[i] 表示是否被占用
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
g[i][j] = '.';
}
}
dfs(0, n, g, co, zx, fx);
return ans;
}
// 深度优先搜索函数
void dfs(int u, int n, char[][] g, int[] co, int[] zx, int[] fx) {
// 如果已经放置了 n 个皇后(到达第 n 行),将当前解加入结果列表
if (u == n) {
List<String> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
// 将当前棋盘的一行转为字符串并加入结果
list.add(new String(g[i]));
}
ans.add(list);
return;
}
// 尝试将皇后放置在第 u 行的每一列
for (int i = 0; i < n; i++) {
// 检查当前位置是否可以放置皇后
if (co[i] == 0 && zx[u - i + n] == 0 && fx[u + i] == 0) {
// 标记当前位置为占用状态
co[i] = zx[u - i + n] = fx[u + i] = 1;
g[u][i] = 'Q'; // 放置皇后
// 递归处理下一行
dfs(u + 1, n, g, co, zx, fx);
// 回溯:将当前位置恢复为未占用状态
g[u][i] = '.';
co[i] = zx[u - i + n] = fx[u + i] = 0;
}
}
}
}
https://leetcode.cn/problems/permutations/
模版题,注释就可以看懂。理解dfs的递归后的"恢复原状"。
class Solution {
private List<List<Integer>> ans = new ArrayList<>();
int[] nums;
// 用于记录当前路径的列表
private List<Integer> path = new ArrayList<>();
public List<List<Integer>> permute(int[] nums) {
// 标记每个元素是否已经被使用过
boolean[] flag = new boolean[nums.length];
this.nums = nums;
// 深度优先遍历生成全排列
dfs(0, nums.length, flag);
return ans;
}
// 深度优先遍历方法
private void dfs(int i, int len, boolean[] flag) {
// 如果当前路径长度等于数组长度,说明产生了一种全排列
if (path.size() == len) {
// 将当前路径的副本加入结果列表
ans.add(new ArrayList<Integer>(path));
return;
}
// 遍历数组中的每个元素
for (int j = 0; j < nums.length; j++) {
// 如果当前元素还没有被使用
if (!flag[j]) {
// 将当前元素标记为已使用
flag[j] = true;
// 将当前元素加入路径列表
path.add(nums[j]);
// 递归调用下一层,继续生成下一个元素
dfs(j + 1, len, flag);
// 回溯:移除当前路径中的最后一个元素
path.remove(path.size() - 1);
// 回溯:将当前元素标记为未使用
flag[j] = false;
}
}
}
}
https://leetcode.cn/problems/subsets/
模版题
回溯:
1.边界条件
2.递归参数
3.恢复原状
class Solution {
private List<List<Integer>> ans = new ArrayList<>(); // 存储所有子集的结果
private int[] nums; // 输入的数组
private List<Integer> path = new ArrayList<>(); // 当前子集的路径
// 主函数:返回所有子集
public List<List<Integer>> subsets(int[] nums) {
this.nums = nums; // 初始化 nums 数组
// 遍历所有可能的子集长度,从 0 到 nums.length
for(int i = 0; i <= nums.length; i++) {
dfs(0, i); // 通过 dfs 寻找当前长度的所有子集
}
return ans; // 返回所有子集
}
// 深度优先搜索函数:找到所有长度为 len 的子集
private void dfs(int i, int len) {
// 递归结束条件:如果当前子集路径的长度等于目标长度
if(path.size() == len) {
ans.add(new ArrayList<Integer>(path)); // 将当前路径添加到结果中
return;
}
// 遍历从 i 开始的所有元素
for(int j = i; j < nums.length; j++) {
path.add(nums[j]); // 将当前元素加入子集
dfs(j + 1, len); // 递归,继续构建子集
path.remove(path.size() - 1); // 回溯,移除最后加入的元素
}
}
}
https://leetcode.cn/problems/letter-combinations-of-a-phone-number/
模版题,经典题
class Solution {
private String[] map = new String[]{"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
private List<String> ans = new ArrayList<>();
// 用于临时存储当前递归路径的字符数组
private char[] path;
// 用于存储输入的数字字符串
private char[] digits;
public List<String> letterCombinations(String digits) {
int n = digits.length();
if(n == 0) return List.of();
this.digits = digits.toCharArray();
path = new char[n];
dfs(0);
return ans;
}
// 深度优先搜索方法,递归生成字母组合
private void dfs(int i){
// 如果当前索引等于数字字符串的长度,说明一组组合已生成,加入结果列表
if(i == digits.length){
ans.add(new String(path));
return;
}
// 获取当前数字对应的字母集合
for(char ch : map[digits[i]-'0'].toCharArray()){
// 将当前字符赋值到路径数组.直接覆盖,不用恢复原状
path[i] = ch;
// 递归调用,继续处理下一个数字
dfs(i+1);
}
}
}
https://leetcode.cn/problems/combination-sum/
常规dfs题,但是此题最重要的就是剪枝,因为已经排序,当第i个数使sum > target,则数组接下来的数都没必要进行计算。
class Solution {
private List<List<Integer>> ans = new ArrayList<>();
private int[] candidates;
private List<Integer> path = new ArrayList<>();
int target;
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
this.candidates = candidates;
this.target = target;
dfs(0, candidates.length, 0);
return ans;
}
private boolean dfs(int i, int len, int sum) {
if (sum > target)
return false;
if (sum == target) {
ans.add(new ArrayList<Integer>(path));
return true;
}
for (int t = i; t < len; t++)
{
path.add(candidates[t]);
if(!dfs(t, len, sum +candidates[t])){
path.remove(path.size() - 1);
break;
}
path.remove(path.size() - 1);
}
return true;
}
}