感觉这周的周赛没有什么具体的算法,都是按照题意来模拟就解决了,主要用到了C++ STL中的map。
第一题:按题意模拟,求某个数的有几位。
第二题:按题意模拟,利用了map。
第三题:按题意模拟,枚举所有出现的字串,求出出现最多次的字串次数,或者深入理解题意,使用滑动窗口。
第四题:按题意模拟,从盒子或者钥匙两方面枚举。
详细题解如下。
1. 统计位数为偶数的数字(Find Numbers with Even Number of Digits)
2. 划分数组为连续数字的集合(Divide Array in Sets of k Consecutive Numbers)
3.子串的最大出现次数(Maximum Number of Occurrences of-a-substring)
4.你能从盒子里获得的最大糖果数(Maximum Candies You Can Get From Boxes)
LeetCode第168场周赛地址:
https://leetcode-cn.com/contest/weekly-contest-168/
1. 统计位数为偶数的数字(Find Numbers with Even Number of Digits)
题目链接
https://leetcode-cn.com/problems/find-numbers-with-even-number-of-digits/
题意
给你一个整数数组
nums
,请你返回其中位数为 偶数 的数字的个数。示例 1:
输入:nums = [12,345,2,6,7896] 输出:2 解释: 12 是 2 位数字(位数为偶数) 345 是 3 位数字(位数为奇数) 2 是 1 位数字(位数为奇数) 6 是 1 位数字 位数为奇数) 7896 是 4 位数字(位数为偶数) 因此只有 12 和 7896 是位数为偶数的数字
示例 2:
输入:nums = [555,901,482,1771] 输出:1 解释: 只有 1771 是位数为偶数的数字。
提示:
1 <= nums.length <= 500
1 <= nums[i] <= 10^5
解题思路
根据题意,枚举数组的每个数,然后求每个数的位数,如果是偶数,那就答案 ++。
时间复杂度是 O(N * maxLen),N 为数组长度,maxLen 表示某个数的位数最大值,根据数据范围是 500*6。不会超时,说明该方法可以。
因此题目转化为,如何求一个数的位数。
对于一个数,我们每次都除10,直到数变为0,每除一次10,相当于位数就 + 1。
比如 1771 - > 177 - > 17 - > 1 - > 0,所以位数为 4。
AC代码(C++)
class Solution {
public:
int findNumbers(vector<int>& nums) {
int ans = 0;
int cnt = 0;
for(auto num : nums)
{
cnt = 0;
while(num)
{
cnt++;
num /= 10;
}
if(cnt % 2 == 0)
++ans;
}
return ans;
}
};
2. 划分数组为连续数字的集合(Divide Array in Sets of k Consecutive Numbers)
题目链接
https://leetcode-cn.com/problems/divide-array-in-sets-of-k-consecutive-numbers/
题意
给你一个整数数组 nums 和一个正整数 k,请你判断是否可以把这个数组划分成一些由 k 个连续数字组成的集合。
如果可以,请返回 True;否则,返回 False。示例 1:
输入:nums = [1,2,3,3,4,4,5,6], k = 4 输出:true 解释:数组可以分成 [1,2,3,4] 和 [3,4,5,6]。
示例 2:
输入:nums = [3,2,1,2,3,4,3,4,5,9,10,11], k = 3 输出:true 解释:数组可以分成 [1,2,3] , [2,3,4] , [3,4,5] 和 [9,10,11]。
示例 3:
输入:nums = [3,3,2,2,1,1], k = 3 输出:true
示例 4:
输入:nums = [1,2,3,4], k = 3 输出:false 解释:数组不能分成几个大小为 3 的子数组。
提示:
1 <= nums.length <= 10^5
1 <= nums[i] <= 10^9
1 <= k <= nums.length
解题思路
先对数组进行排序,从小到大,这样子连续数字就会连在一起。然后统计每个数字出现的次数。
接下来的算法过程
1)遍历数组的数,判断是不是出现次数 > 0,如果大于0,说明要从这个数开始要分组,即走到2)。否则就需继续走回 1)。
2)如果出现了第一个数次数 > 0,那么就要判断这个数并且和接下来的 k-1 连续的数要分成一组。那么接下来的 k-1 个连续的数都要至少出现次数为1,否则就无法分组,那就返回 false。如果可以分组,我们就对这组里面的组对应的数字出现的次数都 - 1。然后回到 1)。
那么这么看的话,时间复杂度应该是 O(N * K),N是数组长度,K是每组由 K 个数组成。从数据大小中可以看出会超时。
但是由于我们在具体代码中,有用到剪枝,即在第一层循环中,判断了遍历的当前数是不是出现次数 > 0。如果不大于0,那么第二层循环不会走,所以具体的时间复杂度应该算是 O(N / K * K),因为从第二层循环到底走不走,可以看成只有第一层循环,被分成了几组(同一组的数,相当于被剪枝了)。
因此时间复杂度是可以通过的。
而我们统计每个数字出现的次数。用的是map,而在C++ STL中有两种:map 和 unordered_map,前一种是会默认排序,后一种相当于是哈希表,只用于查找,没有排序功能。
因此我么使用的是 unordered_map,只要有查找功能即可,可以认为查找是 O(1)。
(我试过分别用map 和 unordered_map来提交,的确是 unordered_map 的运行时间更少)
AC代码(C++)
class Solution {
public:
bool isPossibleDivide(vector<int>& nums, int k) {
int n = nums.size();
if(n % k != 0) return false;
unordered_map<int, int> cnt;
sort(nums.begin(), nums.end());
for(int i = 0;i < n;++i)
{
cnt[nums[i]]++;
}
for(int i = 0;i < n;++i)
{
if(cnt[nums[i]] == 0) continue;
--cnt[nums[i]];
for(int j = nums[i] + 1;j < nums[i] + k;++j)
{
if(cnt[j] == 0)
return false;
--cnt[j];
}
}
return true;
}
};
3.子串的最大出现次数(Maximum Number of Occurrences of-a-substring)
题目链接
https://leetcode-cn.com/problems/maximum-number-of-occurrences-of-a-substring/
题意
给你一个字符串 s ,请你返回满足以下条件且出现次数最大的 任意 子串的出现次数:
子串中不同字母的数目必须小于等于 maxLetters 。
子串的长度必须大于等于 minSize 且小于等于 maxSize 。示例 1:
输入:s = "aababcaab", maxLetters = 2, minSize = 3, maxSize = 4 输出:2 解释:子串 "aab" 在原字符串中出现了 2 次。 它满足所有的要求:2 个不同的字母,长度为 3 (在 minSize 和 maxSize 范围内)。
示例 2:
输入:s = "aaaa", maxLetters = 1, minSize = 3, maxSize = 3 输出:2 解释:子串 "aaa" 在原字符串中出现了 2 次,且它们有重叠部分。
示例 3:
输入:s = "aabcabcab", maxLetters = 2, minSize = 2, maxSize = 3 输出:3
示例 4:
输入:s = "abcde", maxLetters = 2, minSize = 3, maxSize = 3 输出:0
提示:
1 <= s.length <= 10^5
1 <= maxLetters <= 26
1 <= minSize <= maxSize <= min(26, s.length)
s 只包含小写英文字母。
解题分析
拿到题目,我们想到的方法,先是遍历枚举。统计出现的次数,我们依旧用unordered_map,因为只需要查找,不需要排序。
方法一:
我们从字符串 s 的每一个字符开始,然后统计长度为minSize时的字符串,看看出现的字母种类数量满不满足 maxLetters 的条件。
如果不满足,那就说明,从这个字符开始的,不存在满足条件的字串,那么就从下一个字符开始。
如果minSize满足了,那我们就判断,到maxSize长度之前的每一个的字串满不满足条件。
统计所有满足条件的字串,找出出现次数最多的。可以使用 map<string, int>,就可以统计字串对应的出现次数。
这种方法时间复杂度时 O(N * 26),N 是 s 字符串的长度。根据数据大小,不会超时,可以。
这种方法主要还是利用暴力枚举所有可能的字串,把满足条件的字串统计,最后得出最多出现次数。
方法二:
如果我们仔细分析题目,会发现其实 maxSize 这个条件是不影响最后答案的。比如我们同时出现的最大长度的 aab两次,那么其中会包含有:aa,ab,那么短的字串也会对应出现两次。
因此我们主要看的就是 minSize 长度的字串出现的次数即可。
那么我们可以利用滑动窗口来记录当前 minSize 长度的字串满不满足条件,满足就存起来。
当我们移到下一位的时候,相当于把原本字串的第一个字符去掉,后面加入进来新得字符,这样子就可以用滑动窗口得方式遍历到所有满足条件得所有字串。
这个的时间复杂度应该是 O(N - minSize)
根据代码提交之后,方法一的运行时间900ms。方法二的运行时间 68ms。
运行时间明显降低了。
AC代码(方法一 C++)
class Solution {
public:
unordered_map<string, int> strCnt; // 记录出现的次数。
int ans = 0;
int cnt[26];
int cnts = 0;
int maxFreq(string s, int maxLetters, int minSize, int maxSize) {
int n = s.size();
strCnt.clear();
for(int i = 0;i < n;i++)
{
if(n - i < minSize) break;
memset(cnt, 0, sizeof(cnt));
cnts = 0;
string subStr = "";
int j;
for(j = i;j < i + minSize;++j) // 先起点到 minSize的
{
if(cnt[s[j] - 'a'] == 0)
{
++cnts;
}
++cnt[s[j] - 'a'];
subStr += s[j];
}
if(cnts > maxLetters) continue;
++strCnt[subStr];
ans = max(ans, strCnt[subStr]);
for(;j < i + maxSize && j < n;++j) // 再从minSize到maxSize之间遍历所有情况
{
if(cnt[s[j] - 'a'] == 0)
{
++cnts;
}
++cnt[s[j] - 'a'];
if(cnts > maxLetters) break;
subStr += s[j];
++strCnt[subStr];
ans = max(ans, strCnt[subStr]);
}
}
return ans;
}
};
AC代码(方法二 C++)
class Solution {
public:
unordered_map<string, int> strCnt;
int ans = 0;
int cnt[30];
int cnts = 0;
int maxFreq(string s, int maxLetters, int minSize, int maxSize) {
int n = s.size();
if(n < minSize) return 0;
strCnt.clear();
memset(cnt, 0, sizeof(cnt));
// 第一个找到的temp
string temp = "";
for(int i = 0;i < minSize;i++)
{
if(cnt[s[i] - 'a'] == 0) ++cnts;
++cnt[s[i] - 'a'];
temp += s[i];
}
if(cnts <= maxLetters)
{
ans = max(ans, ++strCnt[temp]);
}
// 滑动窗口
int i = 1, j = minSize;
while(j < n)
{
// 删除第一个字符
--cnt[temp[0] - 'a'];
if(cnt[temp[0] - 'a'] == 0) --cnts;
temp.erase(0, 1);
// 后面加入新的字符,从而实现滑动窗口
temp += s[j];
if(cnt[s[j] - 'a'] == 0) ++cnts;
++cnt[s[j] - 'a'];
if(cnts <= maxLetters)
{
ans = max(ans, ++strCnt[temp]);
}
++i;++j;
}
return ans;
}
};
4.你能从盒子里获得的最大糖果数(Maximum Candies You Can Get From Boxes)
题目链接
https://leetcode-cn.com/problems/maximum-candies-you-can-get-from-boxes/
题意
给你 n 个盒子,每个盒子的格式为 [status, candies, keys, containedBoxes] ,其中:
- 状态字 status[i]:整数,如果 box[i] 是开的,那么是 1 ,否则是 0 。
- 糖果数 candies[i]: 整数,表示 box[i] 中糖果的数目。
- 钥匙 keys[i]:数组,表示你打开 box[i] 后,可以得到一些盒子的钥匙,每个元素分别为该钥匙对应盒子的下标。
- 内含的盒子 containedBoxes[i]:整数,表示放在 box[i] 里的盒子所对应的下标。
给你一个 initialBoxes 数组,表示你现在得到的盒子,你可以获得里面的糖果,也可以用盒子里的钥匙打开新的盒子,还可以继续探索从这个盒子里找到的其他盒子。
请你按照上述规则,返回可以获得糖果的 最大数目 。
示例 1:
输入:status = [1,0,1,0], candies = [7,5,4,100], keys = [[],[],[1],[]], containedBoxes = [[1,2],[3],[],[]], initialBoxes = [0] 输出:16 解释: 一开始你有盒子 0 。你将获得它里面的 7 个糖果和盒子 1 和 2。 盒子 1 目前状态是关闭的,而且你还没有对应它的钥匙。所以你将会打开盒子 2 ,并得到里面的 4 个糖果和盒子 1 的钥匙。 在盒子 1 中,你会获得 5 个糖果和盒子 3 ,但是你没法获得盒子 3 的钥匙所以盒子 3 会保持关闭状态。 你总共可以获得的糖果数目 = 7 + 4 + 5 = 16 个。
示例 4:
输入:status = [1], candies = [100], keys = [[]], containedBoxes = [[]], initialBoxes = [] 输出:0
示例 2:其他示例具体看链接,挑出了最重要的两个示例。
提示:
1 <= status.length <= 1000
status.length == candies.length == keys.length == containedBoxes.length == n
status[i] 要么是 0 要么是 1 。
1 <= candies[i] <= 1000
0 <= keys[i].length <= status.length
0 <= keys[i][j] < status.length
keys[i] 中的值都是互不相同的。
0 <= containedBoxes[i].length <= status.length
0 <= containedBoxes[i][j] < status.length
containedBoxes[i] 中的值都是互不相同的。
每个盒子最多被一个盒子包含。
0 <= initialBoxes.length <= status.length
0 <= initialBoxes[i] < status.length
解题分析
我们从示例中分析,可以知道,我们会得到初始化的一些盒子 initialBoxes,如果这个数组为空,那就没法往下走,也就是答案为 0 (对应示例4)。
从 initialBoxes 得到初始盒子,对于 某个 糖果能不能被拿,其实也就是,对应位置的盒子要存在,同时这个盒子状态为 开(因为有钥匙也可以转换为把对应的状态 变为 1)。
比如示例1,初始盒子为 0,根据状态,发现可以开,那么得到了盒子,1和2,发现 1 现在开不了,那就去判断 2,发现可以开。从而得到了钥匙 1,没得到新盒子。这个时候对应这个钥匙1,我们就考虑对应有没有盒子 1,发现之前的确是得到了盒子 1,因此盒子 1 可以开。没有继续得到 新钥匙 和 新盒子,结束。
我们可以发现,当我们打开一个盒子,无论是得到里面的 新钥匙还是 新盒子,我们都需要一起考虑,因此有一些可能在之前的盒子没有钥匙,那么我们新的到了钥匙,就可以去开。或者有一些之前是可以开,但没有对应的盒子,此时得到了就可以开。
所以我们利用两个数组分别统计都一共获得了哪些盒子和哪些可以打开,其中哪些可以打开我们可以用原有数组 status 来记录,得到新钥匙,就把对应的状态 标记为 1即可。
但是我们 新钥匙还是 新盒子 加进去考虑的时候,也有可能是之前已经取过糖果了,避免二次重复,我们用一个数组 vis,记录对应哪些位置的已经被取过糖果了,那么下次我们就不考虑了。
因为要加进去考虑,先进的先判断,我们就用队列来存储 queue。
因此整个流程:
0)一个数组 allBoxes 记录所有已经获得盒子,一个数组 vis 记录这个下标对应的糖果有没有取过
1)先把初始给的盒子放进队列中
2)只要队列不为空,我们就从队列取出接下来要考虑的下标值
3)判断这个下标值对应的糖果能不能取出,也就是如果对应盒子有,同时对应的状态是可以开的,并且这个下标的糖果没被取出,那就说明这个盒子可以打开。那么取出糖果,并且标记 vis = 1。然后打开这个新盒子得到的盒子下标和钥匙下标都放进队列,说明是接下来要考虑的。并且把获得的钥匙对应的状态位置 变为 1,同时获得的新盒子记录保存在 allBoxes中。
比如示例1的例子,一开始队列中是 0,0盒子存在同时可以打开,那么取出糖果,同时得到了,1和2放进队列。
此时判断 1,因为有盒子,还没钥匙,所以开不了。判断2,有盒子,同时可以打开。得到 1 放进队列。此时更新钥匙1进状态记为可以打开。所以判断 1,有盒子,同时可以打开,取出糖果。
所以我们对于新得到的盒子下标和钥匙下标都重新放进队列,以便下次判断,这样子就不会出现,后面出现了新钥匙,盒子是前面出现的,没有考虑到这种情况发生。
所以我们放进队列所有可能是,O(n + n),新得钥匙和新得盒子所有,那么不会超时。
AC代码( C++)
class Solution {
public:
int vis[1010], allBoxes[1010];
int maxCandies(vector<int>& status, vector<int>& candies, vector<vector<int>>& keys, vector<vector<int>>& containedBoxes, vector<int>& initialBoxes) {
if(initialBoxes.size() == 0)
return 0;
memset(vis, 0, sizeof(vis)); // 记录有没有取过糖果了
memset(allBoxes, 0, sizeof(allBoxes)); // 记录得到过的盒子情况
queue<int> q;
for(auto box : initialBoxes) //先从初始盒子中得到要考虑的下标
{
allBoxes[box] = 1;
q.push(box);
}
int ans = 0;
while(!q.empty())
{
int t = q.front();
q.pop();
if(status[t]==1 && allBoxes[t]==1 && !vis[t]) // 对应下标可以拿糖果
{
ans += candies[t];
vis[t] = 1;
for(auto box : containedBoxes[t]) // 得到的新盒子
{
allBoxes[box] = 1;
if(vis[box]==1) continue; // 如果这个盒子没有被取过,那就是下标需要考虑,避免漏掉
q.push(box);
}
for(auto key : keys[t]) // 得到的新钥匙
{
status[key] = 1;
if(vis[key]==1) continue; // 如果对应的下标表示没被取过,就是需要考虑,避免漏掉这种后得钥匙,但前面已经有盒子的情况
q.push(key);
}
}
}
return ans;
}
};