文章目录
17.电话号码 **
思路:
根据每个数字对应的字母列出他们所有的排列组合
- 最终答案vector< string > res(1,"");初始化为一个空串 ,映射表chars[8]对应字符串
- 遍历每一个数字,对于每个数字找到他在映射表中的所有备选字母
- 将所有字母添加到res所有元素后,构成一个新的verctor< string >now
- 将now赋值给res,进行下一次循环
输入数字为23时对应的流程
class Solution {
public:
string chars[8]={"abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
vector<string> letterCombinations(string digits) {
vector<string> res(1,"");
if(!digits.size())return {}; //输入数字为空返回{}
for(auto u:digits){ //遍历每个数字
vector<string> now;
for(auto c:chars[u-'2']) //找到该数字对应的字母
for(auto it:res){
now.push_back(it+c); //将每个字母加入到res之后构成一个新的Vector
}
res=now;
}
return res;
}
};
79.单词搜索 **
思路:对于每个单词都当做开头进行搜索
注意写法和技巧
class Solution {
public:
int n,m;
bool exist(vector<vector<char>>& board, string word) {
n=board.size(),m=board[0].size(); //行,列的值
if(!n||!m)return false;
for(int i=0;i<n;i++)
for(int j=0;j<m;j++)
if(dfs(board,i,j,word,0))return true; //对于矩阵里的每个点,把他们当做开头,搜索一遍
return false;
}
bool dfs(vector<vector<char>> &board,int x,int y,string &word,int count){
if(board[x][y]!=word[count])return false; //不匹配返回false
if(count==word.size()-1)return true; //当前是word的最后一个字母且匹配,则搜索到,返回true
board[x][y]='.'; //赋非常规值,代表已经遍历过,如果下一层再向这个方向走,就会返回
int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0}; //定义要搜索的四个方向
for(int i=0;i<4;i++){
int a=x+dx[i],b=y+dy[i];
if(a>=0&&a<n&&b>=0&&b<m){
if(dfs(board,a,b,word,count+1))return true; //向四个方向分别搜索,如果搜索到返回true
}
}
board[x][y]=word[count]; //回溯,该层恢复原状
return false; //所有方向都不完全匹配则返回false
}
};
46.全排列
排列顺序:对于1~n的每一个位置,再nums选择一个数(还有一种是数选位置,思想和高中排列组合一模一样,这里写法有些不同)
定义一个vector< bool > st 判断该数在当前路径是否使用过
vector< int >存储每一次dfs最后的答案
class Solution {
public:
vector<vector<int>> res;
vector<bool> st; //判重
vector<int> path; //存储每次的结果
int n;
vector<vector<int>> permute(vector<int>& nums) {
n=nums.size();
if(!n)return {{}};
st=vector<bool>(n);
dfs(0,nums);
return res;
}
void dfs(int u,vector<int> & nums){
if(u==n){ //搜索到第n层时返回,注意如果一共有3个数,那么搜索的次数是4次
res.push_back(path);
return ;
}
for(int i=0;i<n;i++){ //对于每个位置,枚举nums的每一个数
if(!st[i]){
path.push_back(nums[i]);
st[i]=true;
dfs(u+1,nums);
path.pop_back(); //每次放进向量的数要拿出
st[i]=false;
}
}
return;
}
};
47 全排列II
我们把数字序列看做带有编号的模式,如[1,1,2,2,2]是第一个1,第二个1,第一个2,第二个2,第三个2,并且人为规定顺序,即第二个1永远只能出现在第一个1的后面,这样就不会导致重复的问题
准备工作是首先将nums排序以保证相同的数有序的出现在一起
思路一:
- 数选位置(本题解法)
- 从头开始dfs每一个数,枚举当前数可能出现在什么位置(start,end),dfs下一个数的时候,如果下一个数和当前数相同,则下一个数的位置只能从(i+1,end)中枚举(i是上一个数最后确定的位置)否则下一个数从(0,end)中枚举
- 位置选数
- 从头开始dfs每一个位置(和上一题一样),对于每一个位置选数,如nums为
[111222333],第一个位置只能从第一个1第一个2第一个3中选,如果它选择了1,那么第二个位置只能从第二个1第一个2第一个3中选,以此类推
//数选位置
class Solution {
public:
vector<int> path; //每一种方案
vector<vector<int>>res; //答案
vector<bool> st; //标记向量
int n;
vector<vector<int>> permuteUnique(vector<int>& nums) {
n=nums.size();
if(!n)return {{}};
sort(nums.begin(),nums.end());
path=vector<int> (n); //为path开辟空间可以直接用下标赋值,避免使用push_back函数
st=vector<bool> (n);
dfs(nums,0,0); //从第0个数,起始位置是0开始dfs
return res;
}
void dfs(vector<int> &nums,int u,int start){
if(u==n){ //枚举n+1次时输出一串序列
res.push_back(path);
return;
}
for(int i=start;i<n;i++){ //枚举nums[u]可能存放的位置(start,end)
if(!st[i]){ //如果当前位置没有被占,那么为它赋值
path[i]=nums[u];
st[i]=true;
dfs(nums,u+1,u+1<n&&nums[u]==nums[u+1]?i+1:0); //搜索下一个数字存放的位置,如果nums[u]==nums[u+1]搜索(i+1,end)否则搜索(0,end);
st[i]=false;
}
}
}
};
78 子集
找出一组数的子集,区别于排列组合,只是寻找出所有可能的数的组合
例如[1,2,3]就有23 个子集(每一个数字只有两种情况,选和不选,一共三个数字所以是23,包括空集)
递归写法
- 加入空集
- 枚举1~n位置个数的情况
- 思想是位置选数
- 对于一个dfs,搜索nums(start,end),对于他的下一个dfs’,搜索nums(i+1,end)-- 确保下一层不回头枚举 i是dfs最后确定的元素下标
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int n;
vector<vector<int>> subsets(vector<int>& nums) {
n=nums.size();
res.push_back({});
for(int i=1;i<=n;i++){ //枚举一个元素,两个元素...n个元素的情况
dfs(nums,0,i,0);
}
return res;
}
void dfs(vector <int> &nums,int u,int count,int start){
if(u==count){ //u=count输出
res.push_back(path);
return;
}
for(int i=start;i<n;i++){ //对于每个位置u,寻找从(start,end)
path.push_back(nums[i]);
dfs(nums,u+1,count,i+1); //为避免重复下一个元素start置为i+1
path.pop_back(); //回溯
}
}
};
接下来写一个y总的另类解法
思路是每一种情况正好可用二进制表示
例如[1,2,3],n=3,正好对应8个二进数000,001,010…
每个二进制数都对应一个子集
如000对应空集{},001对应{3},101对应{1,3}等
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int>> res;
int n=nums.size();
for(int i=0;i<1<<n;i++){ //i<1右移n位 即枚举0~n-1的二进制数
vector<int> now;
for(int j=0;j<n;j++){
if(i>>j&1)now.push_back(nums[j]); //对于每个二进制数都做左移处理,如果当前位存在,则加入该元素
}
res.push_back(now);
}
return res;
}
};
90 子集II **
- 解一:
在前一题的基础上,首先我们知道了枚举无重复元素nums的时候每一层枚举不会回头(保证不出现同元素不同排列),并且每层起始位置+1
这题枚举存在重复元素nums时,只要保证当前层不会再出现相同的元素,即循环的时候每次跳到下一个不同的元素位置
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int n;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
n=nums.size();
res.push_back({});
sort(nums.begin(),nums.end()); //保证相同数出现在一起
for(int i=1;i<=n;i++){ //枚举一个元素,两个元素...n个元素的情况
dfs(nums,0,i,0);
}
return res;
}
void dfs(vector <int> &nums,int u,int count,int start){
if(u==count){ //u=count输出
res.push_back(path);
return;
}
for(int i=start;i<n;i++){ //对于每个位置u,寻找从(start,end)
path.push_back(nums[i]);
dfs(nums,u+1,count,i+1); //为避免重复下一个元素start置为i+1
path.pop_back(); //回溯
while(i<n-1&&nums[i]==nums[i+1])i++; //跳过这个位置相同的元素(剪枝)
}
}
};
- 解二:
枚举每一种可能的情况
如[11122]
那么1出现次数:0,1,2,3
2出现的次数0,1,2
对出现次数全排列,一共12中可能(包含空集)
class Solution {
public:
vector<vector<int>>res;
vector<int>path;
int n;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
n=nums.size();
sort(nums.begin(),nums.end());
dfs(nums,0);
return res;
}
void dfs(vector<int> &nums,int u){
if(u==n){ //u是nums元素下标,搜到最后一个元素下一个位置时返回一种情况
res.push_back(path);
return ;
}
int k=0;
while(u+k<n&&nums[u]==nums[u+k])k++; //找到当前元素相同的个数k
for(int i=0;i<=k;i++){ //枚举这个元素可能出现的次数
dfs(nums,u+k); //枚举下一个不同元素的出现次数
path.push_back(nums[u]); //最后放入元素,写在dfs前会缺少i=0的情况
}
for(int i=0;i<=k;i++)path.pop_back(); //回溯
}
};
dfs情况如[11122]
首次匹配1为0,2为0,1,2的情况
即path为[],[2],[22],加入res,然后清空(回溯)
然后匹配1为1,2为0,1,2的情况
path为[1],[1,2],[1,2,2],加入res,然后清空,以此类推
对比集合I和集合II的第一种朴素写法,发现都没有添加标记数组st
因为这两题都具有共有的特征–不回头枚举,即下一层元素和上一层不可能存在重复情况所以不必添加st数组
216.组合总和
例如k=3,从前向后枚举每一个位置需要填哪一个数,由于不能包含重复组合,所以每次dfs需要更新为(start+1,end)即不存在重复情况
dfs时加上一统计sum,如果sum==n并且搜索到k层时即可输出一个path
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
int nums;
int sum;
vector<vector<int>> combinationSum3(int k, int n) {
nums=k;
dfs(0,1,n);
return res;
}
void dfs(int u,int start,int n){
if(u==nums&&sum==n){ //搜到第k层并且sum=n即找到一组解
res.push_back(path);
return ;
}
for(int i=start;i<10;i++){
if(sum<n){ //如果总和小于n,枚举这个数字
sum+=i;
path.push_back(i);
dfs(u+1,i+1,n); //下一层从第i+1个数字开始枚举
sum-=i; //回溯
path.pop_back();
}
}
}
};
贴上y总从后往前dfs的写法
例k=3,从第k个位置开始向前枚举
class Solution {
public:
vector<vector<int>> res;
vector<int >path;
vector<vector<int>> combinationSum3(int k, int n) {
dfs(k,1,n); //从最后一个位置向前的dfs
return res;
}
void dfs(int k,int start ,int n){ //n时当前和,每次减去一个枚举的数字直到正好为0
if(!k){ //如果k==0并且n==0时得到一个结果
if(!n)res.push_back(path);
return ;
}
for(int i=start;i<=10-k;i++){ //i应该满足i~9至少有k个数字即9-i+1>=k 即i<=10-k
path.push_back(i);
dfs(k-1,i+1,n-i); //k-1,总和n-i
path.pop_back();
}
}
};
52.N皇后 II
经典老番
这题没要求输出具体结果字符串,只统计数量
还是同样的按行枚举
col(n)–列,dg(2n)–正对角线,udg(2n)反对角线标记
class Solution {
public:
int res;
int n;
vector<bool>col,dg,udg;
int totalNQueens(int _n) {
n=_n;
col=vector<bool> (n);
dg=udg=vector<bool> (2*n);
dfs(0);
return res;
}
void dfs(int u){
if(u==n)res++;
for(int i=0;i<n;i++){
if(!col[i]&&!dg[i-u+n]&&!udg[i+u]){
col[i]=dg[i-u+n]=udg[i+u]=true; //注意正对角线不要出现负数下标
dfs(u+1);
col[i]=dg[i-u+n]=udg[i+u]=false;
}
}
}
};
37.解数独 **
思路:同n皇后相似,遍历所有点,枚举出所有可能性(改题只有唯一解)
关键在于如何表达本题的标记(行,列和单元格)
我们用row[9][9]表示9行9数(每一行19是否被用过),col[9][9]表示9列9数(每一列19是否被用过),cell[3][3][9]表示3行3列一共9个单元格(看做整体)每个格子有9个数是否被使用过
class Solution {
public:
bool row[9][9]={0},col[9][9]={0},cell[3][3][9]={0}; //全部初始化为false
void solveSudoku(vector<vector<char>>& board) {
//初始化board(将已有元素标记)
for(int i=0;i<9;i++){
for(int j=0;j<9;j++){
if(board[i][j]!='.'){
int c=board[i][j]-'1'; //1~9字符转化为0~8的下标
row[i][c]=col[j][c]=cell[i/3][j/3][c]=true; //行列分别除以3定位到当前单元格
}
}
}
dfs(board,0,0);
}
bool dfs(vector<vector<char>>& board,int x,int y){
if(y==9)y=0,x++; //列枚举到头,转换到下一行
if(x==9)return true; //行枚举完,跳出
if(board[x][y]!='.')return dfs(board,x,y+1); //如果已经填过数,枚举下一个数
for(int i=0;i<9;i++){
if(!row[x][i]&&!col[y][i]&&!cell[x/3][y/3][i]){
board[x][y]=i+'1';
row[x][i]=col[y][i]=cell[x/3][y/3][i]=true;
if(dfs(board,x,y+1))return true; //添加if判断,便于在搜到正确答案时及时返回
row[x][i]=col[y][i]=cell[x/3][y/3][i]=false;
board[x][y]='.';
}
}
return false;
}
};
注解:对比八皇后和解数独 ?何时应添加if判断使其返回?
可以看到在同样的搜索方式下,解数独添加了返回条件,而八皇后则没有
因为八皇后可能存在多组解,在枚举到最后一层时会添加当前答案到res中
随后回溯(清除状态)返回到上面的某层重新枚举
而数独游戏只存在一组解,并且没有在搜索到最后一层时保存结果,此时添加if条件就会在搜索到最后一层时(只有正确答案能搜索到最后一层)返回,否则回溯就会覆盖当前正确答案
473.火柴拼正方形 ****(经典剪枝)
剪得很骚
顺序:枚举正方形的每一条边
为什么从大到小枚举?
假设木棒从大到小摆放的可能(分支)是3,5,7,8,9种,越大的木棒选择的位置数量越少,那么总共的分支数量就是3* 5* 7* 8* 9种
如果先枚举大边,那么剪枝时每一次砍掉的分支就是5 * 7 * 8 * 9种
而枚举小边每一次砍掉的数量一定小于大边一次性砍掉的 数量,所以先枚举大边要更快,而先枚举小边会最慢,随机枚举处于两者之间
class Solution {
public:
vector<bool> st;
bool makesquare(vector<int>& nums) {
int n=nums.size();
int sum=0;
for(int i=0;i<n;i++)sum+=nums[i];
if(!sum||sum%4)return false; //总长不被4整除一定不行
st=vector<bool> (n);
sort(nums.begin(),nums.end());
reverse(nums.begin(),nums.end()); //从大到小枚举
return dfs(nums,0,0,sum/4);
}
bool dfs(vector<int> &nums,int u,int cur,int length){
if(cur==length)u++,cur=0;
if(u==4)return true;
for(int i=0;i<nums.size();i++){
if(!st[i]&&cur+nums[i]<=length){
st[i]=true;
if(dfs(nums,u,cur+nums[i],length))return true;
st[i]=false; //执行到回溯部分说明if(dfs)部分返回false,表示使用当前nums[i]不能拼凑一个完整的边,需要进行剪枝
while(i<nums.size()-1&&nums[i]==nums[i+1])i++; //当前nums[i]不行,和他相等的肯定也不行,跳过
if(!cur)return false; //如果当前是枚举的第一个木棒,说明此种方案不行,重新枚举
if(cur+nums[i]==length)return false; //如果当前枚举的是最后一个木棒,此种方案同样不行,重新枚举
}
}
return false;
}
};