第一次参加双周赛,不愧是双周赛的简单,本来可以 AK 的,但是第三题理解了很久,不过最后还是想出了怎么做,开shang。
第一题:暴力模拟。
第二题:暴力枚举或者先排序再枚举。
第三题:DFS。
第四题:动态规划DP。
详细题解如下。
1. 有序数组中出现次数超过25%的元素(Element Appearing More Than 25 in Sorted Array)
2. 删除被覆盖区间(Remove Covered Intervals)
3.字母组合迭代器(Iterator for Combination)
4.下降路径最小和 II(minimum-falling-path-sum-ii)
LeetCode第15场双周赛地址:
https://leetcode-cn.com/contest/biweekly-contest-15/
1. 有序数组中出现次数超过25%的元素(Element Appearing More Than 25 in Sorted Array)
题目链接
https://leetcode-cn.com/problems/element-appearing-more-than-25-in-sorted-array/
题意
给你一个非递减的 有序 整数数组,已知这个数组中恰好有一个整数,它的出现次数超过数组元素总数的 25%。
请你找到并返回这个整数
示例 1:
输入:arr = [1,2,2,6,6,6,6,7,10] 输出:6
提示:
1 <= arr.length <= 10^4
0 <= arr[i] <= 10^5
解题思路
假设arr 数组的长度为 N,其中数字中最大数为 M。
方法一:
根据数据范围,数字最小为0,最大为10^5
,一开始想的是,利用开一个一维数组,用于记录每个数出现的次数,然后再遍历该一维数组,判断哪个数出现了超过25%。该种方法的时间复杂度是 O(N+M),空间复杂度是 O(M)。
方法二:
从题目或者题意中我们可以发现给出的 arr 数组是有序的,即是一个非递减的数组。那边我们计算某个数出现的次数时,可以避免使用开一维数组来存储次数。因为是有序的,所以相同的数肯定是相邻的。因此我们可以
记录一开始的数,val,出现次数为cnt = 1,然后判断下一个数,是否和当前记录的 val 相同。
- 如果相同,则val 不变,同时 cnt++,并且判断此时该数出现的次数,是不是已经超过25%,如果是这个就是答案。
- 如果不同,那就次数该数记为 val,并且重新把 cnt = 1,重新开始。
接着判断下一个数。
因此该方法的时间复杂度是O(N),空间复杂度为 O(1)。
AC代码(方法一 C++)
class Solution {
public:
int findSpecialInteger(vector<int>& arr) {
int cnt[100010];
memset(cnt, 0, sizeof(cnt));
int ans = 0;
int len = arr.size();
for(int num: arr)
{
cnt[num]++;
}
for(int i = 0;i < 100010;i++)
{
if(cnt[i]*4 > len)
{
ans = i;
break;
}
}
return ans;
}
};
AC代码(方法二 C++)
class Solution {
public:
int findSpecialInteger(vector<int>& arr) {
int n = arr.size();
if(n==1) return arr[0];
int cnt = 1;
int val = arr[0];
int ans;
for(int i = 1;i < n;i++)
{
if(val == arr[i])
{
cnt++;
if(cnt*4 > n)
{
ans = val;
break;
}
}
if(val != arr[i])
{
val = arr[i];
cnt = 1;
}
}
return ans;
}
};
2. 删除被覆盖区间(Remove Covered Intervals)
题目链接
https://leetcode-cn.com/problems/remove-covered-intervals/
题意
给你一个区间列表,请你删除列表中被其他区间所覆盖的区间。
只有当 c <= a 且 b <= d 时,我们才认为区间 [a,b) 被区间 [c,d) 覆盖。
在完成所有删除操作后,请你返回列表中剩余区间的数目。
示例 1:
输入:intervals = [[1,4],[3,6],[2,8]] 输出:2 解释:区间 [3,6] 被区间 [2,8] 覆盖,所以它被删除了。
提示:
1 <= intervals.length <= 1000
0 <= intervals[i][0] < intervals[i][1] <= 10^5
对于所有的 i != j:intervals[i] != intervals[j]
解题思路
方法一:
拿到题目,一开始的想法是,对于某一个区间,暴力枚举所有其他区间,判断该区间有没有被覆盖。
该方法的时间复杂度是 O(N^2),根据题目的数据大小,可以判断,不会超时,因此该方法可以使用。
方法二:
我们想有没有方法可以优化方法一,那么看看能不能降低时间复杂度。
对于已给的区间数组,我们可以先进行排序。如示例1所给的区间,[[1,4],[3,6],[2,8]],经过升序排序(先按区间第一个数升序排序,如果第一个数相同,就按第二个数升序排序),得到的已排序为 [[1,4],[2,8],[3,6]]。
那么对于判断某一个区间是否被覆盖,比如 [3, 6],因为该区间不是第一个区间,因此对于区间第一个数 3 而言,前面区间的第一个数一定都是小于等于 3 的(因为是升序排序)。
因此重点在于判断区间的第二个数 6 ,是不是前面区间的第二个数中,有大于等于 6的。那么我们可以利用一个变量,存储前面出现的所有区间的第二个数的最大值。示例1中是 8,因此大于等于6,所以该区间被覆盖了。
所以该方法的是,先排序,从而只需要遍历一次即可。时间复杂度是 O(N*logN + N)。
AC代码(方法一 暴力枚举 C++)
class Solution {
public:
int removeCoveredIntervals(vector<vector<int>>& intervals) {
int n = intervals.size();
int ans = n;
for(int i = 0;i < n;i++)
{
int a = intervals[i][0], b = intervals[i][1];
for(int j = 0;j < n;j++)
{
if(j == i)
continue;
int c = intervals[j][0], d = intervals[j][1];
if(c<=a && b<=d)
{
--ans;
break;
}
}
}
return ans;
}
};
AC代码(方法二 排序+遍历 C++)
class Solution {
public:
int removeCoveredIntervals(vector<vector<int>>& intervals) {
sort(intervals.begin(), intervals.end());
int n = intervals.size();
int ans = n;
int val = intervals[0][1];
for(int i = 1;i < n;i++)
{
if(val >= intervals[i][1])
{
--ans;
}
val = max(val, intervals[i][1]);
}
return ans;
}
};
3.字母组合迭代器(Iterator for Combination)
题目链接
https://leetcode-cn.com/problems/iterator-for-combination/
题意
请你设计一个迭代器类,包括以下内容:
- 一个构造函数,输入参数包括:一个 有序且字符唯一 的字符串 characters(该字符串只包含小写英文字母)和一个数字 combinationLength 。
- 函数 next() ,按 字典序 返回长度为 combinationLength 的下一个字母组合。
- 函数 hasNext() ,只有存在长度为 combinationLength 的下一个字母组合时,才返回 True;否则,返回 False。
示例 1:
CombinationIterator iterator = new CombinationIterator("abc", 2); // 创建迭代器 iterator iterator.next(); // 返回 "ab" iterator.hasNext(); // 返回 true iterator.next(); // 返回 "ac" iterator.hasNext(); // 返回 true iterator.next(); // 返回 "bc" iterator.hasNext(); // 返回 false
提示:
1 <= combinationLength <= characters.length <= 15
每组测试数据最多包含 10^4 次函数调用。
题目保证每次调用函数 next 时都存在下一个字母组合。
解题分析
题目的意思是,先执行一个类,然后的进行类中的两个函数调用,要求我们得到对应的返回值。
先理解题目中的几个定义,下一个字母组合,其实就是一个排列组合,但是是按照字典序的顺序得到的,比如示例1中的,长度为 2 ,说明排列组合得到的string 的长度要为 2。即 ab,ac,bc。按字典序顺序。
因此 next() 函数的作用就是,按字典序的顺序输出排列组合的可能。
hasNext() 函数的作用是,判断还有没有存在的排列组合,比如当我们已经 next() 是 ac,因为后面还有,所以无论我们调用几次 hasNext() 函数,都是返回 true。而当调用 next() 是 bc时,无论我们调用几次 hasNext() 函数,都是返回 false。
因此这道题,其实就是当调用类的时候,我们得到了所有的排列组合,同时按字典顺序排好,放在一个数组中。这样子无论是调用哪个函数,只要相对应从这个数组中取出或者进行判断即可。
因此重点在于,如果得到所有的排列组合,那其实就是DFS。
按照题目的题意,我们可以知道,给定的 characters 是有序的,所以我们就按照顺序取,那就是字典序从小到大。比如示例1,abc,那么先从第一个 a ,剩下按顺序取,只要每次取都是从后面取,那就是字典序顺序。
所以我们的DFS,有三个参数,首先是要存储这种情况下组合成的字符串 “”,还有对应此时字符串的长度(用于判断DFS是否结束,当 == combinationLength,说明结束,因为此时组合的长度符合了要求),还有一个此时要取得字符要从原来 characters 得哪个位置开始,这样子取出来得才会是按照字典序。
剩下的就是简单的DFS + 回溯了。
当得到所有可能的排列组合的数组时,只要调用 next(),我们就从中按顺序输出。只要调用 hasNext(),只要判断此时输出还有没有可以输出的。我们可以用一个全局变量index表示输出的下标,如果是 next(),我们就输出的同时,index++。如果是 hasNext(),我们只要判断 index此时等不等于 所有可能的排列组合的数组的长度,如果等于,说明没有了,那就是false。否则true。
AC代码(C++)
class CombinationIterator {
public:
string cha;
int com;
int vis[20];
vector<string> res;
int index = 0;
CombinationIterator(string characters, int combinationLength) {
cha = characters;
com = combinationLength; // 变成全局变量,方便处理,把局部变为全局变量,新建一个全局变量赋值
memset(vis, 0, sizeof(vis));
dfs("", 0, 0);
}
string next() {
return res[index++];
}
bool hasNext() {
if(index == res.size())
return false;
return true;
}
void dfs(string str, int len, int loc)
{
if(len == com)
{
res.push_back(str); // 此时str是一种组合
return;
}
for(int i = loc; i < cha.size();i++)
{
if(vis[i]==0) // 还要判断这个字符有没有被用过
{
vis[i] = 1;
string temp = str;
str += cha[i];
dfs(str, len+1, i+1); // 下一个
vis[i] = 0; // 回溯
str = temp; // 把原来的值重新赋予
}
}
}
};
/**
* Your CombinationIterator object will be instantiated and called as such:
* CombinationIterator* obj = new CombinationIterator(characters, combinationLength);
* string param_1 = obj->next();
* bool param_2 = obj->hasNext();
*/
4.下降路径最小和 II(minimum-falling-path-sum-ii)
题目链接
https://leetcode-cn.com/problems/minimum-falling-path-sum-ii/
题意
给你一个整数方阵 arr ,定义「非零偏移下降路径」为:从 arr 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。
请你返回非零偏移下降路径数字和的最小值。
示例 1:
输入:arr = [[1,2,3],[4,5,6],[7,8,9]] 输出:13 解释: 所有非零偏移下降路径包括: [1,5,9], [1,5,7], [1,6,7], [1,6,8], [2,4,8], [2,4,9], [2,6,7], [2,6,8], [3,4,8], [3,4,9], [3,5,7], [3,5,9] 下降路径中数字和最小的是 [1,5,7] ,所以答案是 13 。
提示:
1 <= arr.length == arr[i].length <= 200
-99 <= arr[i][j] <= 99
解题分析
先理解题目意思,从 arr 数组中的每一行选择一个数字,且按顺序选出来的数字中,相邻数字不在原数组的同一列。表示每一行取一个数,同时下一行取出的数,不能和上一行取出的数在同一列。
因为是要最小值,因此我们考虑能不能使用动态规划DP来做。
动态规划最重要的几个:
1、先定义状态,dp[i][j] 表示,在 [i][j] 位置上非零偏移下降路径数字和的最小值。
2、状态转移方程
示例1,
1 2 3
4 5 6
7 8 9
对应 4 而言,dp[i][j] = 4 + 上一行中除了相同列中最小的 = 4 + 2 = 6
对于 5 而言,dp[i][j] = 5 + 1 = 6
对于 6 而言,dp[i][j] = 6 + 1 = 7
因此 dp[i][j] = arr[i][j] + min(dp[i-1][k : 0 到 m-1 除了 j]) (上一行中,除了相同列的最小值)
3、为了方便处理, dp的大小是 n+1行,m+1列,第 0 行的初始值为 0。
最后得到了 dp[n][ 1 到 m] ,这个只是从上往下,到了最后一行的时候,每个列的最小值,我们再从中取最小值,就是最后的答案。
AC代码( C++)
class Solution {
public:
int dp[210][210];
int minFallingPathSum(vector<vector<int>>& arr) {
int n = arr.size();
int m = arr[0].size();
memset(dp, 0 ,sizeof(dp));
for(int i = 1;i <= n;i++)
{
for(int j = 1;j <= m;j++)
{
int minVal = 99*210;
for(int k = 1;k <= m;k++)
{
if(k==j)
continue;
minVal = min(minVal, arr[i-1][j-1] + dp[i-1][k]);
}
dp[i][j] = minVal;
}
}
int ans = 99*210; // 枚举最小值,所以设最大值,数据中每个数最大99,最大行为200,所以这个就是最大值。
for(int j = 1;j <= m;j++)
{
ans = min(ans, dp[n][j]);
}
return ans;
}
};