因为很久没有刷过题了,打算这段时间把每种类型的过一遍熟悉一下——好了下面进入刷题模式(学习模式)
继续上次的会议室练习,这次我们换用贪心算法——扫描线来解决问题
leetcode上扫描线的概念如下所示:
该题用到的是扫描线技巧。(学习howard大佬的思想,其原文链接在这:. - 力扣(LeetCode))
本题题意求的是会议冲突数量的最大值,就是某一时刻,最多有多少个会议在同时进行。
如图,我们想象有个时间线从左往右走扫描,同时维护一个变量num_of_ongoing_meeting,表示当前正在进行的会议数量。
每遇到一个会议的起始端点,就将num_of_ongoing_meeting加1
每遇到一个会议的结束端点,就将num_of_ongoing_meeting减1
num_of_ongoing_meeting的最大值就是本题的答案。
(不过看了代码感觉空间占用很多...)
//贪心算法——扫描线
//寻求会议冲突数量的最大值,即在某一时刻,最多会有多少个会议在同时进行。
class Solution{
public:
int minMeetingRooms(vector<vector<int>>& intervals){
int last_end = 0;
//starts[i]:有多少个会议在i时刻开始
//由本题条件:0<=starti<endi<=10^6
//可得,会议的结束时间最晚维10^6
int starts[1000001]={0};
int ends[1000001]={0};
for(const auto& interval:intervals){
starts[interval.at(0)]++;
ends[interval.at(1)]++;
last_end = std::max(last_end,interval.at(1));
//记录最晚的结束时间点
}
//所需会议室的最少数量
int min_num_of_room = 0;
//当前时刻有多少个会议正在进行
int num_of_ongoing_meeting = 0;
for(int i=0;i<=last_end;i++){
num_of_ongoing_meeting += starts[i];
num_of_ongoing_meeting -= ends[i];
min_num_of_room = std::max(min_num_of_room,num_of_ongoing_meeting);
}
return min_num_of_room;
}
};
接下来是跳跃游戏一系列题目, 首先是跳跃游戏1
方法一:贪心
我们可以用贪心的方法解决这个问题。
(官方解题)设想一下,对于数组中的任意一个位置 y,我们如何判断它是否可以到达?根据题目的描述,只要存在一个位置 x,它本身可以到达,并且它跳跃的最大长度为 x+nums[x],这个值大于等于 y,即 x+nums[x]≥y,那么位置 y 也可以到达。
换句话说,对于每一个可以到达的位置 x,它使得 x+1,x+2,⋯,x+nums[x] 这些连续的位置都可以到达。
这样以来,我们依次遍历数组中的每一个位置,并实时维护 最远可以到达的位置。对于当前遍历到的位置 x,如果它在 最远可以到达的位置 的范围内,那么我们就可以从起点通过若干次跳跃到达该位置,因此我们可以用 x+nums[x] 更新 最远可以到达的位置。
在遍历的过程中,如果 最远可以到达的位置 大于等于数组中的最后一个位置,那就说明最后一个位置可达,我们就可以直接返回 True 作为答案。反之,如果在遍历结束后,最后一个位置仍然不可达,我们就返回 False 作为答案。
以题目中的示例一:
[2, 3, 1, 1, 4]
为例:
我们一开始在位置 0,可以跳跃的最大长度为 2,因此最远可以到达的位置被更新为 2;
我们遍历到位置 1,由于 1≤2,因此位置 1 可达。我们用 1 加上它可以跳跃的最大长度 3,将最远可以到达的位置更新为 4。由于 4 大于等于最后一个位置 4,因此我们直接返回 True。
代码:
//贪心算法
class Solution {
public:
bool canJump(vector<int>& nums) {
int n = nums.size();
int rightmost = 0;
for(int i=0;i<n;++i){
if(i<=rightmost){
rightmost = max(rightmost,nums[i]+i);
if(rightmost >= n-1){
return true;
}
}
}
return false;
}
};
方法一:正向查找可到达的最大位置
方法一虽然直观,但是时间复杂度比较高,有没有办法降低时间复杂度呢?
如果我们「贪心」地进行正向查找,每次找到可到达的最远位置,就可以在线性时间内得到最少的跳跃次数。
例如,对于数组 [2,3,1,2,4,2,3],初始位置是下标 0,从下标 0 出发,最远可到达下标 2。下标 0 可到达的位置中,下标 1 的值是 3,从下标 1 出发可以达到更远的位置,因此第一步到达下标 1。
从下标 1 出发,最远可到达下标 4。下标 1 可到达的位置中,下标 4 的值是 4 ,从下标 4 出发可以达到更远的位置,因此第二步到达下标 4。
在具体的实现中,我们维护当前能够到达的最大下标位置,记为边界。我们从左到右遍历数组,到达边界时,更新边界并将跳跃次数增加 1。
在遍历数组时,我们不访问最后一个元素,这是因为在访问最后一个元素之前,我们的边界一定大于等于最后一个位置,否则就无法跳到最后一个位置了。如果访问最后一个元素,在边界正好为最后一个位置的情况下,我们会增加一次「不必要的跳跃次数」,因此我们不必访问最后一个元素。
//贪心算法一:正向查找可到达的最大位置
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size(),step=0,maxPos=0,end=0;
for(int i=0;i<n-1;++i){
if(maxPos >= i){
maxPos = max(maxPos,i+nums[i]);
if(i==end){
end = maxPos;
++step;
}
}
}
return step;
}
};
(绝佳思路解释:
1、如果某一个作为 起跳点 的格子可以跳跃的距离是 3,那么表示后面 3 个格子都可以作为 起跳点。 可以对每一个能作为 起跳点 的格子都尝试跳一次,把 能跳到最远的距离 不断更新。
2、如果从这个 起跳点 起跳叫做第 1 次 跳跃,那么从后面 3 个格子起跳 都 可以叫做第 2 次 跳跃。
3、所以,当一次 跳跃 结束时,从下一个格子开始,到现在 能跳到最远的距离,都 是下一次 跳跃 的 起跳点。
对每一次 跳跃 用 for 循环来模拟。
跳完一次之后,更新下一次 起跳点 的范围。
在新的范围内跳,更新 能跳到最远的距离。
4、记录 跳跃 次数,如果跳到了终点,就得到了结果。
from 作者:Ikaruga
链接:https://leetcode.cn/problems/jump-game-ii/solutions/1/45-by-ikaruga/ )
//代码2
class Solution {
public:
int jump(vector<int>& nums) {
int start = 0,end = 1,ans = 0;
while(end<nums.size()){
int maxPos = 0;
for(int i=start;i<end;i++){
maxPos = max(maxPos,i+nums[i]);
}
start = end;
end = maxPos+1;
ans++;
}
return ans;
}
};
其实就是每一个下标,都对应了两条路,选择了每一条路之后,接着各自又有两个分岔口......由此循环往复,直到找到了对应元素为0的点。这就可以联想到深度优先搜索和广度优先搜索。
方法一:广度优先搜索
//广度优先搜索bfs
class Solution {
public:
bool canReach(vector<int>& arr, int start) {
if(arr[start]==0){
return true;
}
int n=arr.size();
vector<bool> used(n);
queue<int> q;
q.push(start);
used[start] = true;
while(!q.empty()){
int u = q.front();
q.pop();
if(u+arr[u]<n && !used[u+arr[u]]){
if(arr[u+arr[u]]==0){
return true;
}
q.push(u+arr[u]);
used[u+arr[u]] = true;
}
if(u-arr[u]>=0 && !used[u-arr[u]]){
if(arr[u-arr[u]]==0){
return true;
}
q.push(u-arr[u]);
used[u-arr[u]] = true;
}
}
return false;
}
};
解决方法:动态规划+前缀和
class Solution{
public:
bool canReach(string s,int minJump,int maxJump){
int n = s.size();
vector<int> f(n),pre(n);
f[0] = 1;
for(int i=0;i<minJump;++i){
pre[i] = 1;
}
for(int i=minJump;i<n;i++){
int left = i-maxJump,right=i-minJump;
if(s[i]=='0'){
int total = pre[right]-(left<=0?0:pre[left-1]);
f[i] = (total!=0);
}
pre[i] = pre[i-1] + f[i];
}
return f[n-1];
}
};
前缀和(第一次听到这个方法+相应的解题,学到了!)
哈希表的简单学习
代码如下所示:(即把所有可能的数字从大到小排列,如果当前的num比valueSymbols大,则结果增添相应字符串)
//模拟 即//贪心——哈希表
const pair<int,string> valueSymbols[]={
{1000,"M"},
{900, "CM"},
{500, "D"},
{400, "CD"},
{100, "C"},
{90, "XC"},
{50, "L"},
{40, "XL"},
{10, "X"},
{9, "IX"},
{5, "V"},
{4, "IV"},
{1, "I"}
};
class Solution {
public:
string intToRoman(int num) {
string roman;
for(const auto &[value,symbol] : valueSymbols){
while(num>=value){
num-=value;
roman+=symbol;
}
if(num==0){
break;
}
}
return roman;
}
};