最大子序和 53
给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
题目要求实现复杂度O(n)
的解法,尝试使用更为精妙的分治法求解。
复杂度O(n)
的做法是,设两个变量cursum
和res
,前者存放当前和的最大者,即之前的累加值加上当前位置的值,和当前位置的值之间的较大值。res
存放当前序列的最大序列和的值。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
if(nums.empty())
return 0;
int cursum = 0;
int res = nums[0];
for(size_t i = 0; i < nums.size(); ++i) {
cursum = max(cursum+nums[i], nums[i]);
res = max(cursum, res);
}
return res;
}
};
分治法的复杂度为O(nlogn)
,类似二分查找,每次将序列二分,找出左右两边的最大序列和值,再从中间值向左右两边找出中间序列和的最大值,三者取最大值。
class Solution {
public:
int maxSubArray(vector<int>& nums) {
return helper(nums, 0, nums.size()-1);
}
int helper(vector<int>& nums, int left, int right) {
int mid = left + ((right-left)>>1);
if(left >= right)
return nums[left];
int lmax = helper(nums, left, mid-1);
int rmax = helper(nums, mid+1, right);
int mmax = nums[mid], t = mmax;
for(int i = mid-1; i >= left; --i) {
t += nums[i]; // 以中间值向左查找连续的序列和,把最大序列和赋给mmax
if(mmax < t) mmax = t;
}
t = mmax;
for(int i = mid+1; i <= right; ++i) {
t += nums[i]; // 以中间值向右查找连续的序列和,把最大序列和赋给mmax
if(mmax < t) mmax = t;
}
return max(mmax, max(lmax, rmax));
}
};
注意:只要循环中用到--
,不要使用size_t
类型,如果到i=0
了,下一步就溢出了!!!
for(size_t i = mid-1/*1*/; i >= left; --i/*2*/) {/*...*/}
//第1个位置和第2个位置都有可能溢出。
冗余连接 II 685
给定一个有向图,删除一条边,使之成为一颗树,如果有多个答案,选择删除最后一条边。
>> 思路
- 判断一个图是否有环,可以采用并查集(union find)。
- 成为树的必要条件是除根结点外,每个结点的入度为1。
因此,可以分为3种情况:
各结点入度均<=1,但是有环
1 / ^ v \ 2--> 3
有结点入度为2,无环
1 \ v 2-->3
有结点入度为2,有环
1 / \ v v 2 -->3
class Solution {
public:
vector<int> findRedundantDirectedConnection(vector<vector<int>>& edges) {
int n = edges.size();
vector<int> root(n + 1, 0), first, second;
for (auto& edge : edges) { // 先查找是否有入度为2的点,把前一个边保存在first,后一个边
// 保存在second。并标记后一条边,保证后一条边不在环内
if (root[edge[1]] == 0) {
root[edge[1]] = edge[0];
} else {
first = {root[edge[1]], edge[1]};
second = edge;
edge[1] = 0;
}
}
for (int i = 0; i <= n; ++i) root[i] = i;
for (auto& edge : edges) {
if (edge[1] == 0) continue; // 如果没有这个,且后一条边在环内,就会导致下面返回错误边
int x = getRoot(root, edge[0]), y = getRoot(root, edge[1]);
if (x == y) return first.empty() ? edge : first;
root[x] = y;
}
return second;
}
int getRoot(vector<int>& root, int i) {
return i == root[i] ? i : getRoot(root, root[i]);
}
};
三数之和 15
给定一个长度为
n
的整数数组nums
,找出其中3个数之和为0的三元组。不包括重复的三元组。
>> 思路
求三元组,可以以一个数作为fix
值,查找另外两个数之和为其相反数。对于已排序的序列,查找两个数之和可以用两个指针,一左一右,向中间夹逼来查找。每次查找到了,要过滤重复的数。每次设定一个fix值也要过滤重复值。
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
vector<vector<int>> res;
if(nums.size() < 3)
return res;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size()-2; ++k) {
if(nums[k] > 0) break; // 剪枝
if(k > 0 && nums[k] == nums[k-1]) continue; // 过滤重复的fix值
int target = -nums[k];
int i = k+1, j = nums.size()-1;
while(i < j) {
int sum = nums[i]+nums[j];
if(sum == target) {
res.push_back({nums[k], nums[i], nums[j]});
while(i < j && nums[++i] == nums[i-1]); // 过滤重复的数
while(i < j && nums[--j] == nums[j+1]); // 过滤重复的数
} else if(sum > target) {
--j;
} else
++i;
}
}
return res;
}
};
最接近的三数之和 16
给定一个长度为n的整数数组nums,和一个目标值target,查找最接近target的三元组之和。
>> 思路
思路和上面的 三数之和 类似,只不过上面查找的是固定值,这里查找的是一个接近target的数,用一个变量来存储目前已查找的和中最接近的一个。
class Solution {
public:
int threeSumClosest(vector<int>& nums, int target) {
if(nums.size() < 3)
return 0;
int res = nums[0]+nums[1]+nums[2];
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size(); ++k) {
if(k > 0 && nums[k] == nums[k-1]) continue;
int i = k+1, j = nums.size()-1;
while(i < j) {
int sum = nums[k]+nums[i]+nums[j];
if(abs(res-target) > abs(target-sum))
res = sum;
if(sum == target)
return sum;
else if(sum < target) {
while(i < j && nums[++i] == nums[i-1]); // 过滤重复值
} else {
while(i < j && nums[--j] == nums[j+1]); // 过滤重复值
}
}
}
return res;
}
};
四数之和 18
给定一个长度为n的整数数组nums,和一个目标值target。求和为target的四元组。
>> 思路
思路和 3数之和 一样,只是多添加了一层循环。复杂度O(n^3):
// 20ms
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
if(nums.size() < 4)
return res;
sort(nums.begin(), nums.end());
for(int k = 0; k < nums.size()-3; ++k) {
if(k> 0 && nums[k] == nums[k-1]) continue; // 判断是否重复
for(int g = k+1; g < nums.size(); ++g) {
if(target < nums[k]+nums[g]) break;
if(g > k+1 && nums[g] == nums[g-1]) continue; // 判断是否重复
int temp = target-nums[k]-nums[g];
int i = g+1, j = nums.size()-1;
while(i < j) {
int sum = nums[i] + nums[j];
if(sum == temp) {
res.push_back({nums[k], nums[g], nums[i], nums[j]});
while(i < j && nums[++i] == nums[i-1]);
while(i < j && nums[--j] == nums[j+1]);
} else if(sum < temp)
++i;
else
--j;
}
}
}
return res;
}
};
// 8ms 别人的代码
class Solution {
public:
vector<vector<int>> fourSum(vector<int>& nums, int target) {
vector<vector<int>> res;
int size = nums.size();
if(size < 4)
return res;
sort(nums.begin(), nums.end());
for(int i = 0; i < size - 3; ++i) // 外循环,四数之和转化为三数之和
{
if(i > 0 && nums[i] == nums[i - 1]) // 如果两个数相等,去重
continue;
if(nums[i] + nums[size-1] + nums[size-2] + nums[size-3] < target) // 由于有序,如果当前数加最后三个数都比target小,那肯定不符合
continue;
if(nums[i] + nums[i+1] + nums[i+2] + nums[i+3] > target) // 如果连续四个数都比target大,后面肯定没有符合要求的情况,直接return
return res;
for(int j = i + 1; j < size - 2; ++j) // 内循环三数之和(从第二个数开始,后面每个数都是从前面一个数的后面开始遍历)
{
if(j > i + 1 && nums[j] == nums[j - 1]) // 如果两个数相等,去重
continue;
if(nums[i] + nums[j] + nums[size-1] + nums[size-2] < target) // 前两个数确定,加上最后两个数都比target小,那肯定不符合
continue;
if(nums[i] + nums[j] + nums[j+1] + nums[j+2] > target) // 前两个数确定,如果后面连续两个数和比target大,不符合条件
break;
int left = j + 1, right = size - 1;
while(left < right) // 两数之和
{
int sum = nums[i] + nums[j] + nums[left] + nums[right];
if(sum == target)
{
res.push_back({nums[i], nums[j], nums[left], nums[right]});
++left;
while(left < right && nums[left] == nums[left - 1])
++left;
--right;
while(left < right && nums[right] == nums[right + 1])
--right;
}
else if(sum > target)
--right;
else
++left;
}
}
}
return res;
}
};
四数之和II 454
>> 思路
分别将两个数组合并为一个,再通过
class Solution {
public:
int fourSumCount(vector<int>& A, vector<int>& B, vector<int>& C, vector<int>& D) {
int res = 0;
if(A.size() == 0 || B.size() == 0|| C.size() == 0 ||D.size() == 0)
return 0;
unordered_map<int, int> AB; // 存放A、B数组和的值及其数量
unordered_map<int, int> CD;
unordered_map<int, int> Am; // 存放A数组的元素及其数量
unordered_map<int, int> Bm;
unordered_map<int, int> Cm;
unordered_map<int, int> Dm;
for(int i = 0; i < A.size(); ++i)
++Am[A[i]];
for(int i = 0; i < B.size(); ++i)
++Bm[B[i]];
for(int i = 0; i < C.size(); ++i)
++Cm[C[i]];
for(int i = 0; i < D.size(); ++i)
++Dm[D[i]];
for(auto &a :Am)
for(auto &b: Bm)
AB[a.first+b.first] += a.second*b.second;
for(auto &c :Cm)
for(auto &d: Dm)
CD[c.first+d.first] += c.second*d.second;
for(auto &ab: AB) { // 根据相反数查找
res += ab.second * CD[-ab.first];
}
return res;
}
};
最长非共同序列 II 522
给定字符串列表,从中找出最长的子序列,它只需要一个字符串,子序列可以通过删除字符串中的某些字符实现,但不能改变剩余字符的相对顺序。空序列为所有字符串的子序列,任何字符串为其自身的子序列。
输出最长非共同子序列的长度。不存在返回-1.
>> 思路
字符串列表中存在重复的字符串,所以,先用map统计所有字符串的出现次数,最长非共同子序列必将存在于只出现一次的字符串中,因为要查找的子序列是最长的,所以,必然是某个字符串自身。接着,查找每个只出现一次的字符串,并且它不需要出现多次的字符串的子序列。
class Solution {
public:
int findLUSlength(vector<string>& strs) {
int maxn = -1;
unordered_map<string, int> Set;
for(auto &str: strs) {
++Set[str];
}
vector<string> vals;
for(auto &elem: Set) {
if(elem.second > 1)
vals.push_back(elem.first);
}
for(auto &elem: Set) {
int size = elem.first.size();
if(elem.second == 1 && !ismatch(vals, elem.first)) {
if(size > maxn)
maxn = size;
}
}
return maxn;
}
bool ismatch(vector<string>& strs, string s) { // 是否属于出现多次的字符串的子序列
for(auto& str: strs) {
if(str.size() < s.size())
continue;
int j = 0;
for(int i = 0; i < str.size() && j < s.size(); ) {
if(str[i] == s[j]) {
++i;
++j;
} else
++i;
}
if(j == s.size())
return true;
}
return false;
}
};
解法2:
思路和上面相同,在求子序列是否匹配时,采用了动态规划思想:
// 这种解法只是凑巧避开了某些测试用例,造成执行很快(0ms)的假象。下面我已经改正。
// 比如["abbbbcd","abbbbcd","fdgk", "gk", "fdgk", "p"],这种解法就错误了,因为`fdgk`是重复的,
//但是不是最长字符串,所以,在匹配子序列时,就漏掉了,导致结果为`2`(gk)。其实应该是`1`(p)。改正后,
//测试结果为12ms,比我上面的(4ms)慢多了。
class Solution {
public:
int findLUSlength(vector<string>& strs) {
map<int, unordered_map<string, int>, greater<int>> m;
for(auto s: strs)++m[s.size()][s]; // 以字符串长度作为键
auto um = m.cbegin()->second; // 长度最大的字符串集合
for(auto p: um) // 如果其中有只出现一次的字符串,就返回它的长度
if(p.second == 1)return m.cbegin()->first;
bool b;
auto it = m.cbegin();
for(++it; it != m.cend(); ++it)
for(auto pp: it->second){
um[pp.first] = pp.second; // 这句是我添加的,如果不添加,它每次只匹配是否为最长
//的字符串的子序列,而实际上应该匹配是否是所有重复的字符串的子序列
if(pp.second != 1)continue;
b = true;
for(auto p: um)
if(lcs(p.first, pp.first)){
b = false;
break;
}
if(b)return it->first;
}
return -1;
}
private:
bool lcs(string A, string B){
int i, j, m = A.size(), n = B.size(), dp[m + 1][n + 1] = {};
for(i = 0; i < m; ++i)
for(j = 0; j < n; ++j)
if(A[i] == B[j])dp[i + 1][j + 1] = dp[i][j] + 1;
else dp[i + 1][j + 1] = max(dp[i][j + 1], dp[i + 1][j]);
return dp[m][n] == n;
}
};
子集 78
给定一组不含重复元素的整数数组nums,返回该数组所有子集包括空集。
解法1:
>> 思路
先生成长度为1的子集,再用该子集生成长度2的子集,以此类推,直到所有子集生成完成。
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
vector<vector<int> > res;
if(nums.empty())
return res;
unordered_map<int, int> pos; // 保存各个数字的位置,方便查找
unordered_map<int, vector<vector<int> > > mp; // 子集长度和相应集合的映射
for(int i = 0; i < nums.size(); ++i) {
pos[nums[i]] = i;
mp[1].push_back({nums[i]});
}
int cnt = 1;
for(int cnt = 1; cnt < nums.size(); ++cnt) {
auto vals = mp[cnt];
for(auto &val: vals) {
int i = pos[val[val.size()-1]];
for(++i; i < nums.size(); ++i) {
vector<int> next = val;
next.push_back(nums[i]);
mp[cnt+1].push_back(next);
}
}
}
res.push_back({});
for(auto& elem :mp) {
auto vals = elem.second;
for(auto &val : vals)
res.push_back(val);
}
// res.push_back(nums);
return res;
}
};
解法2:
// 使用位运算思想!!
class Solution {
public:
vector<vector<int>> subsets(vector<int>& nums) {
const int len = nums.size();
int set_num = 1 << len;
vector<vector<int>> res;
vector<int> t;
for(int i = 0;i < set_num;i++)
{
t.clear();
// bitset<32> bs(i);
// string s = bs.to_string();
// for(int j = 0;j< len;j++)
// if(s[31-j] == '1')
// t.push_back(nums[j]);
int idx = 0;
for(int tmp = i;tmp > 0;tmp >>= 1)
{
if((tmp & 1) == 1)
t.push_back(nums[idx]);
idx ++;
}
res.push_back(t);
}
return res;
}
};
验证二叉搜索树 98
给定一个二叉树,判断是否是一个有效的二叉搜索树。
class Solution {
public:
bool isValidBST(TreeNode *root) {
return isValidBST(root, LONG_MIN, LONG_MAX);
}
bool isValidBST(TreeNode *root, long mn, long mx) {
if (!root) return true;
if (root->val <= mn || root->val >= mx) return false;
return isValidBST(root->left, mn, root->val) && isValidBST(root->right, root->val, mx);
}
};
// 对于每个结点,它要比较的值是它左边的值和右边的值(左< <右)
// 对于左结点,它的左边的值应该是它的祖父结点的值,右边的值应该是它的父结点的值
// 对于右结点,它的左边的值应该是它的父结点的值,右边的值应该是它的祖父结点的值
// 所以递归时,只要改变一边的值,isValidBST(root->left, mn, root->val)
二叉树的中序遍历
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
helper(root, res);
return res;
}
void helper(TreeNode*root, vector<int>& res) {
if(root == nullptr)
return;
helper(root->left, res);
res.push_back(root->val);
helper(root->right, res);
}
};
解法2:
// 非递归
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
if(!root) return res;
stack<TreeNode*> stk;
TreeNode* p = root;
while(p || stk.size()) {
while(p != nullptr) {
stk.push(p);
p = p->left;
}
p = stk.top(); stk.pop();
res.push_back(p->val);
p = p->right;
}
return res;
}
};
解法3:
// 非递归,先查找左,左为空了,再查找右
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode *p = root;
while (!s.empty() || p) {
if (p) {
s.push(p);
p = p->left;
} else {
TreeNode *t = s.top(); s.pop();
res.push_back(t->val);
p = t->right;
}
}
return res;
}
};
// 上面的代码也可以作为先序遍历的非递归代码
vector<int> perorderTraversal(TreeNode* root) {
vector<int> res;
stack<TreeNode*> s;
TreeNode *p = root;
while (!s.empty() || p) {
if (p) {
res.push_back(p->val); // 改为在这插入
s.push(p);
p = p->left;
} else {
TreeNode *t = s.top(); s.pop();
p = t->right;
}
}
return res;
}
解法3:
空间复杂度为O(1)的Morris遍历,它使用一种叫做线索二叉树(threaded binary tree)的方法,即原先的二叉树的原本为空的右子结点指向中序遍历之后的那个结点,把所有原本为空的左子结点指向中序遍历之前的那个结点。
算法步骤:
- 判断当前结点
cur
的左结点是否为空,是则打印当前结点,将右结点cur->right
作为当前结点。 - 否则判断当前结点的左子树的最右结点是否为空,是则赋值为当前结点
cur
,将左结点cur->left
作为当前结点。 - 如果左子树的最右结点不为空,则赋值为空(恢复原值),将右结点
cur->right
作为当前结点。 - 重复1,2,直到当前结点为空。
vector<int> inorderTraversal(TreeNode* root) {
TreeNode* cur = root;
vector<int> res;
while(cur != nullptr) {
if(!cur->left) {
res.push_back(cur->val);
cur = cur->right;
} else {
TreeNode* pre = cur->left;
while(pre->right && pre->right != cur) pre = pre->right;
if(!pre->right) {
pre->right = cur;
cur = cur->left;
} else {
pre->right = nullptr;
res.push_back(cur->val);
cur = cur->right;
}
}
}
return res;
}
如果是前序遍历只需修改一处,即先打印根结点:
vector<int> preorderTraversal(TreeNode* root) {
TreeNode* cur = root;
vector<int> res;
while(cur != nullptr) {
if(!cur->left) {
res.push_back(cur->val);
cur = cur->right;
} else {
TreeNode* pre = cur->left;
while(pre->right && pre->right != cur) pre = pre->right;
if(!pre->right) {
res.push_back(cur->val); // 在这里打印根结点
pre->right = cur;
cur = cur->left;
} else {
pre->right = nullptr;
cur = cur->right;
}
}
}
return res;
}
二叉树的后序遍历 145
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
if (!root) return {};
vector<int> res;
stack<TreeNode*> s{{root}};
TreeNode *head = root;
while (!s.empty()) {
TreeNode *t = s.top();
// 检查是否为叶子结点,或左结点已经加入或右结点已经加入res
if ((!t->left && !t->right) || t->left == head || t->right == head) {
res.push_back(t->val);
s.pop();
head = t;
} else {
if (t->right) s.push(t->right); // 注意先让右结点入栈,让左结点先处理
if (t->left) s.push(t->left);
// 根结点总是先于左右结点入栈的,所以可以保证根结点最后出栈
}
}
return res;
}
};
恢复二叉搜索树 99
二叉搜索树中的两个结点被错误的交换了。在不改变其结构的情况下,恢复这棵树。
>> 思路
我们知道,对于正确的二叉搜索树,中序遍历得到的序列是升序的,那么我们可以通过中序遍历得到这棵树的升序序列,序列的每个值与它所在的结点关联起来,从中找出错误的两个值,交换它们的结点值。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
void recoverTree(TreeNode* root) {
if(root == nullptr)
return;
inorder(root);
int vali = order[0].first;
int valj = order[order.size()-1].first;
int i = 0, j = order.size()-1;
while(i < j && vali < order[++i].first)
vali = order[i].first;
while(j > 0 && valj > order[--j].first)
valj = order[j].first;
auto t1 = order[i-1].second;
auto t2 = order[j+1].second;
t1->val = order[j+1].first;
t2->val = order[i-1].first;
}
void inorder(TreeNode* root) {
if(root == nullptr)
return;
inorder(root->left);
order.push_back({root->val, root});
inorder(root->right);
}
private:
vector<pair<int, TreeNode*> > order;
};
将有序数组转换为二叉搜索树 108
将一个升序排列的数组,转换为一颗高度平衡二叉树。每个结点的左右子树高度相差的绝对值不超过1.
>> 思路
因为是有序的,所以我想到了中序遍历,而高度平衡的二叉树,我想到了完全二叉树,即将有序数组的值往完全二叉树从上到下填入。所以,先用一个数组来存储完全二叉树的结点指针,再中序遍历出每个结点的顺序,再把数组中的值按顺序填入。
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
int sz = nums.size();
if(nums.empty())
return nullptr;
vector<TreeNode*> nodes(sz, nullptr);
nodes[0] = new TreeNode(0);
for(int i = 0; i < sz/2; ++i) { // 生成完全二叉树
int j = (i+1)*2-1;
if(j < sz) {
nodes[j] = new TreeNode(0);
nodes[i]->left = nodes[j];
}
if(j+1 < sz) {
nodes[j+1] = new TreeNode(0);
nodes[i]->right = nodes[j+1];
}
}
vector<TreeNode*> order;
inorder(nodes[0], order); // 中序遍历
for(int i = 0; i < nums.size(); ++i) // 填入值
order[i]->val = nums[i];
return nodes[0];
}
void inorder(TreeNode* root, vector<TreeNode*>& pos) {
if(root == nullptr)
return;
inorder(root->left, pos);
pos.push_back(root);
inorder(root->right, pos);
}
};
// 别人的代码
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
return buildTree(nums,0,nums.size()-1);
}
TreeNode* buildTree(vector<int>& nums, int left, int right){
if(left>right){
return nullptr;
}
int mid = (left+right)/2;
TreeNode* node = new TreeNode(nums[mid]);
node->left = buildTree(nums,left,mid-1);
node->right = buildTree(nums,mid+1,right);
return node;
}
};
矩阵中的最长递增路径 329
给定一个整数矩阵,找出最长递增路径的长度。
对于每个单元格,你只能上下左右四个方向移动。
>> 思路
这题目很明显应该使用深度优先搜索,为了提高搜索效率,应该把每个访问过的位置的相应最长路径记录起来,再次访问到时,直接返回它的最长路径长度。
注意:因为是要找递增序列,所以同一路径下的每个结点不会被重复访问,所以不需要记录结点是否已被访问。
函数参数一定要使用引用类型,不然有可能爆内存!
class Solution {
public:
/*
vector<vector<int>> dirs = {{0, -1}, {-1, 0}, {0, 1}, {1, 0}};
int longestIncreasingPath(vector<vector<int>>& matrix) {
if (matrix.empty() || matrix[0].empty()) return 0;
int res = 1, m = matrix.size(), n = matrix[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0));
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
res = max(res, dfs(matrix, dp, m, n, i, j));
}
}
return res;
}
int dfs(vector<vector<int>> &matrix, vector<vector<int>> &dp, int i, int j) {
if (dp[i][j]) return dp[i][j];
int mx = 1, m = matrix.size(), n = matrix[0].size();
for (auto a : dirs) {
int x = i + a[0], y = j + a[1];
if (x < 0 || x >= m || y < 0 || y >= n || matrix[x][y] <= matrix[i][j]) continue;
int len = 1 + dfs(matrix, dp, x, y);
mx = max(mx, len);
}
dp[i][j] = mx;
return mx;
}
*/
/**/
int longestIncreasingPath(vector<vector<int>>& matrix) {
//int dp[rows][cols] = {0};
int rows = matrix.size();
if(rows<=0)
return 0;
int cols = matrix[0].size();
if(cols<=0)
return 0;
vector<vector<int>> dp(rows, vector<int>(cols, 0));
int i,j;
int ret = 1;
for(i=0; i<rows; i++) {
for(j=0; j<cols; j++) {
if(dfs(matrix, dp, rows, cols, i, j)>ret)
ret = dp[i][j];
}
}
return ret;
}
int dfs(vector<vector<int>>& m, vector<vector<int>>& d, int rows, int cols, int r, int c) {
if(d[r][c]) return d[r][c];
int mx = 1;
int i,j;
int ro[] = {0, 0, -1, 1}; // 方向
int co[] = {1,-1, 0, 0};
for(int k=0; k<4; k++) {
i = r+ro[k];
j = c+co[k];
if(i<rows && i>=0 && j<cols && j>=0 && m[i][j] > m[r][c]) {
int res = 1+dfs(m,d,rows,cols,i,j);
if(res>mx)
mx = res;
}
}
d[r][c] = mx;
return mx;
}
};
>> 想法
如果它需要返回最长递增的路径呢?
因为我们上面已经得到每个位置上最长路径的长度的记录,所以根据该记录回溯就可以找到最长路径。
void findLoad(vector<int>& load, vector<vector<int>>& matrix, vector<vector<int>>& d, int x, int y) {
int row = matrix.size();
int col = matrix[0].size();
load.emplace_back(matrix[x][y]);
int ro[] = {0, 0, -1, 1}; // 方向
int co[] = {1,-1, 0, 0};
for(int k = 0; k < 4; ++k) {
int i = x+ro[k];
int j = y+co[k];
if(i >= 0 && i < row && j >= 0 && j < col && d[i][j] == cnts[x][y]-1)
findLoad(load, matrix, d, i, j);
}
}
编辑距离 72
给定两个单词 word1 和 word2,计算出将 word1 转换为 word2 所使用的最少操作数。
对一个单词进行以下操作:
插入一个字符、删除一个字符、替换一个字符。
>> 思路
所谓的动态规划,就是找出上一步和这一步之间的联系。字符串处理的动态规划,可以先考虑字符串的子集间的联系,比如"abcde"
与"bfdefs"
,就可以先考虑它们子集间的联系,如"a"
与"b"
,"abc"
与"b"
等等,一步步向最终的字符串演化,得到最终结果。
class Solution {
public:
int minDistance(string word1, string word2) {
int row = word1.size();
int col = word2.size();
if(row == 0 || col == 0)
return abs(row-col);
vector<vector<int> > dp(row+1, vector<int>(col+1, 0));
for(int i = 1; i <= row; ++i) {
dp[i][0] = i;
}
for(int j = 1; j <= col; ++j)
dp[0][j] = j;
for(int i = 1; i <= row; ++i)
for(int j = 1; j <= col; ++j) {
if(word1[i-1] != word2[j-1])
dp[i][j] = 1;
dp[i][j] += dp[i-1][j-1];
dp[i][j] = min(dp[i][j], min(dp[i-1][j]+1, dp[i][j-1]+1));
}
return dp[row][col];
}
};
两个字符串的删除操作 583
给定两个单词 word1 和 word2,找出使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。
>> 思路
其实就是找最长公共子串。
class Solution {
public:
int minDistance(string word1, string word2) {
int row = word1.size();
int col = word2.size();
if(row == 0 || col == 0)
return abs(row-col);
vector<vector<int>> dp(row+1, vector<int>(col+1, 0));
for(int i = 1; i <= row; ++i)
for(int j = 1; j <= col; ++j) {
if(word1[i-1] == word2[j-1])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
return row+col-dp[row][col]-dp[row][col];
}
};
两个字符串的最小ASCII删除和 712
给定两个字符串,找出使得两个字符串相等所需删除的字符的ASCII值的最小和。
>> 思路
我以为像上一题一样,先dp求出最长公共子串,再通过回溯找最长公共子串的最大值(最长公共子串有可能有多个),写成以下代码,但总是超时/(ㄒoㄒ)/~~,看了大神的解法,才知道根本不需要回溯,直接dp求公共子串的最大值就好了,只要将原来就最长子串的递推公式改下就可以了。。。
// 我的做法,愚蠢,超时!!
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int sum1 = 0, sum2 = 0;
int row = s1.size(), col = s2.size();
for(auto& ch: s1) sum1 += ch;
for(auto& ch: s2) sum2 += ch;
if(row == 0 || col == 0)
return abs(sum1-sum2);
vector<vector<int>> dp(row+1, vector<int>(col+1, 0));
for(int i = 1; i <= row; ++i)
for(int j = 1; j <= col; ++j) {
if(s1[i-1] == s2[j-1])
dp[i][j] = dp[i-1][j-1]+1;
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
map<pair<int, int>, int> mp;
int sum = dfs(s1, s2, dp, row, col, mp);
return sum1+sum2-2*sum;
}
int dfs(string& s1, string& s2, vector<vector<int>>& dp, int i, int j, map<pair<int, int>, int> mp) {
if(i == 0 || j == 0)
return 0;
if(mp.count({i,j}) > 0)
return mp[{i,j}];
if(s1[i-1] == s2[j-1])
return mp[{i,j}] = dfs(s1, s2, dp, i-1, j-1, mp) + s1[i-1];
if(dp[i-1][j] == dp[i][j-1]) {
int l = dfs(s1, s2, dp, i-1, j, mp);
int r = dfs(s1, s2, dp, i, j-1, mp);
return mp[{i,j}] = max(l, r);
}
if(dp[i-1][j] == dp[i][j])
return mp[{i,j}] = dfs(s1, s2, dp, i-1, j, mp);
return mp[{i,j}] = dfs(s1, s2, dp, i, j-1, mp);
}
};
看了大神的方法后,写的:
class Solution {
public:
int minimumDeleteSum(string s1, string s2) {
int sum1 = 0, sum2 = 0;
int row = s1.size(), col = s2.size();
for(auto& ch: s1) sum1 += ch;
for(auto& ch: s2) sum2 += ch;
if(row == 0 || col == 0)
return abs(sum1-sum2);
vector<vector<int>> dp(row+1, vector<int>(col+1, 0));
for(int i = 1; i <= row; ++i)
for(int j = 1; j <= col; ++j) {
if(s1[i-1] == s2[j-1]) {
dp[i][j] = dp[i-1][j-1]+s1[i-1]; // 只需将长度1改字符值!!
}
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
return sum1+sum2-2*dp[row][col];
}
};
总结:
以后发现字符串求极值,直接将要求的目标作为dp的内容!!
最长递增子序列 300
给定一个无序的整数数组,找出其中最长递增子序列的长度。
输入:
[10,9,2,5,3,7,101,18]
输出:
4
最长递增子序列为
[2, 3, 7, 101]
>> 思路
我想应该可以用dp来解,但是如何解呢?dp的值应该表示什么?dp的索引又表示什么?
我想,如果用dp,那么dp的值应该表示目前最长的递增子序列的长度,而这个递增子序列应该和对应的索引值有关,所以,我就假设以dp的索引值作为目前最长子序列的最后一个字符的位置,即dp的值表示以该索引位置结尾的最长子序列的长度。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.empty())
return 0;
vector<int> dp(nums.size(), 1);
int res = 1;
for(int end = 1; end < nums.size(); ++end) {
int mx = 0;
for(int i = 0; i < end; ++i)
if(nums[i] < nums[end] && mx < dp[i]) {
mx = dp[i];
}
if(mx != 0)
dp[end] = mx+1;
if(dp[end] > res)
res = dp[end];
}
return res;
}
};
bool helper(const pair<int, int>& l, const pair<int, int>& r) {
return l.first < r.first;
}
class Solution {
public:
int maxEnvelopes(vector<pair<int, int>>& envelopes) {
if(envelopes.empty())
return 0;
sort(envelopes.begin(), envelopes.end(), helper);
int sz = envelopes.size();
vector<int> dp(sz, 1);
int res = 1;
for(int end = 1; end < sz; ++end) {
int mx = 0;
for(int i = 0; i < end; ++i) {
if(envelopes[i].second < envelopes[end].second && envelopes[i].first != envelopes[end].first && mx < dp[i])
mx = dp[i];
}
if(mx != 0)
dp[end] = mx+1;
if(res < dp[end])
res = dp[end];
}
return res;
}
};
递增的三元子序列 334
给定一个未排序的数组,判断这个数组中是否存在长度为 3 的递增子序列。
数学表达式如下:
如果存在这样的 i, j, k, 且满足 0 ≤ i < j < k ≤ n-1,
使得 arr[i] < arr[j] < arr[k] ,返回 true ; 否则返回 false 。要求时间复杂度为O(n),空间复杂度为O(1).
>> 思路
我的想法是使用一个双端队列,它的大小不会超过2,它保存着目前遍历到的数中的递增子序列,但递增子序列到3时就返回true
。这个想法虽然能够通过,但是有bug,比如[1,2,-1,-2,1]
、2,3,-2,-3,0
等就出错了,因为它对后面的-1,-2
没有排序就替换了前面的1,2
导致遇到1
时就true
了。。。
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
if(nums.empty())
return false;
int sz = nums.size();
int mn = nums[0];
int cnt = 1;
deque<int> que;
que.push_back(nums[0]);
int n = -1;
for(int i = 1; i < sz; ++i) {
mn = que.back();
if(que.size() == 1) {
if(mn > nums[i]) {
que.pop_back();
}
if(mn != nums[i])
que.push_back(nums[i]);
} else {
if(mn < nums[i])
return true;
if(mn == nums[i])
continue;
mn = que.front();
if(mn == nums[i])
continue;
if(mn < nums[i]) {
que.pop_back();
que.push_back(nums[i]);
} else {
if(n != -1) {
que.pop_front();
que.pop_front();
que.push_back(nums[n]);
que.push_back(nums[i]);
n = -1;
} else {
n = i;
}
}
}
}
return false;
}
};
正确的做法应该是
// 用两个变量来存储前两个递增值,最后一个就返回了。
class Solution {
public:
bool increasingTriplet(vector<int>& nums) {
int m1 = INT_MAX, m2 = INT_MAX;
for (auto a : nums) {
if (m1 >= a)
m1 = a;
else if (m2 >= a)
m2 = a;
else
return true;
}
return false;
}
};
计算各个位数不同的数字个数 357
>
>> 思路
分段求数字个数:
0~9
、10~99
、100~999
、1000~9999
… …
从第二段开始,用同一个方法求数字个数:
除了最高位不能为零,其他位置均可以为0,所以,分为当其他位置的某一位为0,和都不为零:
某一位为0时(设共i+1
位),数字个数为:zo = 9*8*...
i
个数相乘。共有i
位可以为0,所以zo *= i
都不为零时,数字个数为:9*8*...
i+1
个数相乘。所以,合起来可以简化为:
(i+b)*zo
,b
为上面第i+1
个乘数。
class Solution {
public:
int countNumbersWithUniqueDigits(int n) {
if(n == 0)
return 1;
int res = 10;
int zo = 9;
int b = 8;
for(int i = 1; i < n; ++i) {
res += (i+b)*zo;
zo *= b;
--b;
}
return res;
}
};
合并区间 56
给出一个区间的集合,请合并所有重叠的区间。
示例 1:
输入: [[1,3],[2,6],[8,10],[15,18]] 输出: [[1,6],[8,10],[15,18]] 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6].
示例 2:
输入: [[1,4],[4,5]] 输出: [[1,5]] 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。
>> 思路
先按区间开始的位置排序,然后使用贪心算法,开始的位置保存不变,如果后一个区间的开始位置小于前一个区间的结束位置,则合并,把end
改为当前值和下一个区间结束位置的最大值。
/**
* Definition for an interval.
* struct Interval {
* int start;
* int end;
* Interval() : start(0), end(0) {}
* Interval(int s, int e) : start(s), end(e) {}
* };
*/
bool helper(const Interval& lhs, const Interval& rhs) {
return lhs.start < rhs.start;
}
class Solution {
public:
vector<Interval> merge(vector<Interval>& intervals) {
vector<Interval> res;
if(intervals.empty())
return res;
sort(intervals.begin(), intervals.end(), helper);
int start = intervals[0].start;
int end = intervals[0].end;
for(int i = 1; i < intervals.size(); ++i) {
if(end >= intervals[i].start)
end = max(end, intervals[i].end);
else {
res.push_back(Interval(start, end));
start = intervals[i].start;
end = intervals[i].end;
}
}
res.push_back(Interval(start, end));
return res;
}
};
阿里编程题
给定一个字符串,和一个字典,查找能组成这个字符串的一组单词。
输入
asdfjkl
5
as
asd
fj
kl
fjkl
输出
asd fjkl
>> 思路
我一开始就想到深度优先,可是写的过程就不太流畅,总是纠结要返回vector<string> res
,还是把它放在参数位置,搞了半天,以后,遇到要添加新内容的,先考虑引用参数,每次递归传递给它一个局部变量,如果有新内容的,就把局部变量中的内容添加到当前变量来,不要返回!!!返回bool
表示是否能够找到。
#include <bits/stdc++.h>
using namespace std;
bool dfs(string& s, int pos, map<int, vector<string>>& mp, vector<string>& res) {
if(pos == s.size())
return true;
for(auto &em: mp) {
auto words = em.second;
size_t i = 0;
for(auto &word: words) {
for(; i < word.size(); ++i) {
if(pos+i == s.size() || word[i] != s[pos+i])
break;
}
if(i == word.size()) {
res.push_back(word);
if(pos+i == s.size()) {
return true;
}
vector<string> vs;
if(dfs(s, pos+i, mp, vs) == true) {
for(size_t j = 0; j < vs.size(); ++j) {
res.push_back(vs[j]);
}
}
}
}
}
return false;
}
int main()
{
string s;
cin >> s;
int n; cin >> n;
vector<string> words(n);
for(int i = 0; i < n; cin>>words[i++]);
map<int, vector<string>> mp;
for(auto &word: words) {
mp[-word.size()].push_back(word);
}
vector<string> res;
dfs(s, 0, mp, res);
for(int i = 0; i < res.size(); ++i) {
cout << res[i] << " ";
}
cout << endl;
return 0;
}
插入区间 57
给出一个无重叠的 ,按照区间起始端点排序的区间列表。
在列表中插入一个新的区间,你需要确保列表中的区间仍然有序且不重叠(如果有必要的话,可以合并区间)。
>> 思路
因为区间列表已经有序,所以,我比较新区间到第一个大于等于它的位置。
- 如果整个列表都比它小,比较它和最后一个区间,可以合并就合并,不可以则插入,然后返回该列表。
- 存在比它大的位置。比较它与前一个位置,看能不能合并,不能则插入前一个。
- 接着,就是区间合并了。
/**
* Definition for an interval.
* struct Interval {
* int start;
* int end;
* Interval() : start(0), end(0) {}
* Interval(int s, int e) : start(s), end(e) {}
* };
*/
class Solution {
public:
vector<Interval> insert(vector<Interval>& intervals, Interval newInterval) {
vector<Interval> res;
if(intervals.empty())
return vector<Interval>(1, newInterval);
int i = 0;
for(; i < intervals.size(); ++i) {
if(intervals[i].start >= newInterval.start)
break;
}
int start = newInterval.start;
int end = newInterval.end;
if(i == intervals.size()) {
if(intervals[i-1].end >= start) {
intervals[i-1].end = max(intervals[i-1].end, end);
} else {
intervals.push_back(newInterval);
}
return intervals;
}
for(int j = 0; j < i-1; ++j)
res.emplace_back(intervals[j]);
//res.assign(intervals.begin(), intervals.begin()+i-1);
if(i > 0) {
start = intervals[i-1].start;
end = intervals[i-1].end;
if(newInterval.start <= end) {
end = max(end, newInterval.end);
} else {
res.emplace_back(intervals[i-1]);
start = newInterval.start;
end = newInterval.end;
}
}
for(; i < intervals.size(); ++i) {
if(intervals[i].start <= end)
end = max(end, intervals[i].end);
else {
res.emplace_back(Interval(start, end));
start = intervals[i].start;
end = intervals[i].end;
}
}
res.emplace_back(Interval(start, end));
return res;
}
};
注意:assign()
是不会分配空间,要先分配的。
解法2
直接插入到列表的最后,排序,然后区间合并。
bool cmp(Interval& lhs, Interval& rhs) {
return lhs.start <= rhs.start;
}
static int x=[](){
std::ios::sync_with_stdio(false);
cin.tie(NULL);
return 0;
}();
class Solution {
public:
vector<Interval> insert(vector<Interval>& intervals, Interval newInterval) {
intervals.emplace_back(newInterval);
sort(intervals.begin(), intervals.end(), cmp);
vector<Interval> res;
int start = intervals[0].start;
int end = intervals[0].end;
for(int i = 1; i < intervals.size(); ++i) {
if(intervals[i].start <= end)
end = max(end, intervals[i].end);
else {
res.push_back(Interval(start, end));
start = intervals[i].start;
end = intervals[i].end;
}
}
res.push_back(Interval(start, end));
return res;
}
};
区间模块 715
实现三个函数
addRange()
、queryRange()
、removeRange()
来完成区间的添加,查询和删除功能。添加一个区间要完成合并。
>> 思路
解法1
直接使用vector<Interval>
作为存储容器,每插入,删除,查询一个区间的复杂度均为O(n)
。
// 428ms
/*
struct Interval {
int start;
int end;
Interval(int s, int e): start(s), end(e) {}
};*/
bool cmp(Interval& lhs, Interval& rhs) {
return lhs.start <= rhs.start;
}
class RangeModule {
public:
RangeModule(): intervals() {}
void addRange(int left, int right) {
intervals.emplace_back(Interval(left, right));
sort(intervals.begin(), intervals.end(), cmp);
vector<Interval> res;
int start = intervals[0].start;
int end = intervals[0].end;
for(int i = 1; i < intervals.size(); ++i) {
if(intervals[i].start <= end)
end = max(end, intervals[i].end);
else {
res.push_back(Interval(start, end));
start = intervals[i].start;
end = intervals[i].end;
}
}
res.push_back(Interval(start, end));
intervals = move(res);
}
bool queryRange(int left, int right) {
for(int i = 0; i < intervals.size(); ++i) {
if(intervals[i].start <= left && intervals[i].end >= right) {
return true;
}
}
return false;
}
void removeRange(int left, int right) {
if(intervals.empty())
return;
vector<Interval> res;
int i = 0;
for(; i < intervals.size() && intervals[i].end <= left; ++i) { // 删除区间左边的所有区间加入
res.emplace_back(intervals[i]);
}
if(i < intervals.size() && intervals[i].start < left) // 删除区间左边界
res.emplace_back(Interval(intervals[i].start, left));
int j = i;
for(; j < intervals.size() && intervals[j].end <= right; ++j); // 删除区间范围
if(j < intervals.size()) // 删除区间右边界
res.emplace_back(Interval(max(intervals[j].start, right), intervals[j].end));
for(++j; j < intervals.size(); ++j)
res.emplace_back(intervals[j]);
intervals = move(res);
}
private:
vector<Interval> intervals;
};
解法2:
使用链表作为存储结构,插入,删除,查询直接在链表上完成,无需辅助空间。效率高很多。
插入区间:
我使用链表作为存储结构,插入一个区间[left, right)
分三步走:
- 判断链表为空,直接插入头部。判断插入区间是否为最小区间,是则直接插入链表头部。
- 判断插入区间是否最大区间,是则直接插入尾部。
- 上面两个都不是,则再分3种情况:
- 插入区间与已存在的区间没有交叉,那么需要插入。
- 插入区间与一个已存在的区间有交叉,那么只需修改这个区间。
- 插入区间与多个已存在的区间有交叉,那么需要修改其中一个区间,其他删除。
判断区间交叉情况:
- 以插入区间的左端为比较值,从链表头开始与各区间的右端比较,找出第一个区间右端
>=
比较值的区间位置pos
。 - 以
min(pos.start, left)
作为新区间的左端。 - 以插入区间的右端为比较值,从上面的
pos-1
开始与各区间的左端比较,找出最后一个区间左端<=
比较值的区间位置endpos
。 - 以
max(endpos.end, right)
作为新区间的右端。 - 通过比较
pos
与endpos
:
- 如果
pos>endpos
,则插入区间与已存在区间没有交叉,新区间插入pos
位置。 - 如果
pos == endpos
,则插入区间只与pos
位置区间交叉,修改它为新区间。 - 如果
pos < endpos
,则插入区间与多个区间有交叉,修改endpos
为新区间,删除[pos, endpos)
。
- 如果
删除区间:
删除操作就是插入操作的反向操作。
// 98ms 战胜100%
/*
* struct Interval {
* int start;
* int end;
* Interval(int s, int e):start(s), end(e) { }
* };
*
*/
static int x=[](){
std::ios::sync_with_stdio(false);
cin.tie(NULL);
return 0;
}();
class RangeModule {
public:
RangeModule() :intervals() { }
void addRange(int left, int right) {
// 为空 或者 插入区间为最小区间
if(intervals.empty() || right < intervals.front().start) {
intervals.push_front(Interval(left, right));
return;
}
// 插入区间为最大区间
if(intervals.back().end < left) {
intervals.push_back(Interval(left, right));
return;
}
auto pos = intervals.begin();
for(; pos != intervals.end() && pos->end < left; ++pos);
int start = min(left, pos->start);
auto endpos = pos;
for(; endpos != intervals.end() && endpos->start <= right; ++endpos);
if(endpos == pos) {// 没有交叉
intervals.insert(pos, Interval(left, right));
return;
}
// 有交叉
--endpos;
int end = max(right, endpos->end);
endpos->start = start;
endpos->end = end;
if(pos != endpos) // 交叉的区间大于一个
intervals.erase(pos, endpos);
}
bool queryRange(int left, int right) {
for(auto beg = intervals.begin(); beg != intervals.end(); ++beg) {
if(beg->start <= left && beg->end >= right)
return true;
}
return false;
}
void removeRange(int left, int right) {
if(intervals.empty())
return;
// 删除区间不存在
if(intervals.front().start >= right || intervals.back().end <= left)
return;
auto pos = intervals.begin();
for(; pos != intervals.end() && pos->end <= left; ++pos);
Interval first(pos->start, left);
auto endpos = pos;
for(; endpos != intervals.end() && endpos->start < right; ++endpos);
if(endpos == pos) // 区间不存在
return;
--endpos;
Interval second(right, endpos->end);
if(first.start < left) {
// [pos->start, left)
intervals.insert(pos, first);
}
if(second.end > right) {
// [right, endpos->end)
intervals.insert(pos, second);
}
intervals.erase(pos, ++endpos);
}
private:
list<Interval> intervals;
};
/**
* Your RangeModule object will be instantiated and called as such:
* RangeModule obj = new RangeModule();
* obj.addRange(left,right);
* bool param_2 = obj.queryRange(left,right);
* obj.removeRange(left,right);
*/
注意:list
的迭代器是双向迭代器,不是随机迭代器,所以,只能自增或自减。。。
list
的迭代器不会因为插入或删除其他元素而失效!!它是与元素绑定在一起的。
复原IP地址 93
>
>> 思路
一是只要遇到字符串的子序列或配准问题首先考虑动态规划DP,二是只要遇到需要求出所有可能情况首先考虑用递归。
这个题属于第二种,也就是不是一步判断就可以了,判断完要接着判断,有点像深搜。