1658. 将 x 减到 0 的最小操作数
1658. 将 x 减到 0 的最小操作数 - 力扣(LeetCode)
示例 3:
输入:nums = [3,2,20,1,1,3], x = 10
输出:5
解释:最佳解决方案是移除后三个元素和前两个元素(总共 5 次操作),将 x 减到 0 。
如果想直接找到这些元素是比较困难的,所以:
正难则反
找到两个区域,a在左边,b在右边是的a + b = x。
[[ ] [ ]]
a b
创建一个sum,sum是整个数组的和
这时可以考虑中间的这个区域,使得sum - x,使中间这块连续的区域最长
转化:找出最长的子数组的长度,所有元素的和正好等于sum - x
len target
n - len
解法一:暴力枚举
target = sum - x
[ < target ] R(>=target)
[ ] ] ]
L
sum1 来标记L和R所指的区域的和
让R依次向后移动找那个最佳的位置
如果R找到一个元素使得前面的和 >= target,那么那个元素之前的元素之和肯定 < target
暴力解法的话,就让L右移一步,然后R回到L这里
但是R还有必要向左移动吗?
其实没有必要,本身前面的区域就小于target了,这时删除了第一个元素肯定还是小于target
所以R没有必要回去,所以让R要么原地不动,要么向后移动
解法二:滑动窗口
1.left = 0,right = 0
2.进窗口 -> sum1 += nums[right]
3.判断 -> 当这段区域的和 > target 就出窗口
出窗口 -> sum1 -= nums[left]
更新结果 -> 如果这段区域的和 = target 再更新结果
最后再用n减一下,得出最后结果
时间复杂度:O(N)
空间复杂度:O(1)
代码:C++
cpp
class Solution {
public:
int minOperations(vector<int>& nums, int x) {
int sum = 0; // 整个数组的和
for(int a : nums) sum += a; // 把里面每个数都拿出来然后+=
int target = sum - x;
// 细节问题
if(target < 0) return -1;
int ret = -1; // 如果没有找到结果,返回-1,所以直接赋值-1
for(int left=0, right=0, tmp=0; right < nums.size(); right++)
{
// 进窗口
tmp += nums[right];
while(tmp > target) // 判断
{
tmp -= nums[left++]; // 出窗口
}
if(tmp == target) // 更新结果
{
ret = max(ret, right - left + 1);
}
}
if(ret == -1) return ret;
else return nums.size() - ret;
}
};
904. 水果成篮
等于是连续摘
转化:找出一个最长的子数组长度,子数组中不超过两种类型的水果
fruits = [1,2,3,2,2]
解法一:暴力枚举 + 哈希表
[1,2,3,2,2]
比如固定1,那就只能找到1,2
如果固定2,可以采摘到2,3,2,2
...
找出所有可能性,然后选择最长的
可以借助哈希表储存水果类型,统计水果一共出现多少种
解法二:滑动窗口
比如:
R
f = [1, 2, 1, 2, 3, 2, 3, 3]
L
hash<int,int> 不仅要存入水果的种类,还要存入水果的数量
-
left = 0,right = 0
-
进窗口 -> hash[f[Right]]++
-
判断 -> hash.size > 2
出窗口 -> hash[f[Left]]--,要判断一下,如果对应的位置的元素变成0,要从哈希表中删除
更新结果
代码:C++
cpp
class Solution {
public:
int totalFruit(vector<int>& fruits) {
unordered_map<int, int> hash; // 统计窗口内出现了多少种水果
int ret = 0;
for(int left = 0, right = 0; right < fruits.size(); right++)
{
hash[fruits[right]]++; // 进窗口
while(hash.size() > 2) // 判断
{
hash[fruits[left]]--; // 出窗口
if(hash[fruits[left]] == 0)
{
hash.erase(fruits[left]); // 变成0就从哈希表中删除
}
left++;
}
ret = max(ret, right - left + 1); // 结果应该是left和right所指区间的长度
}
return ret;
}
};
时间复杂度比较耗时,所以可以对哈希表做一个优化
cpp
// 优化后:
// 因为
//1 <= fruits.length <= 10^5
//0 <= fruits[i] < fruits.length
//所以可以使用一个数组来代表哈希表
int hash[100001] = {0};
//
for (int left = 0, right = 0, kinds = 0; right < fruits.size(); right++)
if(hash[fruits[right]] == 0) kinds++; // 维护水果的种类
while (kinds > 2)
hash.erase(fruits[left]); -> if(hash[fruits[left]] == 0) kinds--;
// 如果数据范围是有限的,可以使用数组来替代哈希表,比直接使用容器效率的提升很大
438. 找到字符串中所有字母异位词
438. 找到字符串中所有字母异位词 - 力扣(LeetCode)
1. 暴力解法
- 如何快速判断两个字符串是否是"异位词"
s1 = "aabca"
s2 = "abaca"
用排序然后再一一比较的方法太耗时了
所以可以利用哈希表
遍历s1的字符,把字符依次丢到hash1
遍历s2的字符,把字符依次丢到hash2
然后比较两个哈希表中对应位置字符出现的个数是否相等,相等就是异位词
2.解决问题
比如:
s = "cbaebabacd"
以c为起点,找一个长度为m的子串,
把子串里面每个字符出现的个数丢到另一个哈希表hash2里面
然后遍历完这个子串之后,比较一下这两个哈希表是否相等就可以
然后以b为起点...
p = "abc",长度为m,hash1
hash1统计p里面的个数
hash2统计滑动窗口里面每个字符出现的个数
如何优化呢
每次枚举的时候,只需要把第一个字符删除,然后添加下一个字符就可以了
所以没有必要重新从头开始统计信息
s = "c b a e b a b a c d"
L R
s = "c b a e b a b a c d"
L R
解法:滑动窗口 + 哈希表
-
left = 0, right = 0
-
进窗口 -> hash2[in]++
-
判断 -> right - left + 1 > m,就移动
出窗口 -> hash2[out]--
更新结果 -> check(hash2, hash1)
优化:可以对更新结果的判断条件(check)进行优化
利用变量count来统计窗口中"有效字符"的个数
s = "ccbaebabacd",hash2
p = "abc",hash1: a -> 1, b -> 1, c -> 1
进窗口:进入后 -> 判断此时hash2[in]是否小于等于hash1[in]
如果小于等于,就是添加的有效字符,count++
如果是其余情况,就是无效字符,count不变
出窗口:出去前 -> hash2[out] <= hash1[out] -> count--
其余情况count不变
更新结果:count == m
代码:C++
cpp
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> ret;
int hash1[26] = { 0 }; // 统计字符串p中每个字符出现的个数
for(auto ch : p) hash1[ch - 'a']++; // ch - 'a'对应位置的下标
int hash2[26] = { 0 }; // 统计窗口里面每一个字符出现的个数
int m = p.size();
for(int left=0, right=0, count=0; right < s.size(); right++)
{
char in = s[right];
//hash2[in - 'a']++; // 进窗口
// 维护一下count,可以在hash2前面添加++
if(++hash2[in - 'a'] <= hash1[in - 'a']) count++; // 进窗口+维护
if(right - left + 1 > m) // 判断
{
char out = s[left++]; // 元素搞出去之后,left向右移动一步
if(hash2[out - 'a']-- <= hash1[out - 'a']) count--; // 出窗口 + 出去前维护count
//hash2[out - 'a']--; // 出窗口
}
// 更新结果
if(count == m) ret.push_back(left);
}
return ret;
}
};
30. 串联所有单词的子串
s = "barfoothefoobarman", words = ["foo","bar"] words里面的字符串长度都为3
s = "[bar][foo][the][foo][bar][man]"
b a c a b d
跟438. 找出所有字母的异位词 很像
滑动窗口 + 哈希表
不同之处:
- 哈希表
hash<string, int> -> string对应字符,int对应字符串出现的次数
- left与right指针的移动
移动的步长,是给定字符串里面每个单词的长度(len)
- 滑动窗口执行的次数
len
代码:C++
cpp
class Solution {
public:
vector<int> findSubstring(string s, vector<string>& words) {
unordered_map<string, int> hash1; // 保存words 里面所有单词的频次
for(auto& s: words) hash1[s]++; // 遍历字符串数组
vector<int> ret;
int len = words[0].size(), m = words.size();
for(int i = 0; i<len; i++) // 执行len次
{
// 这个哈希表维护滑动窗口内面单词的频次
unordered_map<string, int> hash2;
for(int left=i, right=i, count=0; right + len <= s.size(); right+=len) // 因为是要把后面的字符串放到滑动窗口里面,如果right太靠后,后面根本就没有长度为len的字符串
{
// 进窗口 + 维护count
string in = s.substr(right,len);
hash2[in]++;
// 第一个哈希表不一定有这个单词,这样的话就会重新创建一个in,丢到哈希表
// 所以会有时间消耗
// 如果in出现在第一个哈希表里面的时候才接下来再判断
// 因为此时判断的时候这个in一定是在里面了,所以就不用重新再创建一个in在哈希表里面了
if(hash1.count(in) && hash2[in] <= hash1[in]) count++; // 可以提前判断一下,节省时间
// 判断
if(right - left + 1 > len * m) // len * m 窗口里面所有字符串的长度
{
// 出窗口 + 维护count
string out = s.substr(left, len);
if(hash1.count(out) && hash2[out] <= hash1[out]) count--; // 有效单词
hash2[out]--;
// 出窗口后,left向后移动len步
left += len;
}
// 更新结果,当有效字符的个数正好等于字典里面单词的个数时
if(count == m) ret.push_back(left);
}
}
return ret;
}
};