文章目录
1、组合总和
题目
分析
回溯
这题需要使用回溯的方法来解题,对这个所谓回溯算法,我个人的理解是依次尝试,要配合递归,在递归前把数据放入vector,递归结束后再将加入vector的数据取出,然后进行下一次循环。
剪枝
这里还用到了剪枝,即在递归前先判断下当前的数是否还符合条件,如不符合就直接跳过,这样可以省去一些步骤。
去重
还有一个问题就要去重,比如如果candidates中包含2、3,就会同时出现{2,3}和{3,2},这就造成了重复,而我们现在以不减的顺序进行探测,这就避免了重复发生。
代码如下:
void dfs(const vector<int>& candidates,vector<vector<int>> &ret,vector<int> &a,
int pos,const int &n,int cur,const int &target){
if(pos>=n||cur>target) return;
if(target==cur){
ret.push_back(a);
return;
}
for(int i=pos;i<n;++i){
a.push_back(candidates[i]);
dfs(candidates,ret,a,i,n,cur+candidates[i],target);
a.pop_back();
}
return;
}
复杂度
时间复杂度:O(n^2)
空间复杂度:O(n)
2、全排列&第K个排列
题目
分析
全排列
这题可以说是回溯算法的教材。
主函数中,将每次遍历的头部数先确定好,book记录用过的数
for(int i=0;i<n;++i){
vector<int> ret;
book[i]=true;
ret.push_back(nums[i]);
dfs(nums,book,ret);
book[i]=false;
}
深度遍历
void dfs(vector<int>& nums,int *book,vector<int> &ret){
if(ret.size()==nums.size()){
out.push_back(ret);
return;
}
for(int i=0;i<nums.size();++i){
if(book[i]==true) continue;
ret.push_back(nums[i]);
book[i]=true;
dfs(nums,book,ret);
ret.pop_back();
book[i]=false;
}
return;
}
注:其实上面主函数中的遍也可以写进dfs中。
第K个排列
参考了link
这题的解是基于上一题,是进阶的一道题,这题的难点在于要明白剪枝该怎样处理,如下所示:
因为题目中说“给定 n 的范围是 [1, 9]”,故这里的做法是在一开始就将 0 到 9的阶乘计算好,放在一个数组里,后面直接取用即可,如下:
int* fac=new int[n+1];
memset(fac,0,sizeof(fac));
fac[0]=1;
for(int i=1;i<n+1;++i){
fac[i]=fac[i-1]*i;
}
这里注意 fac[0]=1 这步操作,它表示了没有数可选的时候,即表示到达叶子结点了,排列数只剩下 1 个,例如n=1,k=1时。
随后进行DFS遍历,如下:
void dfs(int cur,int n,int k,int *fac,vector<int>& used,vector<int>&count){
if(cur==n) return;
//还未确定的数字的全排列的个数,第 1 次进入的时候是 n - 1
int cnt=fac[n-1-cur];
for(int i=1;i<n+1;++i){
if(used[i]) continue;
if(cnt<k){
k-=cnt;
continue;
}
count.push_back(i);
used[i]=true;
dfs(cur+1,n,k,fac,used,count);
}
}
这里cur从0开始,注意这里之所以能够以 if(cur==n) return; 作为结束判断的原因在于剪枝过程已经将前k-1个排列都给剪掉了。
cnt是当前节点的一个子分支的总叶子数,因为刚进入时是头结点,所以这里cnt一开始取 fac[n-1] 。
3、子集
题目
分析
这题算是上面全排列的一种变体,不需要使用book数组,而是从左往右进行遍历,如下:
vector<vector<int>> subsets(vector<int>& nums) {
int n=nums.size();
vector<vector<int>> ret;
vector<int> a;
dfs(nums,ret,a,0,n);
return ret;
}
void dfs(vector<int>& nums,vector<vector<int>> &ret,vector<int> &a,int pos,int n){
ret.push_back(a);
if(pos>=n) return;
for(int i=pos;i<n;++i){
a.push_back(nums[i]);
dfs(nums,ret,a,i+1,n);
a.pop_back();
}
return;
}
这里注意下要在遍历的一开始就将a装进ret中。
4、分割回文串
题目
分析
参考:link
回归树如下所示:
步骤:
1)将字符串截取一段a
2)判断a是否是回文串,如果是就将剩下的部分进行下一次递归
如下:
void dfs(vector<vector<string>> &ret,vector<string>& a,const string &s,int pos,const int &n,const vector<vector<int>> &dp){
if(pos>=n){
ret.push_back(a);
return;
}
for(int i=pos;i<n;++i){
if(isPalindrome(b)){
string b=s.substr(pos,i-pos+1);
a.push_back(b);
dfs(ret,a,s,i+1,n,dp);
a.pop_back();
}
}
return;
}
这里的结束条件是已截取的前缀部分的长度等于原s的长度,所以要用pos和n来分别记录当前的位置和s的总长度。
优化:
这里有种空间换时间的优化算法,上述解法中的 isPalindrome 是从两边往中间遍历检查是否为回文串。
基于LK第五题,我们可以先用一个dp数组把所有的回文串给记录下来,如下:
vector<vector<string>> partition(string s) {
int n=s.size();
vector<vector<string>> ret;
vector<string> a;
vector<vector<int>> dp(n,vector<int>(n,0));
for(int i=0;i<n;++i) dp[i][i]=1;
for(int i=0;i<n;++i){
for(int j=0;j<i;++j){
if(s[i]==s[j]){
if(i==j+1){
dp[i][j]=1;
continue;
}
if(dp[i+1][j-1]==1) dp[i][j]=1;
else dp[i][j]=-1;
}
}
}
dfs(ret,a,s,0,n,dp);
return ret;
}
这是isPalindrome 可以替换为
if(dp[pos][i]==1)
5、复原IP地址
题目
分析
代码如下:
void dfs(string s,vector<string> &ret,string ssub,int pos,int count){
if(count==4) {
if(pos==s.size()){
ret.push_back(ssub);
}
return;
}
string ss;
++count;
for(int i=1;i<4;++i){
if(pos+i>s.size()) break;
string b=ssub;
ss=s.substr(pos,i);
if(i>1&&ss[0]=='0') break;
int ssnum=atoi(ss.c_str());
if(ssnum>255) break;
b+=ss;
if(count<4) b+='.';
dfs(s,ret,b,pos+i,count);
}
return;
}
关于这题,因为IP地址要分成四层,然后每层的子串最长为3,所以这里维护了一个count用来记录遍历的层数,当 count=4 时遍历结束,检查当前位置是否到了s的末尾以判断 ssub 是否有效。
在遍历过程中要注意三种不成立的情况,即:
if(pos+i>s.size()) break;
if(i>1&&ss[0]=='0') break;
if(ssnum>255) break;
第一个和第三个好理解,这里来说下第二种,当 s 为 “010010” 时,如果 ss=“010” 也是无效的,但 ‘0’ 是可以的,所以这里还要判断 i>1。
同时注意,第一个中必须是大于,如,考虑pos现在指向最后一个位置,然后 i 为1时刚好得出一个可行解。
6、括号生成
题目
分析
这里我想的是使用动态规划来做,但是对于转移方程没什么思路。
看了解析,发现使用DFS和BFS更好。但这里需要对题目做个简单的数学建模,就是维护两个数,left和right分别表示左括号和右括号数目,如下图所示:
void dfs(vector<string> &out,string s,int left,int right){
//剪枝
if(left>right) return;
if(right==0){
out.push_back(s);
return;
}
else if(left==0){
string s1=s;
while(right>0){
s1+=')';
--right;
}
out.push_back(s1);
return;
}
else{
string s1,s2;
s1=s+')';
dfs(out,s1,left,right-1);
s2=s+'(';
dfs(out,s2,left-1,right);
}
}
而使用DP法的话也可以,转移方程如下:
复杂度
时间复杂度:O(n^2)
空间复杂度:O(n)
7、单词拆分II
题目
分析
参考:link
这里前面的步骤与单词拆分相同,在将dp数组处理完成后就开始进行DFS了,具体分析要结合图和实例才好懂,这里直接上代码了,
vector<string> wordBreak(string s, vector<string>& wordDict) {
int n=s.size();
unordered_set<string> books;
for(auto a:wordDict) books.insert(a);
vector<int> dp(n+1,0);
dp[0]=1;
for(int i=1;i<=n;++i){
for(int j=i-1;j>=0;--j){
string b=s.substr(j,i-j);
if(dp[j]&&books.count(b)){
dp[i]=1;
break;
}
}
}
if(dp[n]==0) return {};
vector<string> ret;
string a;
dfs(s,dp,books,ret,n,a);
return ret;
}
void dfs(const string& s,const vector<int> &dp,const unordered_set<string> &books,vector<string> &ret,int pos,string &a){
if(pos<=0){
a.pop_back();
ret.push_back(a);
return;
}
for(int i=0;i<pos;++i){
if(dp[i]&&books.count(s.substr(i,pos-i))){
string b=s.substr(i,pos-i)+' '+a;
dfs(s,dp,books,ret,i,b);
}
}
return;
}