zoukankan      html  css  js  c++  java
  • 浅谈「First bigger」问题

    浅谈「First bigger」问题

    概述

    本博客主要介绍关于「求序列中每一个数前面第一个大于它的数」一类问题的各种解法。

    相关表述还有「求序列中每一个数前面比它大的数中最靠近它的数」,「求序列中每一个数后面第一个比它大的数」,「求序列中每一个数前面第一个比它小的数」等,都属于同类问题,在这里只对于「求序列中每一个数前面第一个大于它的数」问题进行讨论。

    问题简析

    给定一个长度为 \(n\) 的正整数序列 \(a\) ,其中序列的第 \(i\) 个元素 \(a_i\) 满足 \(1\le a_i\le 10^5\)

    对于每一位置 \(i\) ,求在该数前第一个大于它的数。

    可以发现,问题实际上要我们求一个这样的问题。

    • \(\forall 1\le j<i, a_j>a_i\) ,求 \(\max j\)

    我们设 \(d_i\) 表示位置 \(i\) 的答案

    解法1

    \(O(n^2)\) 算法

    暴力

    该问题可以使用二重循环进行暴力求解。

    对于每一个 \(i\) ,我们直接枚举所有 \(1\le j<i\) ,对于所有 \(a_j>a_i\) ,求 \(j\) 最大的值即可。时间复杂度为 \(O(n^2)\)

    实现上一个简单的优化为我们考虑从大到小枚举 \(j\) ,一旦找到了 \(a_j>a_i\) 就直接 break 即可。这样做可以对暴力进行常数上的优化,不过时间复杂度仍为 \(O(n^2)\)

    玄学优化

    可以发现,简单的二重循环暴力已经无法进行优化,考虑使用一定的贪心。

    观察序列,我们可以发现对于每一个数 \(i\) ,如果 \(a_{i-1}>a_i\) 的话,则 \(d_i=i-1\) 。否则考虑判断如果 \(a_{d_{i-1}}>a_i\) ,则 \(d_i=d_{i-1}\) 否则继续递归循环。可以发现,这样做在随机情况可以做到近似 \(O(n)\) 的均摊的时间复杂度。不过仍能被极端数据卡成 \(O(n^2)\)

    解法2

    \(O(n\sqrt n)\) 算法

    分块

    考虑使用分块解决问题。

    我们首先对于序列进行分块,对于每一块处理出块中的最大值。

    对于 \(i\) 来说,我们首先查询 \(i\) 左侧不属于完整一块的数。如果存在 \(a_j>a_i\)\(d_i=j\)

    否则我们考虑一块一块的暴力跳,如果当前块最大值 \(a_j>a_i\) ,说明 \(d_i\) 位于当前块内,暴力查找块内位置即可。

    否则考虑继续查询下一块。容易发现,这样做的时间复杂度是 \(O(n\sqrt n)\) 的。

    解法3

    \(O(nlog^2n)\) 算法

    二分+线段树

    接下来,我们考虑如何优化上述 \(O(n^2)\) 的算法。

    一个比较容易想到的思路是使用数据结构进行维护。

    首次,我们从左往右遍历序列,同时对线段树进行维护,这样就可以满足 \(1\le j<i\) 的条件了。

    然后我们考虑对于线段树上的每一个位置维护其权值,查询时我们只需要二分倍增一下答案,这样就满足 \(j\) 最大的条件了。

    check 函数的实现即为查询当前区间是否存在值大于 \(a_i\) 即可。也即查询当前区间最大值是否大于 \(a_i\) 即可,这样就满足 \(a_j>a_i\) 的条件了。

    解法4

    \(O(nlogn)\) 算法

    二分+st表

    考虑如何优化上述 \(O(nlog^2n)\) 的算法。

    我们发现,使用线段树维护是完全没有必要的。因为我们二分时 check 的都是 \(j<i\) 的区间,因此并不会出现 \(d_i=j\ge i\) 的情况。

    因此,考虑直接使用st表 \(O(nlogn)\) 预处理, \(O(1)\) 维护区间最大值,这样做的时间复杂度是 \(O(nlogn)\) 的。

    线段树二分

    考虑如何优化上述 \(O(nlog^2n)\) 的算法。

    我们发现,使用二分进行查询是完全没有必要的。我们完全可以做到直接在线段树上进行二分。

    由于我们线段树维护的值都满足 \(j<i\) 。因此我们在查询完全可以进行如下二分。

    如果当前区间的右边的最大值大于 \(a_i\) 则往右边跳,否则往左边跳。

    可以发现,我们只需要将未加入线段树的位置权值设为 \(-inf\) ,则往当右边最大值大于 \(a_i\) 时一定存在 \(j<i\) 使得 \(a_j>a_i\)

    权值线段树

    考虑换一种思路,我们可以使用权值线段树进行维护。

    首先,从左往右遍历序列仍然能够满足 \(1\le j<i\) 的条件。

    同时,我们对于权值进行维护。对于每个权值,我们维护其出现过的最大位置。

    \(d_i\) 就等于权值线段树上权值大于 \(a_i\) 的区间的位置的最大值。可以发现,这样做满足了 \(a_j>a_i\)\(\max j\) 的条件。

    解法5

    \(O(n)\) 算法

    单调栈

    考虑如何优化上述 \(O(nlogn)\) 的算法。

    首先我们可以发现,上述 \(log\) 级数据结构都不能使用了。因此我们考虑使用 \(O(n)\) 的数据结构,单调栈。

    在单调栈中,我们严格单调降序的维护所有权值。

    维护时,如果当前 \(a_i\) 权值大于等于栈顶,则弹出栈顶。最后的栈顶即为 \(d_i\) 。最后加入 \(a_i\) 即可。

    可以发现,由于弹出栈顶的数 \(j\) 是权值 \(a_j\) 小于等于 \(a_i\) 且位置 \(j\) 小于 \(i\) 的数。因此如果存在 \(d_k=j(k>i)\) 的话,\(d_k=i\) 一定更优。因此弹出栈顶元素不会影响答案。

    可以发现,由于所有元素都最多加入和弹出单调栈一次,因此时间复杂度为 \(O(n)\)

    //暴力
    //玄学dp
    //分块
    //二分+线段树
    //二分+st表
    //权值线段树
    //线段树二分
    //单调栈

  • 相关阅读:
    python的sorted相关
    dict两种遍历方法
    python 深拷贝和浅拷贝浅析
    牛人总结python中string模块各属性以及函数的用法,果断转了,好东西
    Python二分查找
    堆和栈区别
    一次完整的HTTP事务是怎样一个过程?(转)
    ------shell学习
    BZOJ1025 [SCOI2009]游戏
    BZOJ1024 [SCOI2009]生日快乐
  • 原文地址:https://www.cnblogs.com/ezlmr/p/15782068.html
Copyright © 2011-2022 走看看