1 分割回文串
LeetCode:分割回文串
分割回文串完全可以想象成一个组合问题,即在哪些位置下刀切开字符串。
理解这一问题的关键在于,需要明白回溯的意义,我们应该进行有意义的探索,因此需要先对子字符串进行回文判断,再进行深一步的搜索与剪枝。
其中类似于KMP问题,这里也可以对字符串进行事先的预处理,利用动态规划的思想:[i,j]为回文串等价于[i+1,j-1]为回文串+s[i]==s[j]。因此实现设立最初的起点(i最大,j最小),进行动态规划。
class Solution {
public:
vector<vector<string>> result;
vector<string> path;
vector<vector<bool>> isPalindrome;
void computePalindrome(string& s)
{
//重新初始化
isPalindrome.resize(s.size(), vector<bool>(s.size(), false));
//[i,j]是回文等价于[i+1,j-1]是回文&&s[i]==s[j]
//因此需要从最大的i和最小j开始,进行动态规划,逐步推进
for(int i=s.size()-1;i>=0;i--)
{
for(int j=i;j<s.size();j++)
{
//1个字符的情况下
if(i==j)
isPalindrome[i][j]=true;
//2个字符的情况下
else if(j-i==1)
isPalindrome[i][j]=(s[i]==s[j]);
//字符更多的情况可以拆解为[i+1,j-1]进行分析,因为i从大到小,j从小到大,所以是s[i+1][j-1]必然已经被算过了
else
isPalindrome[i][j] = ( s[i]==s[j] && isPalindrome[i+1][j-1]);
}
}
}
void backTrack(string& s,int startIndex)
{
if(startIndex>=s.size())
{
result.push_back(path);
return;
}
for(int i=startIndex;i<s.size();i++)
{
//[start,i]是回文子串
if(isPalindrome[startIndex][i])
{
string str=s.substr(startIndex,i-startIndex+1);
path.push_back(str);
//回溯
backTrack(s,i+1);
path.pop_back();
}
}
}
vector<vector<string>> partition(string s) {
computePalindrome(s);
backTrack(s,0);
return result;
}
};
2 复原IP地址
LeetCode:复原IP地址
与上述的字符串分割一样的思路,但因为对于分割的次数有所限制,所以应该在邻近分割上限时,将最后一刀直接切到末尾。
其余就是一些琐碎的合法检测。
class Solution {
public:
vector<string> result;
vector<string> path;
bool isValid(string& s,int start,int end)
{
//4位及以上排除
if(end-start>=3)return false;
//1位都是允许的
if(end==start)return true;
//接下来考虑2/3位
//首位为0排除
if(s[start]=='0')return false;
//此时2位都是允许的
if(end-start==1)return true;
//考虑3位
//300以上排除
if(s[start]>'2')return false;
//100-199ok
if(s[start]=='1')return true;
//只剩200+的情况
//260以上排除
if(s[start+1]>'5')return false;
//256-259排除
if(s[start+1]=='5' && s[start+2]>='6')return false;
//剩下全是200-255之间
return true;
}
void backTrack(string& s,int startIndex,int num_slice)
{
if(num_slice==4)
{
string validIP=path[0];
for(int i=1;i<=3;i++)
{
validIP+="."+path[i];
}
result.push_back(validIP);
return;
}
for(int i=startIndex;i<s.size();++i)
{
//已经分了3段就必须直接到末尾
if(num_slice==3)
i=s.size()-1;
//[startIndex,i]符合条件
if(isValid(s,startIndex,i))
{
path.push_back(s.substr(startIndex,i-startIndex+1));
backTrack(s,i+1,num_slice+1);
path.pop_back();
}
}
}
vector<string> restoreIpAddresses(string s) {
result.clear();
path.clear();
backTrack(s,0,0);
return result;
}
};
3 子集
LeetCode:子集
子集问题区别于组合与排序最大的不同是:子集问题收集的是所有节点上的结果,而排列与组合问题收集的是叶子上的结果。
这就意味着,我们应该在每一次合法的回溯的最开始,直接记录下当前的路径结果,以防止漏过未选节点时的结果。
与此同时,还有另外一种思路,即根据选与不选的2^N种选择进行递归,避开循环。
思路1:记录当前路径结果
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backTrack(vector<int>& nums,int startIndex)
{
result.push_back(path);
for(int i=startIndex;i<nums.size();i++)
{
path.push_back(nums[i]);
backTrack(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsets(vector<int>& nums) {
backTrack(nums,0);
return result;
}
};
思路2:根据选与不选递归
class Solution {
public:
vector<vector<int>> result;
vector<int> subset;
void backTrack(vector<int>& nums,int startIndex)
{
if(startIndex>=nums.size())
{
result.push_back(subset);
return;
}
//不选
backTrack(nums,startIndex+1);
//选
subset.push_back(nums[startIndex]);
backTrack(nums,startIndex+1);
subset.pop_back();
}
vector<vector<int>> subsets(vector<int>& nums) {
backTrack(nums,0);
return result;
}
};
4 子集II
LeetCode:子集II
因为有重复元素,直接利用排序+剪枝的思路即可,依然是在树层上进行去重。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backTrack(vector<int>& nums,int startIndex)
{
result.push_back(path);
for(int i=startIndex;i<nums.size();i++)
{
//剪枝
if(i>startIndex && nums[i-1]==nums[i])
continue;
path.push_back(nums[i]);
backTrack(nums,i+1);
path.pop_back();
}
}
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
//排序,方便剪枝
sort(nums.begin(),nums.end());
backTrack(nums,0);
return result;
}
};
5 递增子序列
LeetCode:递增子序列
因为本体不宜对原数组进行排序修改,因此直接利用used数组对树层进行记录,判断该元素是在某一位置上已经被使用过。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
bool isUsed(vector<int>& used,int k)
{
for(int i=0;i<used.size();i++)
{
if(used[i]==k)return true;
}
return false;
}
void backTrack(vector<int>& nums,int startIndex)
{
if(path.size()>=2)
result.push_back(path);
if(startIndex>=nums.size())
return;
//使用used数组记录每一层选过的数字
vector<int> used;
for(int i=startIndex;i<nums.size();++i)
{
if(isUsed(used,nums[i]))
continue;
if(path.size()==0 || nums[i]>=path[path.size()-1])
{
path.push_back(nums[i]);
backTrack(nums,i+1);
path.pop_back();
used.push_back(nums[i]);
}
}
}
vector<vector<int>> findSubsequences(vector<int>& nums) {
backTrack(nums,0);
return result;
}
};
6 全排列
LeetCode:全排列
排列问题相比于组合问题,在于不同顺序是一致的,这代表着每一轮都应该从最初的元素进行判断与选择,并使用usedIndex记录不同层的使用情况。
组合问题因为顺序无所谓,反而可以利用数组起始坐标的改变,从而控制已出现的元素不必再出现,即靠前的元素没有后出现的道理(无序性)。
理论上使用bool应该会更高效,但无所谓了。
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
bool isUsed(vector<int>& usedIndex,int index)
{
for(int i=0;i<usedIndex.size();i++)
{
if(index==usedIndex[i])return true;
}
return false;
}
void backTrack(vector<int>& nums,vector<int>& usedIndex)
{
if(path.size()==nums.size())
{
result.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(isUsed(usedIndex,i))
continue;
path.push_back(nums[i]);
usedIndex.push_back(i);
backTrack(nums,usedIndex);
path.pop_back();
usedIndex.pop_back();
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> usedIndex;
backTrack(nums,usedIndex);
return result;
}
};
7 全排列II
LeetCode:全排列II
因为出现了重复元素的数组,因此针对排列问题的不同层使用usedIndex进行记录,而针对重复元素利用排序+剪枝进行去重。
这一类问题大体上都可以判断为:排列问题不重复(不同层间的记录)+相同元素的去重(同树层间的跳过)
class Solution {
public:
vector<vector<int>> result;
vector<int> path;
void backTrack(vector<int>& nums,vector<bool>& indexUsed)
{
if(path.size()==nums.size())
{
result.push_back(path);
return;
}
for(int i=0;i<nums.size();i++)
{
if(indexUsed[i]==true)
continue;
if(i>0 && nums[i]==nums[i-1] && indexUsed[i-1]==false)
continue;
path.push_back(nums[i]);
indexUsed[i]=true;
backTrack(nums,indexUsed);
path.pop_back();
indexUsed[i]=false;
}
}
vector<vector<int>> permuteUnique(vector<int>& nums) {
vector<bool> indexUsed(nums.size(),false);
sort(nums.begin(),nums.end());
backTrack(nums,indexUsed);
return result;
}
};
8 总结
感觉回溯问题似乎因为树的原因,没那么难,理解起来还是很快的,不过可能因为都是easy和mid题的缘故吧。
反省一下自己,昨天那个GAN的博客完全没有任何意义,以后绝对不会浪费时间写意义不明的Markdown了,要抓紧时间学习新的知识。
——2023.2.24