LeetCode 第31场双周赛
本周双周赛又是一次福利场,无奈手速不够快
1. 在区间范围内统计奇数数目
给你两个非负整数 low 和 high 。请你返回 low 和 high 之间(包括二者)奇数的数目。
示例 1:
输入:low = 3, high = 7
输出:3
解释:3 到 7 之间奇数数字为 [3,5,7] 。
示例 2:
输入:low = 8, high = 10
输出:1
解释:8 到 10 之间奇数数字为 [9] 。
解析
本题要求区间内(左右都是闭区间)的奇数数量。实际上奇数大约占一段连续数字的一半,具体会有1个数的差异,取决于边界值。可以分三种情况讨论,两端都是偶数、一奇一偶、两个奇数。举例子看一下,
两个偶数,比如4,8,中间有5,7两个奇数,而(8-4)/2=2,恰好为两端差值的一半;
一奇一偶,比如5,8,中间有5,7两个奇数,而(8-5)/2=1(按照向下取整除法),比区间内奇数少一个,要加1;
两个奇数,比如5,9,中间5,7,9三个奇数,而(9-5)/2=2,少一个,要加1。
综上,两端都是偶数,直接求差除以2即可;只要其中一个是奇数,就需要再加1.
C++代码如下:
int countOdds(int low, int high) {
int ans = (high - low) / 2;
if (low % 2 == 1 || high % 2 == 1) ans++;
return ans;
}
2. 和为奇数的子数组数目
给你一个整数数组 arr 。请你返回和为 奇数 的子数组数目。
由于答案可能会很大,请你将结果对 10^9 + 7 取余后返回。
示例 1:
输入:arr = [1,3,5]
输出:4
解释:所有的子数组为 [[1],[1,3],[1,3,5],[3],[3,5],[5]] 。
所有子数组的和为 [1,4,9,3,8,5].
奇数和包括 [1,9,3,5] ,所以答案为 4 。
示例 2 :
输入:arr = [2,4,6]
输出:0
解释:所有子数组为 [[2],[2,4],[2,4,6],[4],[4,6],[6]] 。
所有子数组和为 [2,6,12,4,10,6] 。
所有子数组和都是偶数,所以答案为 0 。
解析
本题最暴力解法就是找到所有的子数组判断和是否是奇数,这样复杂度是
O
(
N
2
)
O(N^2)
O(N2)。
优化一下,可以想到,使用累计和的方式能计算从头加到当前数的和,以当前数字结尾的子数组和实际上就是这个和减掉之前的每一个和,同样这样还是
O
(
N
2
)
O(N^2)
O(N2),但注意,我们并不需要每个子数组的和,只需要知道是技术还是偶数就行了。所以如果0-i的累计和与0-j的累计和奇偶性不同,二者之差必为奇数,也就是i+1到j这个子数组和为奇数。
因此,我们采用动规的方法,两个数组odd和even分别记录到当前数为止的累计和中出现的奇数次数和偶数次数。也即odd[i]表示从0到i的每个累计和有几个奇数,even[i]表示0到i的每个累计和有几个偶数,当计算到i+1时,首先我们在sum的基础上加上i+1,如果此时sum是偶数,就去找odd[i],如果odd[i]=k,意味着以0到i+1和是偶数,而在0-i范围内存在k个数使得从0加到这些位置和为奇数,那从这k个位置的下一个位置到i+1的子数组之和一定是奇数(偶 - 奇 = 奇);同理此时sum为奇数,就去找前一个位置even保存的值(奇 - 偶 = 奇)。然后总的结果数上加上这个值。
遍历完数组得到的总结果数为所求。
为了处理从下标0开始的子数组,在数组最前面增加一个元素0,不影响累计和与奇偶性。
C++代码如下;
int numOfSubarrays(vector<int>& arr) {
int len = arr.size();
int mod = 1e9 + 7;
vector<int>odd(len+1, 0);
vector<int>even(len + 1, 0);
odd[0] = 0;
even[0] = 1;
int ans = 0;
int sum = 0;
for (int i = 1; i <= len; ++i) {
sum += arr[i - 1];
if (sum % 2 == 0) {
ans = (ans + odd[i - 1]) % mod;
odd[i] = odd[i - 1];
even[i] = even[i - 1] + 1;
}
else {
ans = (ans + even[i - 1]) % mod;
odd[i] = odd[i - 1]+1;
even[i] = even[i - 1];
}
}
return ans;
}
3. 字符串的好分割数目
给你一个字符串 s ,一个分割被称为 「好分割」 当它满足:将 s 分割成 2 个字符串 p 和 q ,它们连接起来等于 s 且 p 和 q 中不同字符的数目相同。
请你返回 s 中好分割的数目。
示例 1:
输入:s = “aacaba”
输出:2
解释:总共有 5 种分割字符串 “aacaba” 的方法,其中 2 种是好分割。
(“a”, “acaba”) 左边字符串和右边字符串分别包含 1 个和 3 个不同的字符。
(“aa”, “caba”) 左边字符串和右边字符串分别包含 1 个和 3 个不同的字符。
(“aac”, “aba”) 左边字符串和右边字符串分别包含 2 个和 2 个不同的字符。这是一个好分割。
(“aaca”, “ba”) 左边字符串和右边字符串分别包含 2 个和 2 个不同的字符。这是一个好分割。
(“aacab”, “a”) 左边字符串和右边字符串分别包含 3 个和 1 个不同的字符。
示例 2:
输入:s = “abcd”
输出:1
解释:好分割为将字符串分割成 (“ab”, “cd”) 。
s 只包含小写英文字母。
1 <= s.length <= 10^5
解析
本题要将一个字符串分为两部分,要求左右两部分不同字符的数量相等,求这样分割的数量。
由于本题强调了字符串仅有小写字母组成,所以要统计每个字母出现的数量只需要长度为26的数组,而统计不同字符数量,只需要遍历这个26长度的数组找到其中大于0的元素数量即可。
本题的暴力解法显然是遍历所有分割,每次统计一遍不同字符数量,显然是
O
(
N
2
)
O(N^2)
O(N2)复杂度,会超时。但实际上,我们并没有必要对每个分割都从头统计字符数量,因为每移动一位,比如从左向右,实际上是左边加了一个字符,右边减了一个。
因此我们建立两个数组长度都为26,分别记录左右两个子串各个字符出现次数。通过calDiffNum函数计数不同字符数量,也就是数组中大于零的数量。
首先遍历一遍字符串,将所有字符都算在右边(这里从左向右移动)。然后从字符串第一个字符开始,每移动到一个位置,就意味着将这个元素放在左边,因此左边字符出现次数的数组对这个字符加一,右边减一,然后判断当前的分割是不是左右不同字符数量一致,是的话就对结果加一。然后继续移动。
C++代码如下:
int calDiffNum(vector<int>&m) {
int ans = 0;
for (auto n : m) {
if (n > 0)++ans;
}
return ans;
}
int numSplits(string s) {
vector<int>left(26, 0), right(26, 0);
for (auto c : s) {
right[c - 'a']++;
}
int ans = 0;
for (int i = 0; i < s.size(); ++i) {
left[s[i] - 'a']++;
right[s[i] - 'a']--;
if (calDiffNum(left) == calDiffNum(right)) ++ans;
}
return ans;
}
本题还可以继续优化,显然每次都数一遍两个数组还是太浪费了,因为移动一位只造成一次变化,所以可以新建两个整型变量分别记录左右两部分不同的字符数量,当移动指针时,左边这个字符出现次数加一,那么增加之前如果是0,意味着从0到1,多了一个没出现过的字符,那此时左边字符种类数量加一;同时右边这个字符出现次数减一,如果减到0,意味着少了一中字符,右边字符种类减一。这样不必每次都遍历长度26的数组,时间复杂度从 O ( 26 N ) O(26N) O(26N)降到 O ( N ) O(N) O(N)
4. 形成目标数组的子数组最少增加次数
给你一个整数数组 target 和一个数组 initial ,initial 数组与 target 数组有同样的维度,且一开始全部为 0 。
请你返回从 initial 得到 target 的最少操作次数,每次操作需遵循以下规则:
在 initial 中选择 任意 子数组,并将子数组中每个元素增加 1 。
答案保证在 32 位有符号整数以内。
示例 1:
输入:target = [1,2,3,2,1]
输出:3
解释:我们需要至少 3 次操作从 intial 数组得到 target 数组。
[0,0,0,0,0] 将下标为 0 到 4 的元素(包含二者)加 1 。
[1,1,1,1,1] 将下标为 1 到 3 的元素(包含二者)加 1 。
[1,2,2,2,1] 将下表为 2 的元素增加 1 。
[1,2,3,2,1] 得到了目标数组。
示例 2:
输入:target = [3,1,1,2]
输出:4
解释:(initial)[0,0,0,0] -> [1,1,1,1] -> [1,1,1,2] -> [2,1,1,2] -> [3,1,1,2] (target) 。
1 <= target.length <= 10^5
1 <= target[i] <= 10^5
解析
本题属于看起来比较难的,其实很简单。
可能一开始会把问题看作是堆一座有峰有谷的山想,是不是需要先堆最下面的一层,然后分别堆每一个小山头等等,这样就想复杂了。
我们这样看这个问题,从左向右遍历数组,既然要求次数最少,那就把能一起加的全都一起加,如果一个i位置的数A比前一个数B大,那意味着,i这里B及以下的这部分可以跟着i-1一起加,但是A-B这部分没人带他了,只能自己加,需要增加A-B次操作;但如果一个i位置的数A比前一个数B小,那在增加到B这么大的时候,A早就完成了,不需要新的操作。
所以,综上,首先我们至少需要target[0]这么多次操作,然后从1开始向右遍历数组,如果一个数比前一个大,就加上二者差值,这是为了构造这个更大的数被迫增加的操作;反之则不需要新增操作。
换言之,每次增加
m
a
x
(
0
,
t
a
r
g
e
t
[
i
]
−
t
a
r
g
e
t
[
i
−
1
]
)
max(0,target[i]-target[i-1])
max(0,target[i]−target[i−1])
int minNumberOperations(vector<int>& target) {
int len = target.size();
int ans = target[0];
for (int i = 1; i < len; ++i) {
ans += max(0, target[i] - target[i - 1]);
}
return ans;
}