Algorithm 在滑动窗口中查找第二大元素

Algorithm 在滑动窗口中查找第二大元素,algorithm,dynamic-programming,sliding-window,Algorithm,Dynamic Programming,Sliding Window,所以给定一个数组和一个窗口大小,我需要在每个窗口中找到第二个最大值。蛮力解决方案非常简单,但我想用动态规划找到一个有效的解决方案 蛮力解决方案在我尝试大阵列时超时,因此我需要找到更好的解决方案。我的解决方案是通过对每个滑动窗口进行排序并获得第二个元素来找到第二个最大值。我知道有些数据结构可以更快地排序,但我想知道是否有更好的方法。在^2解决方案上有一个相对简单的dunamic编程: 在子集上构建聚合值的经典金字塔结构,在该结构中,您将下面成对的值组合在一起,形成上面的每一步,跟踪最大的2个值及其

所以给定一个数组和一个窗口大小,我需要在每个窗口中找到第二个最大值。蛮力解决方案非常简单,但我想用动态规划找到一个有效的解决方案


蛮力解决方案在我尝试大阵列时超时,因此我需要找到更好的解决方案。我的解决方案是通过对每个滑动窗口进行排序并获得第二个元素来找到第二个最大值。我知道有些数据结构可以更快地排序,但我想知道是否有更好的方法。

在^2解决方案上有一个相对简单的dunamic编程:
在子集上构建聚合值的经典金字塔结构,在该结构中,您将下面成对的值组合在一起,形成上面的每一步,跟踪最大的2个值及其位置,然后简单地保留4个组合值中的最大2个值,这在实践中由于重叠而较少,使用该位置确保它们实际上是不同的。然后,您只需从具有正确滑动窗口大小的层读取第二大值。

在^2解决方案上有一个相对简单的dunamic编程:
在子集上构建聚合值的经典金字塔结构,在该结构中,您将下面成对的值组合在一起,形成上面的每一步,跟踪最大的2个值及其位置,然后简单地保留4个组合值中的最大2个值,这在实践中由于重叠而较少,使用该位置确保它们实际上是不同的。然后,您只需从具有正确滑动窗口大小的层中读取第二大值。

因此,只需采用与设置类似的数据结构,以有序地存储数据即可。 比如说,如果你将426存储在集合中,它将存储为246

那么算法是什么呢

让,

数组=[12,8,10,11,4,5] 窗口大小=4

第一个窗口=[12,8,10,11] 集合=[8,10,11,12]

如何获得第二高: -从集合中删除最后一个元素并存储在容器中。set=[8,10,11],CONTANIER=12 -删除后,集合的当前最后一个元素是当前窗口的第二大元素。 -再次将存储在容器中的已删除元素放入集合,集合=[8,10,11,12] 现在移开你的窗户, -从集合中删除12,然后添加4。 -现在,您将获得新窗口并设置。 -检查类似的过程。 在集合中删除和添加元素的复杂性与logn有关

其中一个窍门是:


如果总是希望按降序存储数据,则可以通过将数据乘以-1来存储数据。当你弹出数据时,用它乘以-1

因此,只需将数据结构设置为有序存储数据的数据结构即可。 比如说,如果你将426存储在集合中,它将存储为246

那么算法是什么呢

让,

数组=[12,8,10,11,4,5] 窗口大小=4

第一个窗口=[12,8,10,11] 集合=[8,10,11,12]

如何获得第二高: -从集合中删除最后一个元素并存储在容器中。set=[8,10,11],CONTANIER=12 -删除后,集合的当前最后一个元素是当前窗口的第二大元素。 -再次将存储在容器中的已删除元素放入集合,集合=[8,10,11,12] 现在移开你的窗户, -从集合中删除12,然后添加4。 -现在,您将获得新窗口并设置。 -检查类似的过程。 在集合中删除和添加元素的复杂性与logn有关

其中一个窍门是:


如果总是希望按降序存储数据,则可以通过将数据乘以-1来存储数据。当你弹出数据时,用它乘以-1

我们可以为On解决方案使用双端队列。队列前面将有较大且较早看到的元素:

  0  1  2  3  4  5
{12, 8,10,11, 4, 5}
window size: 3

i   queue (stores indexes)
-   -----
0   0
1   1,0
2   2,0 (pop 1, then insert 2)
output 10
remove 0 (remove indexes not in
   the next window from the front of
   the queue.)
3   3 (special case: there's only one
   smaller element in queue, which we
   need so keep 2 as a temporary variable.)
output 10
4   4,3
output 10
remove 2 from temporary storage
5   5,3 (pop 4, insert 5)
output 5

