给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合,输出组合的数量。
candidates 中的数字可以无限制重复被选取。
说明:
所有数字(包括 target)都是正整数。
解集不能包含重复的组合。
示例 1:
输入:candidates = [2,3,6,7], target = 7,
所求解集为:
[
[7],
[2,2,3]
]
输出:2
示例 2:
输入:candidates = [2,3,5], target = 8,
所求解集为:
[
[2,2,2,2],
[2,3,3],
[3,5]
]
输出:3
使用回溯的方法,注意每个元素可以多次选择。
剪枝剪在当前和已经超过target时。
像这种求和的方法,通常有加和减两种方式来实现,加很容易想到,但其实减的代码会更加简洁一点。
void dfs(int start,int target)
{
//如果target为0了,则这是一种解答
if(target==0)
{
ans.push_back(path);
return;
}
//target-candidate[i]>=0剪枝操作
for(int i=start;i<candidate.size()&&target-candidate[i]>=0;i++)
{
path.push_back(candidate[i]);
//因为可以重复选,下一次dfs开始的下标还是i
dfs(i,target-candidate[i]);
//pop 回溯
path.pop_back();
}
}
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
//先排序
sort(candidates.begin(),candidates.end());
this->candidate=candidates;
//初始为从数组第一个元素开始,还需总和为target
dfs(0,target);
return ans;
}
target使用减法会方便很多,少了一个参数也不需要进行sum==target的判断,牢记。
给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合,输出组合的数量。
candidates 中的每个数字在每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
所求解集为:
[
[1, 7],
[1, 2, 5],
[2, 6],
[1, 1, 6]
]
输出:4
示例 2:
输入: candidates = [2,5,2,1,2], target = 5,
所求解集为:
[
[1,2,2],
[5]
]
输出:2
跟前一题不一样的点在于,这次每个元素只能选取一次了。
一开始会很想当然的感觉 让下一次dfs从i+1开始即可。
但是会重复计算相同的情况,比如示例2:[1,2’,2’’]与[1,2’,2’’’]是同一种组合情况,所以去除重复的计算怎么来考虑呢。
在这一次循环中,如果当前元素跟前一个元素一样的话,其实它就不用考虑了,因为前一个元素已经将所有情况包含在里面了,并且考虑的范围更加广。
图解可以看leetcode题解
但是这个循环的第一个元素还是得放进去的,所以i>start,如果还不能理解可以看上述链接的评论一。
void dfs(int start,int target)
{
if(target==0)
{
ans.push_back(path);
return;
}
for(int i=start;i<candidate.size()&&target-candidate[i]>=0;i++)
{
if(i>start && candidate[i]==candidate[i-1])
{
continue;
}
path.push_back(candidate[i]);
dfs(i+1,target-candidate[i]);
path.pop_back();
}
}
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(),candidates.end());
this->candidate=candidates;
dfs(0,target);
return ans;
}
这个剪枝思想十分重要和巧妙,无需想的太复杂,前面元素考虑的dfs情况肯定会包含了后面重复元素的情况,为了避免相同组合情况的出现,可以忽略。但注意是在同一个for循环当中。
if(i>start && candidate[i]==candidate[i-1])
{
continue;
}
给定一个二维网格和一个单词,找出该单词是否存在于网格中。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例:
board =
[
[‘A’,‘B’,‘C’,‘E’],
[‘S’,‘F’,‘C’,‘S’],
[‘A’,‘D’,‘E’,‘E’]
]
给定 word = “ABCCED”, 返回 true
给定 word = “SEE”, 返回 true
给定 word = “ABCB”, 返回 false
非常典型的回溯算法题目,直接采用暴力的行为来解决,从每一个位置开始往四周走,如果走出了给定word就返回true。
由于每个字母只能使用一次,因此需要visited辅助数组。使用了二维方向数组来控制方向,直接学过一个一维的
由于只需返回能否找到,因此省去了path储存的要求,直接使用index来判断每个字符是否正确。
bool dfs(int i,int j,int index,vector<vector<char>> &board,string &word,vector<vector<bool>> &visit)
i,j为在当前board的坐标,index为现在比较word的字符下标。
int dir[4][4]={
{
-1,0},{
0,1},{
0,-1},{
1,0}};
bool exist(vector<vector<char>>& board, string word) {
int m=board.size();
int n=board[0].size();
//初始化visited
vector<vector<bool>> visit(m,vector<bool>(n));
for(int i=0;i<m;i++)
{
for(int j=0;j<n;j++)
{
//暴力解法,每个位置都进行一次探索
if(dfs(i,j,0,board,word,visit))
{
return true;
}
}
}
return false;
}
bool dfs(int i,int j,int index,vector<vector<char>> &board,string &word,vector<vector<bool>> &visit)
{
//最后一个字符时
if(index==word.size()-1)
{
return word[index]==board[i][j];
}
if(word[index]==board[i][j])
{
visit[i][j]=true;
for(int k=0;k<4;k++)
{
//控制方向
int x=i+dir[k][0];
int y=j+dir[k][1];
if(x>=0&&x<board.size()&&y>=0&&y<board[0].size()&&!visit[x][y])
{
if(dfs(x,y,index+1,board,word,visit))
{
return true;
}
}
}
//回溯的操作,将该位改回未访问
visit[i][j]=false;
}
return false;
}
首先二维vector的初始化,总是忘记…
vector<vector<bool>> visit(m,vector<bool>(n));
一定要记得回溯的操作,在所有操作结束后,要将该元素回到操作之前的状态。
像这题没有要求输出每一次的结果,只需要返回bool类型,就不需要path然后在比较字符串了,巧妙的使用index来简化。
给定一个只包含数字的字符串,复原它(在中间插入点号)并返回所有可能的 IP 地址格式,输出可能的格式的数量。
有效的 IP 地址正好由四个整数(每个整数位于 0 到 255 之间)组成,整数之间用 ‘.’ 分隔。
示例:
输入: “25525511135”
输出: 2
说明:因为可能的IP地址包括:[“255.255.11.135”, “255.255.111.35”]
这题用几个循环也是能够实现的,但最主要的是dfs的思想。
这边最主要是使用一个参数n,来计算当前分割了几个数字,如果n=4了,且还需分割的字符串s已经为空了,则说明这是一种正确的分割方式,放入答案中。
否则就继续分割,数字位数有三种可能1,2,3,要判断是否符合(0,255]的要求,其实用例中还暗藏了一个要求,就是不能0开头,这个判断方式也很巧妙。
vector<string> ans;
vector<string> restoreIpAddresses(string s) {
string ip;
dfs(s,0,ip);
return ans;
}
//s为还需分割的字符串,n为当前数字个数,ip存储答案
void dfs(string s,int n,string ip)
{
if(n==4)
{
if(s.empty())
{
ans.push_back(ip);
}
}
else
{
//数字有1,2,3位的可能性
for(int i=1;i<4;i++)
{
if(s.size()<i)
{
break;
}
//判断整数范围是否符合要求
int val=stoi(s.substr(0,i));
//判断是否0开头
if(val>255||i!=to_string(val).size())
{
continue;
}
dfs(s.substr(i),n+1,ip+s.substr(0,i)+(n==3?"":"."));
}
}
return ;
}
首先是substr函数的使用方法,第一个参数为开始位置,第二个参数为切割长度默认一直切到字符串尾。
其次是使用i!=to_string(val).size()
来判断字符串是否以0开头。
这里使用计数n,来实现大量剪枝的操作,可以学习到。
给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。
返回 s 所有可能的分割方案的数量。
示例:
输入: “aab”
输出: 2
说明:可能的分割方案有:
[
[“aa”,“b”],
[“a”,“a”,“b”]
]
需要一个vector来存储这一次的分割情况。
是否为回文字符串的判断函数,这个十分巧妙,无需传入切割下来的这个字符串,会有点复杂,直接传入原本字符串和起止下标即可,快速判断。
如果当前切割的字符串符合要求,则继续dfs。
vector<string> path;
vector<vector<string>> ans;
//是否回文判断函数
bool isValid(string &s,int i,int j)
{
while(i<j)
{
if(s[i]!=s[j])
{
return false;
}
i++;
j--;
}
return true;
}
//pre 为现在开始切割的下标
void dfs(string &s,int pre)
{
//如果切到最后了,则这是一种切割方案
if(pre==s.size())
{
ans.push_back(path);
return;
}
for(int i=pre;i<s.size();i++)
{
if(isValid(s,pre,i))
{
path.push_back(s.substr(pre,i-pre+1));
dfs(s,i+1);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
if(s.length()==0)
{
return ans;
}
dfs(s,0);
return ans;
}
其实做到这里,dfs的思想并不难领会,而且一些题目一看就是需要回溯来实现的,关键就是函数怎么来设计,参数选择哪几个才是最简洁,这里直接选择传入数组下标,回文字符串判断函数总也是下标参数,要学习。
给定一个整数数组 nums,返回区间和在 [lower, upper] 之间的个数,包含 lower 和 upper。
区间和 S(i, j) 表示在 nums 中,位置从 i 到 j 的元素之和,包含 i 和 j (i ≤ j)。
说明:
最直观的算法复杂度是 O(n2) ,请在此基础上优化你的算法。
示例:
输入: nums = [-2,5,-1], lower = -2, upper = 2,
输出: 3
解释: 3个区间分别是: [0,0], [2,2], [0,2],它们表示的和分别为: -2, -1, 2。
暴力解法的话 肯定是O(n^2),一开始想到滑动窗口的解法,但是有负数所以无法实现,肯定是是需要用到前缀和的知识,并且需要将其存储在set中,还看到线段树的解法这学了后面知识再说。
对于任何子数组之和[i,j]都可以表示为prej-prei,
所以lower<=prej-prei<=upper转换为
prej-upper<=prei<=prej-lower
找符合该条件的前缀和有几个,则子数组有多少个,采用lower_bound(第一个大于等于)和upper_bound(第一个大于),通过计算两者之间的迭代器之间的距离表示个数,使用distance。
参考题解
int countRangeSum(vector<int>& nums, int lower, int upper) {
int size=nums.size();
multiset<long long> preSum;
long long tempSum=0;
int ans=0;
for(int i=0;i<size;i++)
{
tempSum+=nums[i];
if(tempSum>=lower&&tempSum<=upper)
{
ans++;
}
if(preSum.size()!=0)
{
long long temp1=tempSum-upper;
long long temp2=tempSum-lower;
auto it1=preSum.lower_bound(temp1);
auto it2=preSum.upper_bound(temp2);
ans+=std::distance(it1,it2);
}
preSum.insert(tempSum);
}
return ans;
}
关于upper_bound和lower_bound的知识,返回的是地址,更详细内容,以及使用distance计算迭代器之间的距离。
给定一个整数数组 nums 和一个正整数 k,找出是否有可能把这个数组分成 k 个非空子集,其总和都相等。
示例 1:
输入: nums = [4, 3, 2, 3, 5, 2, 1], k = 4
输出: True
说明: 有可能将其分成 4 个子集(5),(1,4),(2,3),(2,3)等于总和。
比较典型的dfs算法的题目,可以先进行预先的判断sum/k是否为整,这里采用的是逐渐相加sum与target进行比较,也可以使用减法的,需要添加visited数组记录该数是否使用过,同时添加了start参数表示这一次从数组下标几开始考虑
bool dfs(vector<int> nums,int numsize,int k,int target, int temp,int start,int* visited)
{
//只剩一组肯定OK的
if(k==1)
{
return true;
}
if(temp>target)
{
return false;
}
if(temp==target)
{
//这里两个参数都为0 的原因是 还是从头开始找,但是有visited的帮助,不会重复,否则不知道从哪开始找
return dfs(nums,numsize,k-1,target,0,0,visited);
}
else{
for(int i=start;i<numsize;i++)
{
//之前的元素都考虑过了
if(!visited[i])
{
visited[i]=1;
if(dfs(nums,numsize,k,target,temp+nums[i],i+1,visited))
{
return true;
}else
{
visited[i]=0;
}
}
}
}
return false;
}
bool canPartitionKSubsets(vector<int>& nums, int k) {
int sum=0;
for(int i=0;i<nums.size();i++)
{
sum+=nums[i];
}
int *visited = new int[nums.size()];
memset(visited,0,sizeof(int)*nums.size());
if(k==1)
{
return true;
}
if(sum%k!=0)
{
return false;
}
return dfs(nums,nums.size(),k,sum/k,0,0,visited);
}
可以先预先进行整体判断节约时间,并且这里的start参数总是从头开始的,因为有visited的辅助,不需要考虑复杂的从何开始继续,而是从头开始简洁一点,不会重复。
给定一个化学式formula(作为字符串),返回每种原子的数量。
原子总是以一个大写字母开始,接着跟随0个或任意个小写字母,表示原子的名字。
如果数量大于 1,原子后会跟着数字表示原子的数量。如果数量等于 1 则不会跟数字。例如,H2O 和 H2O2 是可行的,但 H1O2 这个表达是不可行的。
两个化学式连在一起是新的化学式。例如 H2O2He3Mg4 也是化学式。
一个括号中的化学式和数字(可选择性添加)也是化学式。例如 (H2O2) 和 (H2O2)3 是化学式。
给定一个化学式,输出所有原子的数量。格式为:第一个(按字典序)原子的名子,跟着它的数量(如果数量大于 1),然后是第二个原子的名字(按字典序),跟着它的数量(如果数量大于 1),以此类推。
示例 1:
输入:
formula = “H2O”
输出: “H2O”
解释:
原子的数量是 {‘H’: 2, ‘O’: 1}。
示例 2:
输入:
formula = “Mg(OH)2”
输出: “H2MgO2”
解释:
原子的数量是 {‘H’: 2, ‘Mg’: 1, ‘O’: 2}。
示例 3:
输入:
formula = “K4(ON(SO3)2)2”
输出: “K4N2O14S4”
解释:
原子的数量是 {‘K’: 4, ‘N’: 2, ‘O’: 14, ‘S’: 4}。
注意:
所有原子的第一个字母为大写,剩余字母都是小写。
formula的长度在[1, 1000]之间。
formula只包含字母、数字和圆括号,并且题目中给定的是合法的化学式。
这题一看我就不会做…参考题解
对于s[i]而言有以下几种情况
1.大写字母,如果暂存的字符栈中有字符,则将栈清空,将新的大写字母加入,例如MgO,扫描到O时
2.小写字母,加入栈中,扫描到g时
3.数字,则需要对count进行操作,可能是Mg12O12的情况,需要count*=10的操作
4.如果是‘(’则需要进行递归操作,找到与其配对的‘)’,可能出现K4(ON(SO3)2)2多个配对括号的情况,所以需要bucket计数。将(里面这一串)进行递归操作,计算括号外面的count。
map
map<string,int> parse(const string exp,int left, int right)
{
string atom;
int count=0;
map<string,int> m;
int i=left;
while(i<=right)
{
char c=exp[i];
if(isupper(c))
{
if(!atom.empty())
{
m[atom]+=max(count,1);
atom.clear();
count=0;
}
atom+=c;
}else if(islower(c))
{
atom+=c;
}else if(isdigit(c))
{
count*=10;
count+=c-'0';
}else if(c=='(')
{
if(!atom.empty())
{
m[atom]+=max(count,1);
atom.clear();
count=0;
}
int temp= ++i;
int bucket=1;
while(i<=right&&bucket!=0)
{
if(exp[i]=='(') ++bucket;
if(exp[i]==')') --bucket;
if(bucket==0) break;
++i;
}
auto m1=parse(exp,temp,i-1);
count=0;
while(i+1<=right&&isdigit(exp[i+1]))
{
count*=10;
count+=exp[++i]-'0';
}
count=max(count,1);
for(auto& p:m1)
{
m[p.first]+=p.second*count;
}
count=0;
}
++i;
}
if(!atom.empty())
{
m[atom]+=max(count,1);
}
return m;
}
string countOfAtoms(string formula) {
auto m=parse(formula,0,formula.size()-1);
string s;
for(auto&p:m)
{
s+=p.first;
if(p.second>1)
{
s+=to_string(p.second);
}
}
return s;
}
遇见复杂的题目还是要静下心来好好看,模拟一点点来,关于复杂递归就分情况开始套路,按解题思路来,这个函数返回类型为map
用字符串数组作为井字游戏的游戏板 board。当且仅当在井字游戏过程中,玩家有可能将字符放置成游戏板所显示的状态时,才返回 true。
该游戏板是一个 3 x 3 数组,由字符 " ",“X” 和 “O” 组成。字符 " " 代表一个空位。
以下是井字游戏的规则:
玩家轮流将字符放入空位(" ")中。
第一个玩家总是放字符 “X”,且第二个玩家总是放字符 “O”。
“X” 和 “O” 只允许放置在空位中,不允许对已放有字符的位置进行填充。
当有 3 个相同(且非空)的字符填充任何行、列或对角线时,游戏结束。
当所有位置非空时,也算为游戏结束。
如果游戏结束,玩家不允许再放置字符。
示例 1:
输入: board = ["O ", " ", " "]
输出: false
解释: 第一个玩家总是放置“X”。
示例 2:
输入: board = [“XOX”, " X ", " "]
输出: false
解释: 玩家应该是轮流放置的。
示例 3:
输入: board = [“XXX”, " ", “OOO”]
输出: false
示例 4:
输入: board = [“XOX”, “O O”, “XOX”]
输出: true
说明:
游戏板 board 是长度为 3 的字符串数组,其中每个字符串 board[i] 的长度为 3。
board[i][j] 是集合 {" ", “X”, “O”} 中的一个字符。
该题主要是分类讨论,因为只有3*3,所以很好解决,首先统计X和O的个数,X一定是大于等于O的,X=O或X=O+1,否则直接false。
如果X玩家赢,则X=O+1;
如果O玩家赢,则X=O;
判断赢的话,直接实现就行,某一列或某一行或者对角线都是该字符则为赢
bool validTicTacToe(vector<string>& board) {
int oCount=0,xCount=0;
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
if(board[i][j]=='O')
{
oCount++;
}else if(board[i][j]=='X')
{
xCount++;
}
}
}
if(xCount!=oCount&&oCount!=xCount-1)
{
return false;
}
if(win(board,'X')&&oCount!=xCount-1)
{
return false;
}
if(win(board,'O')&&oCount!=xCount)
{
return false;
}
return true;
}
bool win(vector<string> A,char C)
{
for(int i=0;i<3;i++)
{
if(A[i][0]==C&&A[i][1]==C&&A[i][2]==C)
{
return true;
}
if(A[0][i]==C&&A[1][i]==C&&A[2][i]==C)
{
return true;
}
}
if(A[0][0]==C&&A[1][1]==C&&A[2][2]==C)
{
return true;
}
if(A[0][2]==C&&A[1][1]==C&&A[2][0]==C)
{
return true;
}
return false;
}
虽然下棋总是需要递归的思想,但是该题因为情况比较简单所以直接分类就可以解决,而且速度也快。有时候要多注意题目的解释或者设定,一些总体的要求和判断可以先进行,省去很多不必要的情况。
给定一个非负整数数组 A,如果该数组每对相邻元素之和是一个完全平方数,则称这一数组为正方形数组。
返回 A 的正方形排列的数目。两个排列 A1 和 A2 不同的充要条件是存在某个索引 i,使得 A1[i] != A2[i]。
示例 1:
输入:[1,17,8]
输出:2
解释:
[1,8,17] 和 [17,8,1] 都是有效的排列。
示例 2:
输入:[2,2,2]
输出:1
说明:若一个数能表示成某个整数的平方的形式,则称这个数为完全平方数。完全平方数是非负数。
首先要想到建构什么样子的 数据结构,考虑到相邻两个数字都是平方安全数,可以建立图模型,如果符合条件则两点之间有点线,几种排序方式就变成了有几种全图遍历不重复的情况。
不能计算重复的情况,因此建立了map和node分开来记录。
class Solution {
public:
//是否平方数
bool isValid(int n)
{
int t=sqrt(n);
return t*t==n;
}
//顶点度,邻接表,每个顶点的出现的次数,当前遍历的下标,目前遍历了几个数,一共有几个数,答案
void dfs(const vector<int>& nodes,const vector<vector<int>>& ans,map<int,int>& count,int i,int m, int N,int &res)
{
//都遍历完成
if(m==N)
{
res++;
return;
}
for(auto j:ans[i])
{
if(count[nodes[j]]>0)
{
--count[nodes[j]];
dfs(nodes,ans,count,j,m+1,N,res);
++count[nodes[j]];
}
}
}
int numSquarefulPerms(vector<int>& A) {
//可能存在重复元素
map<int,int> count;
for(auto i:A)
{
count[i]++;
}
//建立节点
vector<int> nodes;
for (auto& p : count) {
nodes.push_back(p.first);
}
int N=nodes.size();
//邻接图
vector<vector<int>> ans(N);
for(int i=0;i<N;i++)
{
//如果自己多个元素,则有条自环
if(count[nodes[i]]>1&&isValid(nodes[i]*2))
{
ans[i].push_back(i);
}
//满足条件,则i与j直接有一条线
for(int j=i+1;j<N;j++)
{
if(isValid(nodes[i]+nodes[j]))
{
ans[i].push_back(j);
ans[j].push_back(i);
}
}
}
int res=0;
for(int i=0;i<N;i++)
{
--count[nodes[i]];
dfs(nodes,ans,count,i,1,A.size(),res);
++count[nodes[i]];
}
return res;
}
};
在去除重复情况的时候,考虑使用map来记录重复元素出现的个数。
————————————————————————————
即使知道了回溯和递归非常经典的解题套路和常用方法之后,有时题目一变化就不知道该如何设计函数和所需参数,怎么来更高效率的实现,尽量简化函数参数。有时需要结合不同的数据结构,建立怎样合适高效的数据结构也很重要。(;′⌒`)