前言
来自 英雄哪里出来 的一个 免费 集训,每天
5
5
5 点打卡学习算法(我是为了卷吗,主要是想早起 😏),希望能坚持下去。这里用来复盘每天都的打卡题目。
今日份知识点:滑动窗口
今天的思维强度上去了,除了第三题形象一点,其他几道题都难在想思路上了,都比较巧妙(窝感觉)。
一、题目
题目 | 难度 |
---|---|
1984. 学生分数的最小差值 | ⭐️ |
1763. 最长的美好子字符串 | ⭐️⭐️ |
2269. 找到一个数字的 K 美丽值 | ⭐️ |
995. K 连续位的最小翻转次数 | ⭐️⭐️⭐️ |
二、算法思路
1、学生分数的最小差值
(1)排序:题目所求为任意
k
k
k 个学生的最高分和最低分之差,那么答案必然存在于某个连续段之中,如果最佳
k
k
k 个选择不是连续段,那么可以调整为连续段之后,结果不会变差。
(2)二分:对于某个连续段,最高分与最低分之差为
n
u
m
s
[
i
+
k
−
1
]
−
n
u
m
s
[
i
]
nums[i+k-1]-nums[i]
nums[i+k−1]−nums[i],最大值最小化问题直接二分答案。
时间复杂度:
O
(
(
n
+
1
)
log
n
)
O((n+1)\log{n})
O((n+1)logn)
class Solution {
public:
vector<int> nums;
int k;
int minimumDifference(vector<int>& _nums, int _k) {
nums = _nums;
k = _k;
sort(nums.begin(), nums.end());
int l = 0, r = 100010;
while (l < r) {
int mid = (l + r) >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
return r;
}
bool check(int x) {
int n = nums.size();
int ans = nums[n - 1] - nums[0];
for (int i = 0; i + k - 1 < n && ans > x; ++ i) {
ans = min(ans, nums[i + k - 1] - nums[i]);
}
return ans <= x;
}
};
(3)滑动窗口:对排序后的数组,分别取出窗口内的最大值与最小值的下标,最后一次遍历取最大值与最小值之差的最小值。
时间复杂度:
O
(
n
log
n
+
3
n
)
O(n\log{n}+3n)
O(nlogn+3n)
class Solution {
public:
int minimumDifference(vector<int>& nums, int k) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<int> Max, Min, q(n);
int hh = 0, tt = -1;
for (int i = 0; i < n; ++ i) {
if (hh < tt && i - k + 1 > q[hh]) ++ hh;
while (hh <= tt && nums[i] >= nums[q[tt]]) -- tt;
q[++ tt] = i;
if (i >= k - 1) Max.push_back(q[hh]);
}
hh = 0, tt = -1;
q.clear();
for (int i = 0; i < n; ++ i) {
if (hh < tt && i - k + 1 > q[hh]) ++ hh;
while (hh <= tt && nums[i] <= nums[q[tt]]) -- tt;
q[++ tt] = i;
if (i >= k - 1) Min.push_back(q[hh]);
}
int ret = 1e9;
for (int i = 0; i < Max.size(); ++ i) {
ret = min(ret, nums[Max[i]] - nums[Min[i]]);
}
return ret;
}
};
(4)直接取结果:排序完之后,每个窗口内的值都是一个连续段,然后这个连续段内的最大值与最小值之差为
n
u
m
s
[
i
+
k
−
1
]
−
n
u
m
s
[
i
]
nums[i+k-1]-nums[i]
nums[i+k−1]−nums[i],遍历数组,求出答案。
时间复杂度:
O
(
n
log
n
+
n
)
O(n\log{n}+n)
O(nlogn+n)
class Solution {
public:
int minimumDifference(vector<int>& nums, int k) {
sort(nums.begin(), nums.end());
int n = nums.size();
int ans = nums[n - 1] - nums[0];
for (int i = k - 1; i < n; ++ i) {
ans = min(ans, nums[i] - nums[i - k + 1]);
}
return ans;
}
};
2、最长的美好子字符串
(1)枚举:枚举所有的子串,然后对子串进行合法性检查。
时间复杂度:
O
(
n
3
)
O(n^3)
O(n3)
class Solution {
public:
string longestNiceSubstring(string s) {
string ans = "";
int len = -1;
for (int i = 0; i < s.size(); ++ i) {
for (int j = i + 1; j < s.size(); ++ j) {
string str = s.substr(i, j - i + 1);
if (j - i + 1 > len && check(str)) {
len = j - i + 1;
ans = str;
}
}
}
return ans;
}
bool check(string s) {
set<char> hashset;
for (auto c: s) {
hashset.insert(c);
}
for (auto c: hashset) {
if (c >= 'a' && c <= 'z')
c -= 32;
else
c += 32;
if (!hashset.count(c))
return false;
}
return true;
}
};
(2)二进制优化:int
有
4
4
4 个字节,共
32
32
32 位,那么使用第
26
26
26 位来记录每个字母是否出现。小写用
a
a
a,大写用
b
b
b,如果某一段字母中,
a
=
=
b
a==b
a==b,那么,该段字符串为美好字符串。
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
class Solution {
public:
string longestNiceSubstring(string s) {
string ans = "";
int len = -1;
for (int i = 0; i < s.size(); ++ i) {
int a = 0, b = 0;
for (int j = i; j < s.size(); ++ j) {
if (s[j] >= 'a' && s[j] <= 'z')
a |= 1 << (s[j] - 'a');
else if (s[j] >= 'A' && s[j] <= 'Z')
b |= 1 << (s[j] - 'A');
if (a != 0 && a == b) {
if (j - i + 1 > len) {
len = j - i + 1;
ans = s.substr(i, len);
}
}
}
}
return ans;
}
};
(3)分治:枚举下标从起点
b
e
g
beg
beg 到末尾
e
n
d
end
end 的字符串,如果某个字符
s
[
i
]
s[i]
s[i] 不符合最美字符串的要求,那么符合条件的最美字符串所在范围为
[
b
e
g
,
i
−
1
]
,
[
i
+
1
,
e
n
d
]
[beg,i-1],[i +1,end]
[beg,i−1],[i+1,end] 这两个范围,再次进行递归,返回一个更长更早出现的最美字符串。
时间复杂度:
O
(
n
2
)
O(n^2)
O(n2)
class Solution {
public:
string longestNiceSubstring(string s) {
function<string(int, int)> dfs = [&] (int beg, int end)->string {
if (beg >= end)
return "";
int a = 0, b = 0;
for (int i = beg; i <= end; ++ i) {
if (s[i] >= 'a' && s[i] <= 'z')
a |= 1 << (s[i] - 'a');
else
b |= 1 << (s[i] - 'A');
}
int spilt = -1;
int c = a & b;
for (int i = beg; i <= end; ++ i) {
int id = s[i] >= 'a' && s[i] <= 'z' ? s[i] - 'a' : s[i] - 'A';
if (((c >> id) & 1) == 0) {
spilt = i;
break;
}
}
if (spilt == -1) {
return s.substr(beg, end - beg + 1);
}
string left = dfs(beg, spilt - 1), right = dfs(spilt + 1, end);
return left.size() >= right.size() ? left : right;
};
return dfs(0, s.size() - 1);
}
};
3、找到一个数字的 K 美丽值
(1)枚举:将
n
u
m
num
num 转为
s
t
r
i
n
g
string
string 类型,枚举字符串,截取一段长为
k
k
k 的子字符串,使用
a
t
o
i
atoi
atoi 函数将该子字符串转为
i
n
t
int
int 类型,随后判断一下是否可以整除即可。
时间复杂度:
O
(
n
)
O(n)
O(n)
class Solution {
public:
int divisorSubstrings(int num, int k) {
string s = to_string(num);
int ans = 0;
for (int i = 0; i + k <= s.size(); ++ i) {
int div = atoi(s.substr(i, k).c_str());
if (div != 0 && num % div == 0)
++ ans;
}
return ans;
}
};
4、K 连续位的最小翻转次数
基本思路是 贪心,枚举数组,每当遇到
0
0
0,即把包括当前位置的数字共
k
k
k 个数字与
1
1
1 做异或运算。如果发现
0
0
0 的位置及其后面的数组长度不足
k
k
k,那么满足条件,返回
−
1
-1
−1;当数组全部枚举完,返回翻转的次数。
(1)枚举:每次找到为
0
0
0 的数字即把包括其在内的之后
k
k
k 个数字与
1
1
1 做异或运算。
时间复杂度:
O
(
k
∗
n
)
O(k*n)
O(k∗n),在极限数据的情况下,运算次数为
1
0
10
10^{10}
1010,会 TLE 。超时的原因就是对于每一个需要翻转的数都
真
真
实
实
真真实实
真真实实 的进行了翻转操作。
class Solution {
public:
int minKBitFlips(vector<int>& nums, int k) {
int n = nums.size();
int ret = 0;
for (int i = 0; i < n; ++ i) {
if (nums[i] == 0) {
if (i + k > n)
return -1;
ret ++;
for (int j = i; j - i < k; ++ j) {
nums[j] ^= 1;
}
}
}
return ret;
}
};
(2)差分前缀和优化:通过寻找规律我们得知,每个数翻转 偶数 次之后依旧是其本身;翻转 奇数 次之后,0 变 1,1 变 0。
① 那么我们只需要记录每个数字的 翻转次数
来确定当前的数字的值,再进行操作。翻转操作的对象是一段区间内的数字,所以自然想到了 差分
来记录翻转次数,该差分数组的 前缀和
就是当前数字的翻转次数。差分数组 arr[l+1]++,arr[l+k]--
代表了从
l
+
1
l+1
l+1 到
l
+
k
−
1
l+k-1
l+k−1 位置的翻转次数
+
1
+1
+1。
② 如果
n
u
m
s
[
i
]
nums[i]
nums[i] 是
奇
数
1
奇数_1
奇数1,我们需要其的翻转次数为 偶数
;如果
n
u
m
s
[
i
]
nums[i]
nums[i] 是
偶
数
0
偶数_0
偶数0,那么我们需要其的翻转次数为 奇数
。
③ 如果
n
u
m
s
[
i
]
nums[i]
nums[i] 是 1
,
c
n
t
cnt
cnt 是奇数, 那么需要再次进行翻转,反之不需要;如果
n
u
m
s
[
i
]
nums[i]
nums[i] 是 0
,
c
n
t
cnt
cnt 是 偶数,那么需要再次翻转,反之不需要。
④ 发现规律:如果
n
u
m
s
[
i
]
+
c
n
t
nums[i]+cnt
nums[i]+cnt 是 偶数
的话需要再次翻转,否则不需要。
时间复杂度:
O
(
n
)
O(n)
O(n)
class Solution {
public:
int minKBitFlips(vector<int>& nums, int k) {
int ret = 0;
vector<int> arr(nums.size() + 3, 0);
int cnt = 0;
for (int i = 0; i < nums.size(); ++ i) {
cnt += arr[i];
if ((cnt + nums[i]) % 2 == 0) {
if (i + k > nums.size()) {
return -1;
}
ret ++;
arr[i + 1] ++;
arr[i + k] --;
}
}
return ret;
}
};
(3)队列优化:使用一个队列保存需要翻转的起点,队列内的元素个数就是每个数字需要翻转的次数。
class Solution {
public:
int minKBitFlips(vector<int>& nums, int k) {
int ret = 0;
vector<int> q(100010);
int hh = 0, tt = 0;
for (int i = 0; i < nums.size(); ++ i) {
if (hh < tt && q[hh] + k <= i) ++ hh;
int cnt = tt - hh;
if ((nums[i] + cnt) % 2 == 0) {
if (i + k > nums.size())
return -1;
++ ret;
q[tt ++] = i;
}
}
return ret;
}
};
(4)既然是用到了 贪心,那么说什么都要证明以下其的正确性。
归纳推理:遇到
0
0
0 马上进行翻转得到最优解。
① 假设前
i
−
1
i-1
i−1 个元素已经全是
1
1
1,第
i
i
i 个元素是
0
0
0。如果要将前
i
i
i 个元素都变为
1
1
1,那么需要将第
i
i
i 个元素翻转 奇数
次,由于前
i
−
1
i-1
i−1 个元素已经是
1
1
1,那么只需要将窗口头部为
i
i
i 的窗口内的元素翻转
1
1
1 次即可将前
i
i
i 个元素变为
1
1
1,这是代价最小的做法。
② 如果第
i
i
i 个元素是
1
1
1,窗口头部滑过时不会进行操作,这样是把前
i
i
i 个元素变为
1
1
1 的最小代价。
③ 现在前
i
i
i 个元素已经是
1
1
1,扩大规模,那么将前
i
+
1
i+1
i+1 个元素前变为
1
1
1 的操作的步骤同上,是最小代价做法。
结语
今天的题对我来说挺难的,可能是之前基本没做过相关的题目,知识是学过的,但是没有用过,所以想题的时候抽象一点就没思路了,盯着屏幕在发呆,然后看题解找思路然后自己按照思路敲代码👊希望有点用。早上有点起不来了😣感觉,今晚早点睡,不刷视频了。