前面的pop和remove是[queue_back]我们可以使用双端队列作为On解决方案。队列前面将有较大且较早看到的元素:

  0  1  2  3  4  5
{12, 8,10,11, 4, 5}
window size: 3

i   queue (stores indexes)
-   -----
0   0
1   1,0
2   2,0 (pop 1, then insert 2)
output 10
remove 0 (remove indexes not in
   the next window from the front of
   the queue.)
3   3 (special case: there's only one
   smaller element in queue, which we
   need so keep 2 as a temporary variable.)
output 10
4   4,3
output 10
remove 2 from temporary storage
5   5,3 (pop 4, insert 5)
output 5

从前面弹出和删除是[queue_back]有很多方法可以解决这个问题。这里有几个选择。在下面的内容中,我将让n表示输入数组中的元素数,w表示窗口大小

选项1:一个简单的日志w时间算法 一种选择是维护一个平衡的二进制搜索树,其中包含当前窗口中的所有元素,包括重复项。在这个BST中插入一些东西需要花费时间Olog w,因为窗口中只有w个元素,出于同样的原因,删除一个元素也需要时间Olog w。这意味着将窗口滑动一个位置需要时间

要查找窗口中的第二大元素,只需应用一个,这需要在包含w元素的BST中花费时间

这种方法的优点是,在大多数编程语言中,编写这种方法相当简单。它还利用了大量资源 使用著名的标准技术。缺点是运行时不是最优的,我们可以改进它

选项2:基于前缀/后缀的算法 这是一个相对简单的线性时间解决方案。在较高级别上,解决方案通过将阵列拆分为一系列块来工作,每个块的大小为w。例如,考虑以下数组:

31  41  59  26  53  58  97  93  23  84  62  64  33  83  27  95  02  88  41  97
假设w=5。我们将数组拆分为大小为5的块,如下所示:

31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
                             |-----------------|
现在,想象一下将一个长度为5的窗口放置在此数组中的某个位置,如下所示:

31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
                             |-----------------|
请注意,此窗口始终由一个块的后缀和另一个块的前缀组成。这很好,因为它允许我们解决一个稍微简单的问题。想象一下,不知何故,我们可以有效地确定任何块的任何前缀或后缀中的两个最大值。然后,我们可以在任何窗口中找到第二个最大值,如下所示:

找出窗口对应的块的前缀和后缀。 如果窗口足够小,则从每个前缀和后缀中获取最上面的两个元素,或者仅获取最上面的一个元素。 在最多四个值中,确定第二大值并返回它。 通过一点预处理,我们确实可以设置窗口来回答以下形式的查询:每个后缀中最大的两个元素是什么?每个前缀中最大的两个元素是什么?您可以将其视为一个动态规划问题,设置如下:

对于长度为1的任何前缀/后缀,将单个值存储在该前缀/后缀中。 对于长度为2的任何前缀/后缀,前两个值是两个元素本身。 对于任何较长的前缀或后缀,该前缀或后缀可以通过单个元素扩展较小的前缀或后缀来形成。要确定较长前缀/后缀的前两个元素,请将用于扩展范围的元素与前两个元素进行比较,然后从该范围中选择前两个。 请注意,填写每个前缀/后缀的前两个值需要时间O1。这意味着我们可以及时填写任何窗口,因为有w个条目需要填写。此外,由于有On/w total窗口,因此填写这些条目所需的总时间是On的,因此我们的整体算法在On上运行

至于空间使用:如果您急切地计算整个数组中的所有前缀/后缀值,则需要使用空格来保存所有内容。但是,由于在任何时候我们只关心两个窗口,因此您也可以只在需要时计算前缀/后缀。这将只需要空间哦,这是真的,真的很好

选项3:使用智能数据结构的准时解决方案 最后一种方法与上述方法完全相同,但框架不同

这是可能的。这个队列背后的思想——从两堆栈队列构造开始,然后在两堆栈队列构造中使用它——可以很容易地推广到构建一个队列,该队列提供对第二大元素的恒定时间访问。为此,您只需调整堆栈结构,在每个时间点存储最上面的两个元素,而不仅仅是最大的元素

如果您有这样一个队列,那么在任何窗口中查找第二个最大值的算法都非常快:使用前w个元素加载队列,然后重复将一个元素移出窗口,将下一个元素移入窗口。这些操作中的每一项都需要摊销O1时间才能完成,因此总体而言这需要时间

有趣的事实-如果你看看这个队列实现在这个特定用例中实际做了什么,你会发现它完全等同于上面的策略。一个堆栈对应于前一个块的后缀,另一个堆栈对应于下一个块的前缀

