leetcode1562. 查找大小为 M 的最新分组
给你一个数组 arr
,该数组表示一个从 1
到 n
的数字排列。有一个长度为 n
的二进制字符串,该字符串上的所有位最初都设置为 0
。
在从 1
到 n
的每个步骤 i
中(假设二进制字符串和 arr
都是从 1
开始索引的情况下),二进制字符串上位于位置 arr[i]
的位将会设为 1
。
给你一个整数 m
,请你找出二进制字符串上存在长度为 m
的一组 1
的最后步骤。一组 1
是一个连续的、由 1
组成的子串,且左右两边不再有可以延伸的 1
。
返回存在长度 恰好 为 m
的 一组 1
的最后步骤。如果不存在这样的步骤,请返回 -1
。
示例 1:
输入:arr = [3,5,1,2,4], m = 1
输出:4
解释:
步骤 1:"00100",由 1 构成的组:["1"]
步骤 2:"00101",由 1 构成的组:["1", "1"]
步骤 3:"10101",由 1 构成的组:["1", "1", "1"]
步骤 4:"11101",由 1 构成的组:["111", "1"]
步骤 5:"11111",由 1 构成的组:["11111"]
存在长度为 1 的一组 1 的最后步骤是步骤 4 。
示例 2:
输入:arr = [3,1,5,4,2], m = 2
输出:-1
解释:
步骤 1:"00100",由 1 构成的组:["1"]
步骤 2:"10100",由 1 构成的组:["1", "1"]
步骤 3:"10101",由 1 构成的组:["1", "1", "1"]
步骤 4:"10111",由 1 构成的组:["1", "111"]
步骤 5:"11111",由 1 构成的组:["11111"]
不管是哪一步骤都无法形成长度为 2 的一组 1 。
示例 3:
输入:arr = [1], m = 1
输出:1
示例 4:
输入:arr = [2,1], m = 2
输出:2
提示:
n == arr.length
1 <= n <= 10^5
1 <= arr[i] <= n
arr
中的所有整数 互不相同1 <= m <= arr.length
方法:模拟维护区间
思路:
题意是按照arr的顺序不断将对应的0变成1,我们关注的是连续1的区间的长度。因此可以使用并查集来解决,相邻的两个点都是1,那么这两个点就可以合并,同时修改并查集定义,维护每个连通区间的长度,找到最后一次有长度为m的连通区间的步骤编号即可。
由于本题这些点是一位的,连通分量一定在一维上连续,因此我们可以简化,不使用并查集。我们维护两个数组left、right。
- left[i]表示以结点i为连通分量右端点,它对应的左端点
- right[i]表示以结点i为连通分量左端点,它对应的右端点
我们这种结构类似于双向链表,即对一个连通分量(即全是1的连通区间),左端点的right为右端点,右端点的left为左端点。因左端点的left无意义(因为左端点的左边肯定为0,因此以它为右端点的连通区间就是它自己,因此它的left设置为自己),同理右端点的right也设置为自己。
两个数组初始值都为0,长度应该为n+2(在下面介绍)。
这种情况下,我们如何计算连通区间的长度呢,很简单,我们输入连通区间的左端点坐标x,那么长度即为right[x]-x+1。我们用一个函数getlength来表示这个式子。
介绍完如何表示连通区间之后,我们介绍本题的总体思路,使用res来保存答案,初始为-1。使用变量count来保存当前长度为m的连通分量个数,刚开始肯定是0。
- 开始按照arr顺序进行遍历,根据arr[i]的不同,即将0变为1的位置不同,更新left和right变量(后面详细解释)。
- 对涉及到的,更新的连通区间进行求长度,如果原来是m,那么count–,如果新变成的区间是m,那么count++。
- 更新结束后,如果此时count>0,更新res为i+1(这是因为i从0开始,而答案返回从第1步开始,因此要+1),即该步结束后,存在长度为m的连通区间。
- 完成arr的遍历,返回res即可。如果前面遍历过程中,没有一次出现m,那么res为初始值-1;如果有,那么res为最后一次有长度为m的区间的步骤序号。
下面我们来详细讲解区间更新的步骤,可以知道,假设x=arr[i],那么更新的时候,x的左右可能出现四种情况:
- 第一种是x的左右两边都是1,即左右两边都有连通区间了,那么最后要将这两个区间和x合并成一个区间。
- 第二种是左边为0,右边是1,那么只有右边有连通区间,那么更新之后需要将右边区间扩大一位
- 第三种是右边为0,左边是1,那么只有左边有连通区间,更新之后将左边区间扩大一位
- 最后一种是左右都为0,那么就只有自己,更新之后生成长度为1的新连通区间。
上面一共四种情况我们需要考虑,首先需要说明的是,我们不需要使用一个数组来维护当前1-n的排列如何,因为如果某个位置已经是1了,根据我们上面的介绍,它已经存在于一个连通区间中,因此它的left和right肯定是不为0的,因此我们可以直接用left和right来判断。
下面说明如何判断x左边和右边是不是1,首先我们知道,对于x=arr[i],x点此时肯定是0,
- 那么如果x-1是1,那么它肯定是某个区间的右端点,因此我们判断left[x-1]是否不为0,如果不为0,那么即存在左区间。且将left[x-1]送入getlength函数即可得到左区间长度。
- 对于有区间,如果x+1是1,那么它肯定是某个区间的左端点,因此我们判断right[x+1]是否不为0,如果不为0,那么即存在右区间,且将x+1送入getlength即可得到右区间长度。
- 由上面可以看出,我们考虑left[x-1]和right[x+1],x的取值为1-n,下标的取值为0-n+1,这就是为什么我们的两个数组长度应该为n+2。
下面我们考虑四种情况如何更新left和right。
第一种情况的图解如下图所示(其中有一些没用的左右连线就忽略了,因为我们只关心left[x-1]和right[x+1]):
**我们需要将左区间的左端点的right变为右区间的右端点,右区间的右端点的left变为左区间的左端点,中间点的这些不需要考虑,因为中间都是1了,继续遍历arr的过程中,也不会遍历到这些点了。**因此代码为:
right[left[x-1]] = right[x+1]
left[right[x+1]] = left[x-1]
然后判断新的区间长度是不是m,更新count。
同理,对于第二种和第三种情况,我们只需要考虑其中一边的更新,因为另一侧的端点为x,直接给出更新的代码:
# 只有左边有区间
# 将左边区间向右扩展一位,更新left和right
right[left[x-1]] = x
left[x] = left[x-1]
if getlength(left[x-1]) == m:
count += 1
# 只有右边有区间
# 将右边区间向左扩展一位,更新left和right
left[right[x+1]] = x
right[x] = right[x+1]
if getlength(x) == m:
count += 1
对于最后一种情况,就生成一个新的长度为1的区间,即left[x]=right[x]=x即可。
代码:
Python3:
class Solution:
def findLatestStep(self, arr: List[int], m: int) -> int:
n = len(arr)
# left[i]表示以结点i为连通分量右端点,它对应的左端点
# right[i]表示以结点i为连通分量左端点,它对应的右端点
# 这里的连通分量指的是连续的1
# 之所以长度为n+2,是因为后面要考虑left[i-1]和right[i+1],因此下标会取到0和n+1
left = [0]*(n+2)
right = [0]*(n+2)
# 返回以x为左端点的连通分量长度
def getlength(x):
return right[x]-x+1
# count表示长度为m的连通分量个数
count = 0
res = -1
for i in range(n):
x = arr[i]
# 如果x的左右都有连通区间,那么这两个需要合并
if left[x-1] and right[x+1]:
# 如果左边或者右边原来长度为m,那么m的数量--
if getlength(left[x-1]) == m:
count -= 1
if getlength(x+1) == m:
count -= 1
# 将区间合并,更新left和right
right[left[x-1]] = right[x+1]
left[right[x+1]] = left[x-1]
# 如果新的长度为m,count++
if getlength(left[x-1]) == m:
count += 1
# 只有左边有区间
elif left[x-1]:
if getlength(left[x-1]) == m:
count -= 1
# 将左边区间向右扩展一位,更新left和right
right[left[x-1]] = x
left[x] = left[x-1]
if getlength(left[x-1]) == m:
count += 1
# 只有右边有区间
elif right[x+1]:
if getlength(x+1) == m:
count -= 1
# 将右边区间向左扩展一位,更新left和right
left[right[x+1]] = x
right[x] = right[x+1]
if getlength(x) == m:
count += 1
# 左右都没有区间
else:
left[x] = x
right[x] = x
if m == 1:
count += 1
# 如果该步大小为m的分组不为0,那么res即为当前步骤(i+1)
if count > 0:
res = i + 1
return res
cpp:
class Solution {
public:
vector<int>left,right;
int findLatestStep(vector<int>& arr, int m) {
int n = arr.size();
// left[i]表示以结点i为连通分量右端点,它对应的左端点
// right[i]表示以结点i为连通分量左端点,它对应的右端点
// 这里的连通分量指的是连续的1
// 之所以长度为n+2,是因为后面要考虑left[i-1]和right[i+1],因此下标会取到0和n+1
left.resize(n+2);
right.resize(n+2);
// count表示长度为m的连通分量个数
int count = 0 ,res = -1;
for (int i = 0; i < n; i++){
int x = arr[i];
// 如果x的左右都有连通区间,那么这两个需要合并
if (left[x-1] && right[x+1]){
// 如果左边或者右边原来长度为m,那么m的数量--
if (getlength(left[x-1]) == m)
count --;
if (getlength(x+1) == m)
count --;
// 将区间合并,更新left和right
right[left[x-1]] = right[x+1];
left[right[x+1]] = left[x-1];
// 如果新的长度为m,count++
if (getlength(left[x-1]) == m)
count ++;
}
// 只有左边有区间
else if (left[x-1]){
if (getlength(left[x-1]) == m)
count --;
// 将左边区间向右扩展一位,更新left和right
right[left[x-1]] = x;
left[x] = left[x-1];
if (getlength(left[x-1]) == m)
count ++;
}
// 只有右边有区间
else if (right[x+1]){
if (getlength(x+1) == m)
count --;
// 将右边区间向左扩展一位,更新left和right
left[right[x+1]] = x;
right[x] = right[x+1];
if (getlength(x) == m)
count ++;
}
// 左右都没有区间
else{
left[x] = x;
right[x] = x;
if (m == 1)
count ++;
}
// 如果该步大小为m的分组不为0,那么res即为当前步骤(i+1)
if (count > 0)
res = i + 1;
}
return res;
}
// 返回以x为左端点的连通分量长度
int getlength(int x){
return right[x]-x+1;
}
};