滑动窗口
思路:
遍历右端点,如果不符合条件/符合条件,循环 调整左端点 直至
209.长度最小的子数组
给定一个含有 n 个正整数的数组和一个正整数 target 。
找出该数组中满足其总和大于等于 target 的长度最小的 子数组
[
n
u
m
s
l
,
n
u
m
s
l
+
1
,
.
.
.
,
n
u
m
s
r
−
1
,
n
u
m
s
r
]
[nums_l, nums_{l+1}, ..., nums_{r-1}, nums_r]
[numsl,numsl+1,...,numsr−1,numsr] ,并返回其长度。如果不存在符合条件的子数组,返回 0 。
示例 1:
输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。
遍历右端点,如果符合条件(大于等于 target ),左端点一直调整(调整过程中更新res因为要最短的)
class Solution {
public:
int minSubArrayLen(int target, vector<int>& nums) {
int l=0,r=0;
int res = INT_MAX;
int tmp = 0;
while(r<nums.size()){
tmp += nums[r];
while(tmp >= target){
res = min(res,r-l+1);
tmp -= nums[l];
l++;
}
r++;
}
if (res< INT_MAX)
return res;
else
return 0;
}
};
3.给定一个字符串 s ,请你找出其中不含有重复字符的 最长 子串的长度。
示例 1:
输入: s = “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len=s.size();
int l=0,r=0;
map<char,int> st;//char 次数
if(len==0)
return 0;
int res=1;
while(r<len){
//l指针移到r 所在位置,因为最大的就是[l,r),这区间的不可能出现更大的了
//所以将r之前的全删掉
while(st[s[r]]) { //和上面一样都是while
st[s[l]]--;l++;
}
res=max(res,r-l+1);
st[s[r]]+=1;
r++;
}
return res;
}
};
713. 给你一个整数数组 nums 和一个整数 k ,请你返回子数组内所有元素的乘积严格小于 k 的连续子数组的数目。
示例 1:
输入:nums = [10,5,2,6], k = 100
输出:8
解释:8 个乘积小于 100 的子数组分别为:[10]、[5]、[2],、[6]、[10,5]、[5,2]、[2,6]、[5,2,6]。
需要注意的是 [10,5,2] 并不是乘积小于 100 的子数组。
示例 2:
输入:nums = [1,2,3], k = 0
输出:0
class Solution {
public:
int numSubarrayProductLessThanK(vector<int>& nums, int k) {
if (k==0 || k==1)
return 0;
int l=0,r=0;
int len=nums.size();
int tmp=1,res=0;
while(r<len){
tmp *= nums[r];//这两的顺序
while(tmp>=k){//好吧,一直都是这样的,先把右端点加进去,然后while直接判断
tmp /= nums[l];
l++;
}
res += r-l+1;//如果[l,r]小于k,那么[l+1,r][l+2,r],[r]肯定也小于k
r++;
}
return res;
}
};
二叉树
千万不要一上来就陷入二叉树的细节
104. 二叉树的最大深度
给定一个二叉树 root ,返回其最大深度。
二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。
时间复杂度:
O
(
n
)
O(n)
O(n),其中 nnn 为二叉树的节点个数。
空间复杂度:
O
(
n
)
O(n)
O(n)。最坏情况下,二叉树退化成一条链,递归需要
O
(
n
)
O(n)
O(n) 的栈空间。
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root==nullptr) return 0;
int l=maxDepth(root->left);
int r=maxDepth(root->right);
return max(l,r)+1;
}
};
方法2:用全局变量
class Solution {
int ans = 0;
void dfs(TreeNode *node, int cnt) {
if (node == nullptr) return;
++cnt;
ans = max(ans, cnt);
dfs(node->left, cnt);
dfs(node->right, cnt);
}
public:
int maxDepth(TreeNode *root) {
dfs(root, 0);
return ans;
}
};
111. 二叉树的最小深度
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
class Solution {
int ans = INT_MAX;
void dfs(TreeNode* root,int cnt){
if(root == nullptr) return;
cnt++;
if(root->left == root->right){
ans=min(ans,cnt);return;
}
dfs(root->left,cnt);
dfs(root->right,cnt);
return;
}
public:
int minDepth(TreeNode* root) {
dfs(root,0);
return root ? ans : 0;
}
};
优化
如果递归中发现 cnt≥ans,由于继续向下递归也不会让 ans\textit{ans}ans 变小,直接返回。
这一技巧叫做「最优性剪枝」。
void dfs(TreeNode* root,int cnt){
if(root == nullptr || cnt>=ans) return;
cnt++;
if(root->left == root->right){// node 是叶子
ans=min(ans,cnt);return;
}
dfs(root->left,cnt);
dfs(root->right,cnt);
return;
}
时间复杂度:
O
(
n
)
\mathcal{O}(n)
O(n),其中 n为二叉树的节点个数。
空间复杂度:
O
(
n
)
\mathcal{O}(n)
O(n)。最坏情况下,二叉树退化成一条链,递归需要
O
(
n
)
\mathcal{O}(n)
O(n)的栈空间。
112.路径总和
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false 。
叶子节点 是指没有子节点的节点。
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if(root==nullptr) return false;
targetSum -= root->val;//
if(root->left == root->right && targetSum==0) return true;
return hasPathSum(root->left,targetSum) || hasPathSum(root->right,targetSum);//
}
};
129. 求根节点到叶节点数字之和
给你一个二叉树的根节点 root ,树中每个节点都存放有一个 0 到 9 之间的数字。
每条从根节点到叶节点的路径都代表一个数字:
例如,从根节点到叶节点的路径 1 -> 2 -> 3 表示数字 123 。
计算从根节点到叶节点生成的 所有数字之和 。
class Solution {
int ans=0;
void dfs(TreeNode* root, int x){
if(root==nullptr) return;
x = x * 10 + root->val;
if(root->left==root->right) {
ans += x;return;
}
dfs(root->left,x);
dfs(root->right,x);
}
public:
int sumNumbers(TreeNode* root) {
dfs(root,0);
return ans;
}
};
int goodNodes(TreeNode* root,int value=INT_MIN) {//value表示当前遇到的最大值
if(root==nullptr) return 0;
int left = goodNodes(root->left,max(value,root->val));
int right = goodNodes(root->right,max(value,root->val));
return left+right+(value<=root->val);//如果当前结点的值 < 当前遇到的最大值,该结点就符合要求
}
1448. 统计二叉树中好节点的数目
给你一棵根为 root 的二叉树,请你返回二叉树中好节点的数目。
「好节点」X 定义为:从根到该节点 X 所经过的节点中,没有任何节点的值大于 X 的值。
在深度优先遍历的过程中,记录从根节点到当前节点的路径上所有节点的最大值,若当前节点的值大于等于该最大值,则认为当前节点是好节点。
class Solution {
int dfs(TreeNode* root,int preMax = INT_MIN){//要用参数传递更新 当前遇到的最大值的
if(root==nullptr) return 0;
int ans = 0;
if(root->val >= preMax){
preMax = root->val;
ans++;
}
ans += dfs(root->left,preMax) + dfs(root->right,preMax);
return ans;
}
public:
int goodNodes(TreeNode* root) {//value表示当前遇到的最大值
return dfs(root);
}
};
双指针
双向双指针 接雨水
11. 盛最多水的容器
给定一个长度为 n 的整数数组 height 。有 n 条垂线,第 i 条线的两个端点是 (i, 0) 和 (i, height[i]) 。
找出其中的两条线,使得它们与 x 轴共同构成的容器可以容纳最多的水。
返回容器可以储存的最大水量。
因为宽在缩小,所以 看那边高度小,就不要这个高度小的 「谁小移动谁」
第一次没有想到用双指针。。。
n = = h e i g h t . l e n g t h , 2 < = n < = 1 0 5 , O ( n 2 ) n == height.length,2 <= n <= 10^5, O(n^2) n==height.length,2<=n<=105,O(n2)会超时
在每个状态下,无论长板或短板向中间收窄一格,都会导致水槽 底边宽度 −1-1−1 变短:>
- 若向内 移动短板 ,水槽的短板 min(h[i],h[j])可能变大,因此下个水槽的面积 可能增大 。
- 若向内 移动长板 ,水槽的短板 min(h[i],h[j])不变或变小,因此下个水槽的面积 一定变小 。
class Solution {
public:
int maxArea(vector<int>& height) {
int len=height.size();
int left=0,right=len-1;
int ans=0;
while(left < right){
int tmp = (right-left)*min(height[left],height[right]);
ans = max(ans,tmp);
if(height[left]<=height[right]) left++;
else right--;
}
return ans;
}
};
42.接雨水
给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。
求每一列能装的水,我们只需要关注当前列,以及其左边最高的墙,和其右边最高的墙。
根据较矮的那个墙和当前列的墙的高度可以分为三种情况。
- 较矮的墙的高度大于当前列的墙的高度
- 较矮的墙的高度小于(等于)当前列的墙的高度:正在求的列不会有水,因为它大于了两边较矮的墙。
分成一列一列 理解
第一次时间复杂度
O
(
n
2
)
O(n^2)
O(n2),超时了
后面想,对于一个序列,每个位置的左边最大值和 右边最大值有关系------->也就是下面的动态规划里的==每个位置的左边最大值max_left [i] = Max(max_left [i-1],height[i-1])。==它前边的墙的左边的最高高度** 和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了
。
遍历所有列,每列能接的雨水=min(左边最大值,右边最大值)-当前高度
左边最大值dp[i]=max(dp[i-1],h[i-1]) h(i-1)是因为不包含当前这一列
右边最大值dp[i]=max(dp[i+1],h[i+1])从大到小遍历
public int trap(int[] height) {
int sum = 0;
int[] max_left = new int[height.length];
int[] max_right = new int[height.length];
for (int i = 1; i < height.length - 1; i++) {
max_left[i] = Math.max(max_left[i - 1], height[i - 1]);
}
for (int i = height.length - 2; i >= 0; i--) {
max_right[i] = Math.max(max_right[i + 1], height[i + 1]);
}
for (int i = 1; i < height.length - 1; i++) {
int min = Math.min(max_left[i], max_right[i]);
if (min > height[i]) {
sum = sum + (min - height[i]);
}
}
return sum;
}
双指针
max_left [ i ]
和 max_right [ i ]
数组中的元素我们其实只用一次,然后就再也不会用到了。所以我们可以不用数组,只用一个元素就行了。但是会发现我们不能同时把 max_right
的数组去掉,因为最后的 for 循环是从左到右遍历的,而 max_right
的更新是从右向左的。
所以这里要从两个方向去遍历。
那么什么时候从左到右,什么时候从右到左呢?
max_left = Math.max(max_left, height[i - 1]);
max_right = Math.max(max_right, height[i + 1]);
只要保证 height [ left - 1 ] < height [ right + 1 ]
,那么 max_left
就一定小于 max_right
public int trap(int[] height) {
int sum = 0;
int max_left = 0;
int max_right = 0;
int left = 1;
int right = height.length - 2; // 加右指针进去
for (int i = 1; i < height.length - 1; i++) {
//从左到右更
//当前是left,计算其左1之前的最大值和左一哪个大
if (height[left - 1] < height[right + 1]) {
max_left = Math.max(max_left, height[left - 1]);
int min = max_left;
if (min > height[left]) {
sum = sum + (min - height[left]);
}
left++;
//从右到左更
} else {
max_right = Math.max(max_right, height[right + 1]);
int min = max_right;
if (min > height[right]) {
sum = sum + (min - height[right]);
}
right--;
}
}
return sum;
}
栈
说到栈,我们肯定会想到括号匹配了。我们仔细观察蓝色的部分,可以和括号匹配类比下。每次匹配出一对括号(找到对应的一堵墙),就计算这两堵墙中的水。
-
当前高度小于等于栈顶高度,入栈,指针后移。
-
当前高度大于栈顶高度,出栈,计算出当前墙和栈顶的墙之间水的多少,然后计算当前的高度和新栈的高度的关系,重复第 2 步。直到当前墙的高度不大于栈顶高度或者栈空,然后把当前墙入栈,指针后移。
public int trap6(int[] height) {
int sum = 0;
Stack<Integer> stack = new Stack<>();
int current = 0;
while (current < height.length) {
//如果栈不空并且当前指向的高度大于栈顶高度就一直循环
while (!stack.empty() && height[current] > height[stack.peek()]) {
int h = height[stack.peek()]; //取出要出栈的元素
stack.pop(); //出栈
if (stack.empty()) { // 栈空就出去
break;
}
int distance = current - stack.peek() - 1; //两堵墙之前的距离。
int min = Math.min(height[stack.peek()], height[current]);
sum = sum + distance * (min - h);
}
stack.push(current); //当前指向的墙入栈
current++; //指针后移
}
return sum;
}
时间复杂度:虽然 while 循环里套了一个 while 循环,但是考虑到每个元素最多访问两次,入栈一次和出栈一次,所以时间复杂度是 O(n)。
空间复杂度:O(n)。栈的空间。
作者:windliang
链接:https://leetcode.cn/problems/trapping-rain-water/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
方法一:前后缀分解
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
vector<int> pre_m(len);
vector<int> after_m(len);
pre_m[0] = height[0];
after_m[len-1] = height[len-1];
for(int i=1;i<len;i++){
pre_m[i] = max(pre_m[i-1],height[i]);
}
for(int i=len-2;i>=0;i--){
after_m[i] = max(after_m[i+1],height[i]);
}
int ans=0;
for(int i=0;i<len;i++){
ans += min(pre_m[i],after_m[i]) - height[i];
}
return ans;
}
};
优化空间复杂度至
O
(
1
)
O(1)
O(1)
方法2: 单调栈 TODO
B站讲解
class Solution {
public:
int trap(vector<int>& height) {
int len = height.size();
int ans=0;
stack<int> st;
for(int i=0;i<len;i++){
while(!st.empty() && height[i]>height[st.top()]){//可以接水了
int j=st.top();
st.pop();
if(st.empty()) break;
int left = st.top();
int dh = min(height[left],height[i]) - height[j];//min(当前,第二个栈顶)-栈顶
ans += dh * (i-left-1);
}
st.push(i);
}
return ans;
}
};
98. 验证二叉搜索树
给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。
有效 二叉搜索树定义如下:
节点的左子树只包含 小于 当前节点的数。
节点的右子树只包含 大于 当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
class Solution {
long pre = LONG_MIN;
public:
bool isValidBST(TreeNode* root,long left=LONG_MIN,long right=LONG_MAX) {
if(root==nullptr) return true;
long x=root->val;
return x>left && x<right && isValidBST(root->left,left,x) && isValidBST(root->right,x,right);
}
bool isValidBST(TreeNode* root){
if(root == nullptr) return true;
if(!isValidBST(root->left) || root->val <= pre)
return false;
pre = root->val; //中序遍历这时候 根节点
return isValidBST(root->right);
}
};
单调栈
739. 每日温度
给定一个整数数组 temperatures ,表示每天的温度,返回一个数组 answer ,其中 answer[i] 是指对于第 i 天,下一个更高温度出现在几天后。如果气温在这之后都不会升高,请在该位置用 0 来代替。
class Solution {
public:
vector<int> dailyTemperatures(vector<int>& temperatures) {
stack<int> st;
int n = temperatures.size();
vector<int> res(n);
for(int i=0;i<n;i++){
//栈是单减的
while(!st.empty() && temperatures[i]>temperatures[st.top()]){
int j = st.top();
st.pop();
res[j] = i - j;//栈顶 被pop()出去,才是下一个比它大的
}
st.push(i);
}
return res;
}
};
class Solution {
public int[] dailyTemperatures(int[] temperatures) {
int n = temperatures.length;
int[] ans = new int[n];
int[] st = new int[n];
int top = -1;
for(int i = 0; i < n; i++){
int t = temperatures[i];
while(top >= 0 && t > temperatures[st[top]]){
int j = st[top];
top --;
ans[j] = i - j;
}
top ++;
st[top] = i;
}
return ans;
}
}
496. 下一个更大元素 I
nums1 中数字 x 的 下一个更大元素 是指 x 在 nums2 中对应位置 右侧 的 第一个 比 x 大的元素。
给你两个 没有重复元素 的数组 nums1 和 nums2 ,下标从 0 开始计数,其中nums1 是 nums2 的子集。
对于每个 0 <= i < nums1.length ,找出满足 nums1[i] == nums2[j]
的下标 j ,并且在 nums2 确定 nums2[j] 的 下一个更大元素 。如果不存在下一个更大元素,那么本次查询的答案是 -1 。
返回一个长度为 nums1.length 的数组 ans 作为答案,满足 ans[i] 是如上所述的 下一个更大元素
class Solution {
public:
vector<int> nextGreaterElement(vector<int>& nums1, vector<int>& nums2) {
stack<int> st;
int n = nums1.size();
vector<int> ans(n,-1);
unordered_map<int,int> idx;
for(int i=0;i<nums1.size();i++){
idx[nums1[i]]=i;
}
for(int i=nums2.size()-1;i>=0;i--){
int x=nums2[i];
while(!st.empty() && x >= st.top()){
//由于 往前有一个更大的,所以当前栈顶肯定不会是左边元素的 下一个更大的元素,当前栈顶没有用
st.pop();
}
if(!st.empty() && idx.contains(x)) {
ans[idx[x]] = st.top();//这维护了一个单减的单调栈,比x小的等于的 都被pop出去了,所以栈顶就是下一个比他大的
}
st.push(x);
}
for(int i=0;i<nums2.size();i++){
int x = nums2[i];
while(!st.empty() && x>st.top()){//从左往右,比他大的数,单调栈是递增的
ans[idx[st.top()]] = x;//x是栈顶的下一个更大元素
st.pop();//构造递增单调栈
}
if(idx.contains(x)) //该题的特殊要求
st.push(x);
}
return ans;
}
};
1019. 链表中的下一个更大节点
给定一个长度为 n 的链表 head
对于列表中的每个节点,查找下一个 更大节点 的值。也就是说,对于每个节点,找到它旁边的第一个节点的值,这个节点的值 严格大于 它的值。
返回一个整数数组 answer ,其中 answer[i] 是第 i 个节点( 从1开始 )的下一个更大的节点的值。如果第 i 个节点没有下一个更大的节点,设置 answer[i] = 0 。
class Solution {
public:
vector<int> nextLargerNodes(ListNode* head) {
vector<int> ans;
stack<pair<int,int>> s;
ListNode* cur=head;
int idx=-1;
while(cur){
idx++;
ans.push_back(0);
while(!s.empty() && cur->val > s.top().first){
ans[s.top().second] = cur->val;//x是栈顶的下一个更大元素
s.pop();//构造递增单调栈
}
s.emplace(cur->val,idx);
cur=cur->next;
}
return ans;
}
};
DFS
全排列
class Solution {
vector<vector<int>> ans;
vector<int> path;
vector<int> st;
void dfs(vector<int>& nums, int i){
if(i == nums.size()) {
ans.push_back(path);return;
}
for(int j = 0; j < nums.size(); j ++ ){
if(!st[j]){
path.push_back(nums[j]);
st[j] = 1;
dfs(nums, i + 1);
path.pop_back();
st[j] = 0;
}
}
}
public:
vector<vector<int>> permute(vector<int>& nums) {
for(int i = 0; i < nums.size(); i++) st.push_back(0);
dfs(nums, 0);
return ans;
}
};
47. 全排列 II
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
输入:nums = [1,1,2]
输出:
[[1,1,2],
[1,2,1],
[2,1,1]]
示例 2:
输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
//st[j-1] == false是因为nums[j-1]在回溯的过程中被撤销选择
//candidates[j] == candidates[j - 1]是同层的,所以答案中仍然会有[1,1’,2]这样的,但是不会存在[1’,1,2]
class Solution {
vector<vector<int>> ans;
vector<int> path;
vector<bool> st;
void dfs(vector<int>& candidates, int u){
if(u == candidates.size()) {
ans.emplace_back(path);
return;
}
for(int j = 0; j < candidates.size(); j++ ){
if(!st[j]) {
//st[j-1] == false是因为nums[j-1]在回溯的过程中被撤销选择
//candidates[j] == candidates[j - 1]是同层的,所以答案中仍然会有[1,1',2]这样的,但是不会存在[1',1,2]
if(j != 0 && candidates[j] == candidates[j - 1] && st[j-1] == false)
continue;
st[j] = true;
path.push_back(candidates[j]);//从左到右依次枚举每个数,每次将它放在一个空位上;
dfs(candidates, u + 1);
path.pop_back();
st[j] = false;
}
}
}
public:
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());//先将所有数从小到大排序,这样相同的数会排在一起; 因为结果是要去重的&candidates[j] == candidates[j - 1]
st = vector<bool>(nums.size(), false);
dfs(nums, 0);
return ans;
}
};
回溯
子集回溯
递归不要想太多层(想太多反而把自己弄晕了),只需要把边界条件和非边界条件写对
17. 电话号码的字母组合
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。
给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。
78. 子集
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的
子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
class Solution {
string MAPPING[10] = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};//用数组表示哈希表。数组比哈希表更快
void dfs(string digits,int len,int idx,vector<string>& ans,string& path){
if(idx == len){//边界条件
ans.emplace_back(path);
return;
}
for(char c:MAPPING[digits[idx] - '0']){
path[idx] = c;
dfs(digits,len,idx+1,ans,path);//下一个子问题
//不需要返回现场 因为MAPPING没有受到影响没有改变,对于idx+1,还是可以从头选择for(char c:MAPPING[digits[idx] - '0'])
}
}
public:
vector<string> letterCombinations(string digits) {
int len = digits.size();
if(len==0) return {};
vector<string> ans;
string path(len,0);//这个不初始化,后面path一直是""
dfs(digits,len,0,ans,path);
return ans;
}
};
每一个数字都有(3−4)个字符选择,可以算成4;总共有 n 个字符,因此有 4 n 4^n 4n种选择(状态), 对于每一种选择(状态),都需要$O(n)的时间 append 到 ans, 因此是 O ( n ∗ 4 n ) O(n*4^n) O(n∗4n)
方法一:选谁不选谁的视角
class Solution {
void dfs(int i, int len, vector<int>& nums, vector<int>& path, vector<vector<int>>& ans){
if(i == len) {// 子集构造完毕
ans.emplace_back(path);
return;
}
dfs(i+1,len, nums, path, ans); // 不选 nums[i]
path.push_back(nums[i]);
dfs(i+1, len, nums, path, ans);
path.pop_back();//恢复现场
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
int len = nums.size();
vector<vector<int>> ans;
vector<int> path;
dfs(0,len, nums, path, ans);
return ans;
}
};
方法二: 放在哪个位置答案的视角【不保证有序】
相似子问题分解+递归
枚举子集(答案)的第一个数选谁,第二个数选谁,第三个数选谁,依此类推。
class Solution {
void dfs(int i, int len, vector<int>& nums, vector<int>& path, vector<vector<int>>& ans){
if(i == len) {// 子集构造完毕
ans.emplace_back(path);
return;
}
//如果选 nums[j] 添加到 path 末尾,那么下一个要添加到 path 末尾的数,就要在 nums[j+1] 到 nums[n−1] 中枚举了。
for(int j=i;j<len;j++){
path.push_back(nums[j]);
dfs(j+1,len,nums,path,ans);
path.pop_back();
}
}
public:
vector<vector<int>> subsets(vector<int>& nums) {
int len = nums.size();
vector<vector<int>> ans;
vector<int> path;
dfs(0,len, nums, path, ans);
return ans;
}
};
131. 分割回文串
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串。返回 s 所有可能的分割方案。
示例 1:
输入:s = “aab”
输出:[[“a”,“a”,“b”],[“aa”,“b”]]
示例 2:
输入:s = “a”
输出:[[“a”]]
1. 选不选谁的视角
class Solution {
bool isPalindrome(string& s, int left, int right){
while(left < right)//双向 双指针 判断s[left,right]是不是回文串
if(s[left++] != s[right--])
return false;
return true;
}
//通过引入一个start变量解决
void dfs(int i,int start,int len,vector<vector<string>>& ans,vector<string>& path,string& s){
if(i == len) {
ans.emplace_back(path);
return;
}
//不选 i 和 i+1 之间的逗号
if (i < len - 1)
//i=n-1 时,也就是剩最后一个的时候 一定要选(因为是将S分割的,必须包含所有的 最后一个字母必须要选上)
dfs(i+1,start,len,ans,path,s);
if(isPalindrome(s, start, i)){//s[start,i]
//选 i 和 i+1 之间的逗号
path.push_back(s.substr(start,i-start+1));
dfs(i+1,i+1,len,ans,path,s);//对于s,下一个子串的选择从s的i+1开始。所以还是一个一个 看选不选
path.pop_back();//恢复现场
}
}
public:
vector<vector<string>> partition(string s) {
int len = s.length();
vector<vector<string>> ans;
vector<string> path;
dfs(0,0,len,ans,path,s);
return ans;
}
};
时间复杂度:
O
(
n
2
n
)
O(n2^n)
O(n2n),其中 n 为 s 的长度。每次都是选或不选,递归次数为一个满二叉树的节点个数,那么一共会递归
O
(
2
n
)
O(2^n)
O(2n) 次(等比数列和),再算上判断回文和加入答案时需要 O(n) 的时间,所以时间复杂度为
O
(
n
2
n
)
O(n2^n)
O(n2n)。
空间复杂度:
O
(
n
)
O(n)
O(n)。返回值的空间不计。
2. 答案的视角(枚举子串结束位置)
class Solution {
bool isPalindrome(string& s, int left, int right){
while(left < right)//双向 双指针 判断s[left,right]是不是回文串
if(s[left++] != s[right--])
return false;
return true;
}
void dfs(int i,int len,vector<vector<string>>& ans,vector<string>& path,string& s){
if(i == len) {
ans.emplace_back(path);
return;
}
for(int j=i;j<len;j++){// 枚举子串的结束位置
if(isPalindrome(s,i,j)){//s[i,j]
path.push_back(s.substr(i,j-i+1));
dfs(j+1,len,ans,path,s);//处理s[j+1]
path.pop_back();//恢复现场
}
}
}
public:
vector<vector<string>> partition(string s) {
int len = s.length();
vector<vector<string>> ans;
vector<string> path;
dfs(0,len,ans,path,s);
return ans;
}
};
时间复杂度:
O
(
n
2
n
)
O(n2^n)
O(n2n),其中 n 为 s 的长度。答案的长度至多为逗号子集的个数,即
O
(
2
n
)
O(2^n)
O(2n),因此会递归
O
(
2
n
)
O(2^n)
O(2n) 次,再算上判断回文和加入答案时需要 O(n) 的时间,所以时间复杂度为
O
(
n
2
n
)
O(n2^n)
O(n2n)。
空间复杂度:
O
(
n
)
O(n)
O(n)。返回值的空间不计。
class Solution {
vector<vector<string>> ans;
vector<string> path;
unordered_set<string> sset;
void dfs(int i,int len,string& s){
if(i == len){
ans.emplace_back(path);
return;
}
for(int j=i;j<len;j++){
string str = s.substr(i,j-i+1);
if(sset.find(str) == sset.end()){
sset.insert(str);
path.push_back(str);
dfs(j+1,len,s);
path.pop_back();
sset.erase(str);
}
}
}
public:
int maxUniqueSplit(string s) {
int len = s.length();
int num = 0;
dfs(0,len,s);
for(int i=0;i<ans.size();i++){
int n=ans[i].size();
num = max(num,n);
}
return num;
}
};
模板77. 组合
给定两个整数 n 和 k,返回范围 [1, n] 中所有可能的 k 个数的组合。
你可以按 任何顺序 返回答案。
class Solution {
vector<int> path;
vector<vector<int>> ans;
void dfs(int k, int n, int idx){
if (path.size() == k) { // 选好了
ans.emplace_back(path);
return;
}
for(int j = idx; j <= n; j++){ //模板循环j从当前下标开始
path.push_back(j);
dfs(k, n, j+1);//模板 j+1
path.pop_back();
}
}
public:
vector<vector<int>> combine(int n, int k) {
dfs(k, n, 1);
return ans;
}
};
排序:// 这是因为数组已排序,后边元素更大,子集和一定超过 target。数组 candidates 有序,也是 深度优先遍历 过程中实现「剪枝」的前提
//将数组先排序的思路来自于这个问题:去掉一个数组中重复的元素。很容易想到的方案是:先对数组 升序 排序,重复的元素一定不是排好序以后相同的连续数组区域的第 1 个元素。
class Solution {
vector<vector<int>> ans;
vector<int> path;
void dfs(vector<int>& candidates, int target, int i){
if(target == 0) {
ans.emplace_back(path);
return;
}
for(int j = i; j < candidates.size(); j++ ){
if(target - candidates[j] < 0) break;// 剪枝 不可能直接break不继续下去了
path.push_back(candidates[j]);
dfs(candidates, target - candidates[j], j); //这里是j的话,实现了 可以重复选;j+1是不可重复
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 为啥要先排序呢?降序的话会报错
dfs(candidates, target, 0);
return ans;
}
};
40. 组合总和 II
给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。
candidates 中的每个数字在每个组合中只能使用 一次 。
注意:解集不能包含重复的组合。
示例 1:
输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]
if(j > start && candidates[j] == candidates[j-1]) continue;
解释:
class Solution {
vector<vector<int>> ans;
vector<int> path;
void dfs(vector<int>& candidates, int target, int start){
if(target == 0) {
ans.emplace_back(path);
return;
}
for(int j = start; j < candidates.size(); j++ ){
if(target - candidates[j] < 0) break;// 剪枝 不可能直接break不继续下去了
if(j > start && candidates[j] == candidates[j-1]) continue;
// j > start 跳过,[1,1,5]这种,后面遇到第二个1的时候没有continue跳过
path.push_back(candidates[j]);
dfs(candidates, target - candidates[j], j + 1); //只用一次
path.pop_back();
}
}
public:
vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
sort(candidates.begin(), candidates.end()); // 为啥要先排序呢?降序的话会报错
dfs(candidates, target, 0);
return ans;
}
};
排序
快排
时间: O ( n l o g n ) O(nlogn) O(nlogn)
void quick_sort(int q[], int l, int r)
{
//递归的终止情况
if(l >= r) return;
//第一步:分成子问题
int i = l - 1, j = r + 1, x = q[l + r >> 1];
while(i < j)
{
do i++; while(q[i] < x);
do j--; while(q[j] > x);
if(i < j) swap(q[i], q[j]);
}
//第二步:递归处理子问题
quick_sort(q, l, j), quick_sort(q, j + 1, r);
//第三步:子问题合并.快排这一步不需要操作,但归并排序的核心在这一步骤
}
快速选择 O ( n ) O(n) O(n):
int quickSort(vector<int>& nums, int l, int r, int k) { //快速选择 第k个(id:k-1)
if(l == r) return nums[k];
int x = nums[l], i = l - 1, j = r + 1;
while(i < j) {
do i ++ ; while(nums[i] > x); //从大往小 降序排
do j -- ; while(nums[j] < x);
if(i < j) swap(nums[i], nums[j]);
}
if(k <= j) return quickSort(nums, l, j, k);//已经确定第k大的元素在前半截
else return quickSort(nums, j+1, r, k);
}
计数排序
时间复杂度
O
(
n
)
O(n)
O(n)
hash是统计每个元素出现次数,s是统计出现次数的个数,比如[1,1,2,2,3] s[2]=2,s[1]=1。后面操作是,从出现次数最大的,累加到k
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int,int> hash;
vector<int> res;
for (int x : nums) hash[x] ++;
int n = nums.size();
vector<int>s(n + 1, 0);
for (auto &p : hash) s[p.second] ++ ;
int i = n, t = 0;
while (t < k) t += s[i -- ];
for (auto &p : hash)
if (p.second > i)
res.push_back(p.first);
return res;
}
};
215. 数组中的第K个最大元素
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
class Solution {
public:
int findKthLargest(vector<int>& nums, int k) {
return quickSelect(nums, k);
}
int quickSelect(vector<int>& nums, int k) {
// 基于快排的快速选择
// 随机选择基准数字
int p = nums[rand() % nums.size()];
// 将大于等于小于的元素分别放入三个数组
vector<int> big, equal, small;
for (int a : nums) {
if (a < p) small.push_back(a);
else if (a == p) equal.push_back(a);
else big.push_back(a);
}
// 第k大元素在big中, 递归划分
if (k <= big.size()) {
return quickSelect(big, k);
}
// 第k大元素在small中, 递归划分
if (big.size() + equal.size() < k) {
return quickSelect(small, k - (big.size() + equal.size()));
}
// 第k大元素在equal中, 返回p
return p;
}
};
链表
21合并两个链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
递归:
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if (l1 == null) {
return l2;
} else if (l2 == null) {
return l1;
} else if (l1.val < l2.val) {
l1.next = mergeTwoLists(l1.next, l2); //
return l1; //
} else {
l2.next = mergeTwoLists(l1, l2.next);
return l2;
}
}
}
作者:力扣官方题解
链接:https://leetcode.cn/problems/merge-two-sorted-lists/solutions/226408/he-bing-liang-ge-you-xu-lian-biao-by-leetcode-solu/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
迭代:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* ListNode *next;
* ListNode() : val(0), next(nullptr) {}
* ListNode(int x) : val(x), next(nullptr) {}
* ListNode(int x, ListNode *next) : val(x), next(next) {}
* };
*/
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
ListNode* pre = new ListNode(-1);
ListNode* res = pre;
while(list1 != nullptr && list2 != nullptr){
if(list1->val < list2->val) {
res->next = list1;
list1 = list1->next;
}
else{
res->next = list2;
list2 = list2->next;
}
res = res->next;
}
if(list1 != nullptr) res->next = list1;
else res->next = list2;
return pre->next;
}
};
原地交换
限制条件:时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1) 允许修改数组
LCR 120. 寻找文件副本
设备中存有 n 个文件,文件 id 记于数组 documents。若文件 id 相同,则定义为该文件存在副本。请返回任一存在副本的文件 id。
提示:
0 ≤ documents[i] ≤ n-1
2 <= n <= 100000
示例 1:
输入:documents = [2, 5, 3, 0, 5, 0]
输出:0 或 5
在一个长度为 n 的数组 nums里的所有数字都在 0 ~ n-1 的范围内 。 此说明含义:数组元素的 索引 和 值 是 一对多 的关系。因此,可遍历数组并通过交换操作,使元素的 索引 与 值一一对应(即 n u m s [ i ] = i nums[i]=i nums[i]=i )。因而,就能通过索引映射对应的值,起到与字典等价的作用。
对于
i
i
i,一直交换swap(nums[i],nums[nums[i]])
,(该交换操作保证 nums[i]
位置上一定是nums[i]
)
直至nums[i]=i (i位置上是i)或者找到重复的元素 i++
找到重复元素:nums[nums[i]]==nums[i] && nums[i] != i
第一次出现是在i位置,第二次出现是在nums[i]位置,所以返回nums[i]
class Solution {
public:
int findRepeatDocument(vector<int>& nums) {
int i=0;
while(i<nums.size()){
if(nums[i]!=i) {// 对于i,一直交换,直至nums[i]=i或者找到重复的元素
if(nums[nums[i]]!=nums[i])//因为都在[0,n)之间,不会越界
swap(nums[i],nums[nums[i]]);
//如果nums[x]==x,这是x第二次出现了,第一次是nums[i]=x
//举例子:[3,?,?,3,;;] nums[0]=3=nums[3]=nums[nums[0]]
else return nums[i];
}
else i++;
}
return -1;
}
};
41. 缺失的第一个正数
给你一个未排序的整数数组 nums ,请你找出其中没有出现的最小的正整数。
请你实现时间复杂度为 O(n) 并且只使用常数级别额外空间的解决方案。
输入:nums = [1,2,0]
输出:3
解释:范围 [1,2] 中的数字都在数组中。
示例 2:
输入:nums = [3,4,-1,1]
输出:2
解释:1 在数组中,但 2 没有。
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int len = nums.size();
for(int i = 0; i < len; i++) {
while(nums[i] != i + 1 && nums[i] > 0 && nums[i] <= len && nums[i] != nums[nums[i]-1])
swap(nums[i],nums[nums[i] - 1]);
}
for(int i = 0; i < len; i++) {
if(nums[i] != i + 1) return i + 1;
}
return len + 1;
}
};
快慢指针
限制条件:时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1) 不允许修改数组
287. 寻找重复数
给定一个包含 n + 1 个整数的数组 nums ,其数字都在 [1, n] 范围内(包括 1 和 n),可知至少存在一个重复的整数。
假设 nums 只有 一个重复的整数 ,返回 这个重复的数 。
你设计的解决方案必须 不修改 数组 nums 且只用常量级 O(1) 的额外空间
不能修改数组,题目给的特殊的数组当作一个链表来看,数组的下标就是指向元素的指针,把数组的元素也看作指针。如 0 是指针,指向 nums[0],而 nums[0] 也是指针,指向 nums[nums[0]].
int point = 0;
while(true){
point = nums[point]; // 等同于 next = next->next;
}
链表中的环:
slow 和 fast 会在环中相遇,先假设一些量:起点到环的入口长度为 m,环的周长为 c,在 fast 和 slow 相遇时 slow 走了 n 步。则 fast 走了 2n 步,fast 比 slow 多走了 n 步,而这 n 步全用在了在环里循环(n%c==0)。
当 fast 和 last 相遇之后,我们设置第三个指针 finder,它从起点开始和 slow(在 fast 和 slow 相遇处)同步前进,当 finder 和 slow 相遇时,就是在环的入口处相遇,也就是重复的那个数字相遇。
为什么 finder 和 slow 相遇在入口
fast 和 slow 相遇时,slow 在环中行进的距离是 n-m,其中 n%c==0
。这时我们再让 slow 前进 m 步——也就是在环中走了 n 步了。而 n%c==0 即 slow 在环里面走的距离是环的周长的整数倍,就回到了环的入口了,而入口就是重复的数字。
我们不知道起点到入口的长度 m,所以弄个 finder 和 slow 一起走,他们必定会在入口处相遇。finder走了m步刚好到入口,slow和finder走的步数一样,也是m,slow走了n+m,也刚好到入口。相遇
class Solution {
public:
//1.将数组等价成链表,快指针走2步慢指针每次1步 直至相遇: n%c==0
//2.再定义finder,从入口开始走直至和slow相遇。目的是使slow前进m步,(相遇时finder肯定走了m步)
int findDuplicate(vector<int>& nums) {
int fast = 0;
int slow = 0;
while(true){
fast = nums[nums[fast]];
slow = nums[slow];
if(fast == slow) break;
}
int finder = 0;
while(finder != slow){
slow = nums[slow];
finder = nums[finder];
if(finder == slow) break;
}
return slow;
}
};
异或
136. 只出现一次的数字
给你一个 非空 整数数组 nums ,除了某个元素只出现一次以外,其余每个元素均出现两次。找出那个只出现了一次的元素。
你必须设计并实现线性时间复杂度的算法来解决此问题,且该算法只使用常量额外空间。
a^0 = a; a^a = 0
a1^a2^a3^a4^a5^a6...^a_{2m} ^a_{2m+1} = a_{2m+1}
假设a1-2m中每个元素出现两次,a2m+1出现一次
class Solution {
public:
int singleNumber(vector<int>& nums) {
int ans = 0;
for(int num : nums){
ans = ans ^ num;
}
return ans;
}
};
二分
第一段满足一个性质,第二段都不满足该性质,通过二分找出边界点(满足该性质的最后一个数)
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// lower_bound 返回最小的满足 nums[i] >= target 的 i
def lower_bound(nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1 # 闭区间 [left, right]
while left <= right: 区间不为空# 闭区间写法
# 循环不变量:
# nums[left-1] < target
# nums[right+1] >= target
mid = (left + right) // 2
if nums[mid] < target:
left = mid + 1 # 范围缩小到 [mid+1, right]
else:
right = mid - 1 # 范围缩小到 [left, mid-1]
return left # 或者 right+1
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (nums[mid] >= target) r = mid; // 找>=target的第一个
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
69. x 的平方根
给你一个非负整数 x ,计算并返回 x 的 算术平方根 。
由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。
注意:不允许使用任何内置指数函数和算符,例如 pow(x, 0.5) 或者 x ** 0.5 。
class Solution {
public:
int mySqrt(int x) {
if(x == 0) return 0;
if(x < 4) return 1;
int left = 0;
int right = x - 1;
while(left < right){
int mid = (left + right) / 2;
// 注意:这里为了避免乘法溢出,改用除法
if(mid > x / mid) {
right = mid;
}
else{
left = mid + 1;
}
}
return left - 1;//以前返回left的时候是left和right是数组的索引,这次是数,不一样
}
};
两次二分:
第一次二分:定位到所在行(从上往下,找到最后一个满足 mat[x]][0] <= t 的行号)
第二次二分:从所在行中定位到列(从左到右,找到最后一个满足 mat[row][x] <= t 的列号)
要先找行。因为target可能大于第0行的最大的,先遍历会找不到
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int rl = 0, rr = m - 1;
int cl = 0, cr = n -1;
while(rl < rr){
int rmid = (rl + rr + 1) >> 1;
if(matrix[rmid][0] <= target) rl = rmid;
else rr = rmid - 1;
}
int row = rl;
if(matrix[row][0] == target) return true;
if(matrix[row][0] > target) return false;
while(cl < cr){
int cmid = (cl + cr + 1) >> 1;
if(matrix[row][cmid] <= target) cl = cmid;
else cr = cmid - 1;
}
int col = cl;
return matrix[row][col] == target;
}
};
改进成一次二分查找,利用性质:一维和二维矩阵的对应
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int l = 0, r = m*n - 1;
while(l < r){
int rmid = (l + r + 1) >> 1;
if(matrix[rmid/n][rmid%n] <= target) l = rmid;
else r = rmid - 1;
}
if(matrix[l/n][l%n] == target) return true;
return false;
}
240. 搜索二维矩阵 II
编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性:
每行的元素从左到右升序排列。
每列的元素从上到下升序排列。
因为没有下一行肯定比上一行都大这个性质,用不了二次二分
使用二分的话,时间复杂度:O(mlogn)。要对每一行使用二分查找的时间复杂度为 O(logn),最多需要进行 m 次二分查找。
抽象BST,也是Z 字形查找: 每次搜索可以排除一行或一列的元素。
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
for(int i = 0, j = n -1; i < m && j >= 0; ){
if(matrix[i][j] > target) j--;
else if(matrix[i][j] < target) i++;
else return true;
}
return false;
}
};
动态规划(贪心)
45.跳跃游戏 II
给定一个长度为 n 的 0 索引整数数组 nums。初始位置为 nums[0]。
每个元素 nums[i] 表示从索引 i 向前跳转的最大长度。换句话说,如果你在 nums[i] 处,你可以跳转到任意 nums[i + j] 处:
0 <= j <= nums[i]
i + j < n
返回到达 nums[n - 1] 的最小跳跃次数。生成的测试用例可以到达 nums[n - 1]。
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> f(n);
for(int i = 1, last = 0; i < n; i ++ ) {
while(i > last + nums[last]) last ++ ;//后面的i无法经由last跳到了(这里都是位置,下标)
f[i] = f[last] + 1;//f[i]:从0到i需要的最短步数
//if(last == i) return false; 说明无法跳到i位置了,也就无法跳到最后
}
return f[n-1];
}
};
迷宫问题爆搜模板
class Solution {
bool dfs(vector<vector<char>>& board, int u, string word, int i, int j) {
if(board[i][j] != word[u]) return false;
if(u == word.size() - 1)
return true;
int X[4] = {1, 0, -1,0}, Y[4] = {0, 1, 0, -1};
char tmp = board[i][j];
board[i][j] = '*';
for(int t = 0; t < 4; t ++ ) {
int nx = i + X[t], ny = j + Y[t];
if(nx>=0 && ny>=0 && nx<board.size() && ny<board[0].size()) {
if(dfs(board, u + 1, word, nx, ny)) return true;
}
}
board[i][j] = tmp;
return false;
}
public:
bool exist(vector<vector<char>>& board, string word) {
for(int i = 0; i < board.size(); i ++ )
for(int j = 0; j < board[0].size(); j ++ )
if(dfs(board, 0, word, i, j))
return true;
return false;
}
};
200.岛屿数量
还是像爆搜一样的遍历,找到1,然后dfs,将所有相连的1变为0,找完所有相连的返回。
【注意 这个不用恢复现场】
class Solution {
bool dfs(vector<vector<char>>& grid, int x, int y) {
if(grid[x][y] == '0') return false;
int X[4] = {1,0,-1,0}, Y[4] = {0,1,0,-1};
grid[x][y] = '0';
for(int i = 0; i < 4; i ++ ) {
int a = x + X[i], b = y + Y[i];
if(a >= 0 && a < grid.size() && b >= 0 && b < grid[0].size()) {
if(dfs(grid, a, b)) return true;
}
}
return false;
}
public:
int numIslands(vector<vector<char>>& grid) {
int ans = 0;
for(int i = 0; i < grid.size(); i ++ )
for(int j = 0; j < grid[0].size(); j ++)
if(grid[i][j] == '1') {
ans ++;
dfs(grid, i, j);
}
return ans;
}
};
求面积
public int maxAreaOfIsland(int[][] grid) {
int res = 0;
for (int r = 0; r < grid.length; r++) {
for (int c = 0; c < grid[0].length; c++) {
if (grid[r][c] == 1) {
int a = area(grid, r, c);
res = Math.max(res, a);
}
}
}
return res;
}
int area(int[][] grid, int r, int c) {
if (!inArea(grid, r, c)) {
return 0;
}
if (grid[r][c] != 1) {
return 0;
}
grid[r][c] = 2;
return 1
+ area(grid, r - 1, c)
+ area(grid, r + 1, c)
+ area(grid, r, c - 1)
+ area(grid, r, c + 1);
}
boolean inArea(int[][] grid, int r, int c) {
return 0 <= r && r < grid.length
&& 0 <= c && c < grid[0].length;
}
BFS
class Solution {
int ans = -1;
int fresh = 0;
void bfs(vector<vector<int>> &grid){
queue<pair<int, int>> q;
//多源BFS
for(int i = 0; i < grid.size(); i ++)
for(int j = 0; j < grid[0].size(); j ++){
if(grid[i][j] == 2) q.push({i,j});
if(grid[i][j] == 1) fresh ++;
}
int dx[4] = {1,0,-1,0}, dy[4] = {0,1,0,-1};
while(!q.empty()) {
ans ++; //为啥以-1开始,这时候是0分钟
int sz = q.size();
while(sz--) {
auto t = q.front(); q.pop();
for(int i = 0; i < 4; i ++) {
int x = t.first + dx[i], y = t.second + dy[i];
if(x < 0 || x >= grid.size() || y < 0 || y >= grid[0].size()) continue;
if(grid[x][y] == 1) {
fresh --;
q.push({x,y}); grid[x][y] = 2;
}
}
}
}
}
public:
int orangesRotting(vector<vector<int>>& grid) {
bfs(grid);
return fresh ? -1 : max(ans, 0);;
}
};
拓扑排序
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
int n = prerequisites.size();
vector<int> result;
vector<int> inDegree(numCourses, 0);//记录入度数
unordered_map<int, vector<int>> umap;//数据结构中的啥表啊
for (int i = 0 ; i < n ; i ++ ) {
inDegree[prerequisites[i][1]] ++;
umap[prerequisites[i][0]].push_back(prerequisites[i][1]);
}
queue<int> que;
for (int i = 0 ; i < numCourses ; i ++ ) {
if (inDegree[i] == 0) {
que.push(i);
}
}
while(que.size()) {
int cur = que.front();
que.pop();
result.push_back(cur);
vector<int> files = umap[cur];
if (files.size()) {
for (int i = 0 ; i < files.size() ; i ++ ) {
inDegree[files[i]] --;
if (inDegree[files[i]] == 0) que.push(files[i]);
}
}
}
if (result.size() == numCourses) {
return true;
}else {
return false;
}
}
};
法2:DFS是否有环
class Solution {
bool dfs(unordered_map<int, vector<int>>& mPre, vector<int>& inDegree, vector<int> mVisit, int i) {
if(mVisit[i] == 1) return false;
if(inDegree[i] == 0) return true;
mVisit[i] = 1;//这错了,你咋知道i就可以被访问(即i的入读不一定是0) 怎么改啊
for(int k = 0; k < mPre[i].size(); k ++ ) {
inDegree[mPre[i][k]]--;
if(!dfs(mPre, inDegree, mVisit, mPre[i][k])) return false;
}
mVisit[i] = 0;
return true;
}
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<int> m_visit(numCourses,0);
vector<int> inDegree(numCourses, 0);//记录入度数
unordered_map<int, vector<int>> m_pre;
for(int i = 0; i < prerequisites.size(); i ++) {
m_pre[prerequisites[i][0]].push_back(prerequisites[i][1]); //[1]<-[0]
inDegree[prerequisites[i][1]]++;
}
for(int i = 0; i< numCourses; i ++ ) {
if(!dfs(m_pre, inDegree, m_visit, i)) return false;
}
return true;
}
};
#TODO: 上面法2哪里错了
有三种方式判断图内是否有环,拓扑排序,dfs,和union and find:
dfs相当于判定有没有回溯边,并查集的两端如果已经在同一集合也说明有环
并查集不能用于判断有向图存在环,只能用于无向图
背包
01背包
2915. 和为目标值的最长子序列的长度
给你一个下标从 0 开始的整数数组 nums 和一个整数 target 。
返回和为 target 的 nums 子序列中,子序列 长度的最大值 。如果不存在和为 target 的子序列,返回 -1 。
子序列 指的是从原数组中删除一些或者不删除任何元素后,剩余元素保持原来的顺序构成的数组。
class Solution {
public:
int lengthOfLongestSubsequence(vector<int>& nums, int target) {
//0-1背包问题,f[i][j]表示从前i个物品中选,总量刚好为j 情况下选择的最多物品数量(每件只能选一次)
// f[i][j] = max(f[i-1][j], f[i-1][j-nums[i]]+1) f[i-1][j]:不选第i个物品
vector<vector<int>> f(nums.size()+1, vector<int>(target+1, INT_MIN));
//初始值设为INT_MIN,是让f(j)里的值如果大于0,那肯定是从f(0)递归来的,就是题目说的要完全填满背包的意思。如果不设定这个,是可以全部设为0,表示最多能装几个数字~
f[0][0] = 0;
for(int i = 0; i < nums.size(); i ++ ) {
for(int j = 0; j <= target; j ++ ) {
f[i+1][j] = f[i][j];
if(j >= nums[i]) f[i+1][j] = max(f[i][j], f[i][j-nums[i]]+1); f[i+1][j] = max(f[i][j], f[i][j-nums[i]]+1) 否则nums[i]会溢出
}
}
int res = f[nums.size()][target];
return res > 0 ? res: -1;
}
};
优化:
class Solution {
public:
int lengthOfLongestSubsequence(vector<int>& nums, int target) {
vector<int> f(target+1, INT_MIN);
f[0] = 0;
for(int i = 0; i < nums.size(); i ++ ) {
for(int j = target; j >= nums[i]; j -- ) { //逆序 从target到nums[i]
f[j] = max(f[j], f[j-nums[i]]+1);
}
}
int res = f[target];
return res > 0 ? res: -1;
}
};
二维01背包:设 f(t,i,j)表示考虑了前 t 个物品,体积 为 i 重量为 j 所能得到最大价值
signed main () {
cin >> n >> V >> M;
for (int i = 1; i <= n; i ++) {
cin >> v[i] >> m[i] >> w[i];//体积,重量,价值
}
for (int i = 1; i <= n; i ++)
for (int j = V; j >= v[i]; j --)
for (int k = M; k >= m[i]; k --)
f[j][k] = max (f[j - v[i]][k - m[i]] + w[i], f[j][k]);//动态转移方程,01 背包的思路
cout << f[V][M];
}