最后一个策略是我个人最喜欢的,但不可否认,这只是我自己的数据结构偏见


希望这有帮助

有很多方法可以解决这个问题。这里有几个选择。在下面的内容中,我将让n表示输入数组中的元素数,w表示窗口大小

选项1:一个简单的日志w时间算法 一种选择是维护一个平衡的二进制搜索树,其中包含当前窗口中的所有元素,包括重复项。在这个BST中插入一些东西需要花费时间Olog w,因为窗口中只有w个元素,出于同样的原因,删除一个元素也需要时间Olog w。这意味着将窗口滑动一个位置需要时间

要查找窗口中的第二大元素,只需应用一个,这需要在包含w元素的BST中花费时间

这种方法的优点是,在大多数编程语言中,编写这种方法相当简单。它还利用了一系列众所周知的标准技术。缺点是运行时不可用 最佳,我们可以改进它

选项2:基于前缀/后缀的算法 这是一个相对简单的线性时间解决方案。在较高级别上,解决方案通过将阵列拆分为一系列块来工作,每个块的大小为w。例如,考虑以下数组:

31  41  59  26  53  58  97  93  23  84  62  64  33  83  27  95  02  88  41  97
假设w=5。我们将数组拆分为大小为5的块,如下所示:

31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
                             |-----------------|
现在,想象一下将一个长度为5的窗口放置在此数组中的某个位置,如下所示:

31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
31  41  59  26  53 | 58  97  93  23  84 | 62  64  33  83  27 | 95  02  88  41  97
                             |-----------------|
请注意,此窗口始终由一个块的后缀和另一个块的前缀组成。这很好,因为它允许我们解决一个稍微简单的问题。想象一下,不知何故,我们可以有效地确定任何块的任何前缀或后缀中的两个最大值。然后,我们可以在任何窗口中找到第二个最大值,如下所示:

找出窗口对应的块的前缀和后缀。 如果窗口足够小,则从每个前缀和后缀中获取最上面的两个元素,或者仅获取最上面的一个元素。 在最多四个值中,确定第二大值并返回它。 通过一点预处理,我们确实可以设置窗口来回答以下形式的查询:每个后缀中最大的两个元素是什么?每个前缀中最大的两个元素是什么?您可以将其视为一个动态规划问题,设置如下:

对于长度为1的任何前缀/后缀,将单个值存储在该前缀/后缀中。 对于长度为2的任何前缀/后缀,前两个值是两个元素本身。 对于任何较长的前缀或后缀,该前缀或后缀可以通过单个元素扩展较小的前缀或后缀来形成。要确定较长前缀/后缀的前两个元素,请将用于扩展范围的元素与前两个元素进行比较,然后从该范围中选择前两个。 请注意,填写每个前缀/后缀的前两个值需要时间O1。这意味着我们可以及时填写任何窗口,因为有w个条目需要填写。此外,由于有On/w total窗口,因此填写这些条目所需的总时间是On的,因此我们的整体算法在On上运行

至于空间使用:如果您急切地计算整个数组中的所有前缀/后缀值,则需要使用空格来保存所有内容。但是,由于在任何时候我们只关心两个窗口,因此您也可以只在需要时计算前缀/后缀。这将只需要空间哦,这是真的,真的很好

选项3:使用智能数据结构的准时解决方案 最后一种方法与上述方法完全相同,但框架不同

这是可能的。这个队列背后的思想——从两堆栈队列构造开始,然后在两堆栈队列构造中使用它——可以很容易地推广到构建一个队列,该队列提供对第二大元素的恒定时间访问。为此,您只需调整堆栈结构,在每个时间点存储最上面的两个元素,而不仅仅是最大的元素

如果您有这样一个队列,那么在任何窗口中查找第二个最大值的算法都非常快:使用前w个元素加载队列,然后重复将一个元素移出窗口,将下一个元素移入窗口。这些操作中的每一项都需要摊销O1时间才能完成,因此总体而言这需要时间

有趣的事实-如果你看看这个队列实现在这个特定用例中实际做了什么,你会发现它完全等同于上面的策略。一个堆栈对应于前一个块的后缀,另一个堆栈对应于下一个块的前缀

最后一个策略是我个人最喜欢的,但不可否认,这只是我自己的数据结构偏见


希望这有帮助

我不认为动态规划是答案。一个专门化的应该可以。@user3386109:建议一个堆专门支持按键删除。不知道我为什么说min heap。那应该是max heap。专门化是@greybeard提到的1个按键移除和2个peek第二大。请注意,根是堆中最大的元素。要么是根的左子元素,要么是根的右子元素,这是第二大元素。@user3386109我认为动态编程可能会有所帮助,就像在滑动窗口中一样,现有的第二大元素可能仍然是第二大元素,如果不删除它或第一大元素。当然也会检查使用最小和最大堆。感谢you@user3386109我认为这里可能有一个解决方案,不是吗?我不认为动态规划是答案。一个专门化的应该可以。@user3386109:建议一个堆专门支持按键删除。不知道我为什么说min heap。那应该是max heap。专门化是@greybeard提到的1个按键移除和2个peek第二大。注意,根是t

他是堆中最大的元素。要么是根的左子元素,要么是根的右子元素,这是第二大元素。@user3386109我认为动态编程可能会有所帮助,就像在滑动窗口中一样,现有的第二大元素可能仍然是第二大元素,如果不删除它或第一大元素。当然也会检查使用最小和最大堆。感谢you@user3386109我想这里可能有一个On解决方案,不是吗?我的蛮力解决方案几乎是一样的,只是我对数字进行排序,而不是使用集合。集合的优点是较低的复杂性。我想知道是否有其他方法来解决这个问题。谢谢这里还有其他一些方法!类似于段树数据结构的复杂性。如果我的回答有帮助,请投票表决。ThanksMy brute force的解决方案几乎相同,只是我对数字进行了排序,而不是使用集合。集合的优点是较低的复杂性。我想知道是否有其他方法来解决这个问题。谢谢这里还有其他一些方法!类似于段树数据结构的复杂性。如果我的回答有帮助,请投票表决。谢谢第二位:我不明白你的答案。调用窗口和队列k的大小,对于您的解决方案所花费的时间,nk是否是一个严格的上限?如果没有:如何在移除一段时间内有ben输出的元素后,选择下一个要输出的元素?@greybeard我们从正面右侧输出第二个元素。从与用户3386109的交换中:请设置一个窗口大小>4的测试框架和测试用例,编写方法代码并报告结果。一个程序或程序代码的草图也会有好处。@greybeard我更愿意解释任何简单的反例。为什么大于而不仅仅等于4?您能否详细说明如何决定在每个点从队列中删除哪个值?例如,假设我的窗口大小为3,数组为[10,12,9,5]。当10从窗口移出时,我该如何把它从队列中踢出去?我第二个问题:我不明白你的答案。调用窗口和队列k的大小,对于您的解决方案所花费的时间,nk是否是一个严格的上限?如果没有:如何在移除一段时间内有ben输出的元素后,选择下一个要输出的元素?@greybeard我们从正面右侧输出第二个元素。从与用户3386109的交换中:请设置一个窗口大小>4的测试框架和测试用例,编写方法代码并报告结果。一个程序或程序代码的草图也会有好处。@greybeard我更愿意解释任何简单的反例。为什么大于而不仅仅等于4?您能否详细说明如何决定在每个点从队列中删除哪个值?例如,假设我的窗口大小为3,数组为[10,12,9,5]。当10被移出窗口时,我如何将其从队列中踢出?我不明白为什么我们需要在您共享的链接中描述的特殊get_max操作,因为我们可以像我所描述的那样从简单队列中弹出较小的元素,因为这些元素无论如何都是无用的。@可以弹出较小的元素。我仔细阅读了您的解决方案,不幸的是,我无法理解您如何决定在队列中插入或删除元素的基本原理。您能详细说明一下吗?每个元素都会插入队列,但只有在从后面移除所有较小或相等的元素,并且从前面移除此窗口之外的所有元素之后。因为我们正在寻找第二大的,所以我们需要维护一个偶尔填充的临时变量,它曾经是队列中较小的单个元素,与队列中当前最大的两个元素进行比较,并适当地丢弃。由于队列存储索引,很容易判断前面是否在窗口之外。我不明白为什么我们需要在您共享的链接中描述的特殊get_max操作,因为我们可以像我所描述的那样从简单队列中弹出较小的元素,因为这些元素无论如何都是无用的。@元素。我仔细阅读了您的解决方案,不幸的是,我无法理解您如何决定在队列中插入或删除元素的基本原理。您能详细说明一下吗?每个元素都会插入队列,但只有在从后面移除所有较小或相等的元素,并且从前面移除此窗口之外的所有元素之后。因为我们正在寻找第二大的,所以我们需要维护一个偶尔填充的临时变量,它曾经是队列中较小的单个元素,与队列中当前最大的两个元素进行比较,并适当地丢弃。由于队列存储索引,因此很容易判断前端是否在窗口之外。