P7907 [Ynoi2005] rmscne题解

发布时间 2024-01-07 21:16:43作者: Athanasy

题目链接:rmscne

神仙经典数据结构难题。看到求区间种类数有关的东西,需要下意识的反应到经典老题 HH的项链,这里可以学习我这篇 题解。具体学习下扫描线怎么做这类东西的。

看看本题,首先处理区间查询问题,而且是这种很复杂的子区间问题。这里的 \(l'\)\(r'\) 所组成的子区间 \([l',r']\) 实在太多了,我们可以先思考一个弱化问题版本。固定某一个端点再询问。

我们固定 \(r'=r\),考虑对于某个 \(r\) 来说,此时此刻的 \(l'\) 为满足答案的点,换句话来说,\([l',r]\)\([l,r]\) 的弱化版答案。我们来考虑一波 \(r\) 的移动会带来的影响,使用 HH 的项链的经典 \(last\) 分析,确保你会这个东西,否则需要看我上面说的那篇题解。

\(r+1\) 以后,区间会增加一个数 \(x\) 我们采用最经典得到 \(last_x\) 分析这个 \(x\) 导致的变化,很显而易见的是,如果 \(x\) 恰好为 \(l'\) 对应的点。那么很容易知道 HH 的项链里面的核心思想:后面新出行的 \(new_x\) 会代替掉原本的 \(last_x\) 的贡献,这个时候 \(l'\) 会向右移动。考虑一般情况,对于任意一个 \(last_x\) 显然在更新的时候都该做一个 \(last_x \rightarrow last_x +1\) 的操作。意思是如果我对于当前的 \(r\) 由于它有一个 \(x\),那么我的答案不需要包含这个 \(last_x\) 作为 \(l'\),向右移动一位,如果有多个肯定向右移动的不止一位了。所以这个问题也是需要考虑的。但总而言之,我们现在至少解决了一类问题 \([l,r]\) 中国如果右端点固定了,我们可以清楚地知道 \([l',r]\) 作为 \([l,r]\) 的最小被包含区间的最右 \(l'\) 的计算方法。

核心点的提出

那么我们反过来考虑固定的是 \(l\), 也即是 \(l'\)。它的 \(r'\) 是否也那么好找。我们注意到一个问题,这个 \(l'\) 并不是确定的,所以我们需要把每个点都认为是查询点 \(l=l'\) 去看。我们来考虑画出若干个点在 \(r\) 移动到 \(r+1\) 的变化。

我们观察若干个 \(l_i\) 的情况,我们很容易知道有个性质,\(r_i\) 一定单独调不降。因为 \(l_i\) 每移动一位就会少一个数,如果种类少了,需要后面的 \(r_i\) 移动进行补数操作。所以 \(r_i\) 不会下降的。再来一遍经典的 \(last_x\) 思想,很容易知道 \((last_x,r]\) 这个区间上已经没有 \(x\) 了,需要把 \(r+1\) 对应的 \(x\) 用于弥补,也就是后者代替前者贡献的基本思想。那么会有哪些 \(l'\) 需要包括这个新的 \(r+1\) 呢,显然是 \((last_x,r]\) 范围内的,因为他们都需要这个 \(x\) 进行补充。那么区改?覆盖?这不线段树吗?这样我们就可以做到同时维护一堆 \(l'\) 的前缀答案。我们惊讶地发现对于一个区间 \([l,r]\) 我们这样拆分:

\[[l,r]、[l+1,r],[l+2,r]....[l+diff,r],其中可以通过第一点有[l+diff,r]是 \]

\[最小的[l,r] 上的后缀答案,具体的获取后续说如何实现,它们每一个的 [l_i,r] 的答案 [l_i,r_i] \]

\[恰好又是第二点提到的所有的前缀最终答案,易知答案为 min(r_i-l_i+1)(前缀最小) \]

重新阐述下,首先呢答案 \([l',r']\) 既然满足 \([l,r]\) 的条件,那么也一定满足它的子区间,把它拆成若干个 \([l_i,r]\) 很显然,最终答案一定是某个左端点为 \(l_i\) 的,而对应答案的 \([l_{ans},r]\) 这个后缀区间是最长的,连它都不满足 \([l,r]\) 的前提,那就更不可能更小区间了。

如图所示,假如 \([l_i,r_i]\) 是答案,那么它对应的后缀区间也一定是答案。而由于后缀区间随 \(l_i\) 增大而减小长度,所以越不容易满足条件的,这个显然是单调的,一定有一个 \(l_{max}\) 为最大的 \(l_i\) 点。然后答案很显而易见的就是这个小区间上的前缀区间,这个不就是我们说的第二点吗?

至此,大体已经讲清,正式阐述细节和算法核心思路。

算法逻辑与框架

  1. 首先若干个区间查询很麻烦,我们能不能只抽象成单点移动,显然参照 HH 的项链,我们只需要离线扫描线查询,从 \(1 \sim n\) 更新 \(r\) 再查询对应时刻答案即可。

  2. 对于 \(r\) 的移动,基于第一点,我们会发现它所更新的 \(last_x\) 也会向右移动一位,换句话来讲这可以看做区间合并的过程,你想到了什么?经典的并查集合加快合并区间的套路。为每个点维护一个当前能快速移动到的位置,这个位置是什么东西呢?让我们来想一下,点 \(l\) 能移动到 \(L\) 处在此时 \(r\) 时刻。那么显而易见的是在更新 \(r\) 的过程中在 \([l,L]\) 中,它都更新过 \(last_x\) ,最新的L处 \(last_x\) 还未被后面覆盖,如果我们在往右移动,岂不是没有补的了?wow 这个就是我们要找的那个最大的 \(l_{max}\)

  3. 考虑第二点说的,当 \(r\) 移动,我们需要把一系列点的 \(r'\) 改为当前的 \(r\),显而易见的是需要区间覆盖,然后我们还需要查询基于上述的 \([l,l_{max}]\) 范围内所有 \(l_i\) 后缀对应的答案前缀,也即是 \(\min(r_i-l_i+1)\),这玩意不就是区间最小值查询吗?所以线段树维护即可,具体的维护区间最小值,区间覆盖,它们的右端点都是需要覆盖成 \(r\) ,所以考虑左端点最大就行,每次覆盖更新的时候容易区间最小值一定为区间最右边的点到当前的 \(r\) 处,如图所示。

显然最小的区间最右边的点到当前点。然后注意区间合并并查集是向右合并,至此,全部完毕。数据量 \(2e6\),所以可以考虑上快读其实。

参照代码
#include <bits/stdc++.h>

//#pragma GCC optimize("Ofast,unroll-loops")

#define isPbdsFile

#ifdef isPbdsFile

#include <bits/extc++.h>

#else

#include <ext/pb_ds/priority_queue.hpp>
#include <ext/pb_ds/hash_policy.hpp>
#include <ext/pb_ds/tree_policy.hpp>
#include <ext/pb_ds/trie_policy.hpp>
#include <ext/pb_ds/tag_and_trait.hpp>
#include <ext/pb_ds/hash_policy.hpp>
#include <ext/pb_ds/list_update_policy.hpp>
#include <ext/pb_ds/assoc_container.hpp>
#include <ext/pb_ds/exception.hpp>
#include <ext/rope>

#endif

using namespace std;
using namespace __gnu_cxx;
using namespace __gnu_pbds;
typedef long long ll;
typedef long double ld;
typedef pair<int, int> pii;
typedef pair<ll, ll> pll;
typedef tuple<int, int, int> tii;
typedef tuple<ll, ll, ll> tll;
typedef unsigned int ui;
typedef unsigned long long ull;
typedef __int128 i128;
#define hash1 unordered_map
#define hash2 gp_hash_table
#define hash3 cc_hash_table
#define stdHeap std::priority_queue
#define pbdsHeap __gnu_pbds::priority_queue
#define sortArr(a, n) sort(a+1,a+n+1)
#define all(v) v.begin(),v.end()
#define yes cout<<"YES"
#define no cout<<"NO"
#define Spider ios_base::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
#define MyFile freopen("..\\input.txt", "r", stdin),freopen("..\\output.txt", "w", stdout);
#define forn(i, a, b) for(int i = a; i <= b; i++)
#define forv(i, a, b) for(int i=a;i>=b;i--)
#define ls(x) (x<<1)
#define rs(x) (x<<1|1)
#define endl '\n'
//用于Miller-Rabin
[[maybe_unused]] static int Prime_Number[13] = {0, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};

template <typename T>
int disc(T* a, int n)
{
    return unique(a + 1, a + n + 1) - (a + 1);
}

template <typename T>
T lowBit(T x)
{
    return x & -x;
}

template <typename T>
T Rand(T l, T r)
{
    static mt19937 Rand(time(nullptr));
    uniform_int_distribution<T> dis(l, r);
    return dis(Rand);
}

template <typename T1, typename T2>
T1 modt(T1 a, T2 b)
{
    return (a % b + b) % b;
}

template <typename T1, typename T2, typename T3>
T1 qPow(T1 a, T2 b, T3 c)
{
    a %= c;
    T1 ans = 1;
    for (; b; b >>= 1, (a *= a) %= c)if (b & 1)(ans *= a) %= c;
    return modt(ans, c);
}

template <typename T>
void read(T& x)
{
    x = 0;
    T sign = 1;
    char ch = getchar();
    while (!isdigit(ch))
    {
        if (ch == '-')sign = -1;
        ch = getchar();
    }
    while (isdigit(ch))
    {
        x = (x << 3) + (x << 1) + (ch ^ 48);
        ch = getchar();
    }
    x *= sign;
}

template <typename T, typename... U>
void read(T& x, U&... y)
{
    read(x);
    read(y...);
}

template <typename T>
void write(T x)
{
    if (typeid(x) == typeid(char))return;
    if (x < 0)x = -x, putchar('-');
    if (x > 9)write(x / 10);
    putchar(x % 10 ^ 48);
}

template <typename C, typename T, typename... U>
void write(C c, T x, U... y)
{
    write(x), putchar(c);
    write(c, y...);
}


template <typename T11, typename T22, typename T33>
struct T3
{
    T11 one;
    T22 tow;
    T33 three;

    bool operator<(const T3 other) const
    {
        if (one == other.one)
        {
            if (tow == other.tow)return three < other.three;
            return tow < other.tow;
        }
        return one < other.one;
    }

    T3() { one = tow = three = 0; }

    T3(T11 one, T22 tow, T33 three) : one(one), tow(tow), three(three)
    {
    }
};

template <typename T1, typename T2>
void uMax(T1& x, T2 y)
{
    if (x < y)x = y;
}

template <typename T1, typename T2>
void uMin(T1& x, T2 y)
{
    if (x > y)x = y;
}

constexpr int N = 2e6 + 10;
constexpr int INF = 1e9 + 7;

struct Node
{
    int cov;
    int len;
} node[N << 2];

#define cov(x) node[x].cov
#define len(x) node[x].len
int n;
//区间答案最小值
inline void push_up(const int curr)
{
    len(curr) = min(len(ls(curr)),len(rs(curr)));
}

inline void push_down(const int curr, const int l, const int r)
{
    const int mid = l + r >> 1;
    if (cov(curr))
    {
        cov(ls(curr)) = cov(rs(curr)) = cov(curr);
        //[l,mid] [mid+1,r] cov(curr) 最小值为右端点到当前点
        len(ls(curr)) = cov(curr) - mid + 1;
        len(rs(curr)) = cov(curr) - r + 1;
        cov(curr) = 0;
    }
}

inline void cover(const int curr, const int l, const int r, const int val, const int s = 1, const int e = n)
{
    //用区间右端点更新
    if (l <= s and e <= r)
    {
        cov(curr) = val;
        len(curr) = val - e + 1;
        return;
    }
    const int mid = s + e >> 1;
    push_down(curr, s, e);
    if (l <= mid)cover(ls(curr), l, r, val, s, mid);
    if (r > mid)cover(rs(curr), l, r, val, mid + 1, e);
    push_up(curr);
}

//查询最小值
inline int query(const int curr, const int l, const int r, const int s = 1, const int e = n)
{
    if (l <= s and e <= r)
    {
        return len(curr);
    }
    const int mid = s + e >> 1;
    push_down(curr, s, e);
    int ans = n + 1;
    if (l <= mid)uMin(ans, query(ls(curr), l, r, s, mid));
    if (r > mid)uMin(ans, query(rs(curr), l, r, mid + 1, e));
    return ans;
}

int fa[N];

inline int find(const int x)
{
    return x == fa[x] ? x : fa[x] = find(fa[x]);
}

int last[N];
vector<pii> child[N];
int q;
int a[N], ans[N];

inline void solve()
{
    read(n);
    forn(i, 1, n)read(a[i]), fa[i] = i;
    read(q);
    forn(i, 1, q)
    {
        int l, r;
        read(l, r);
        child[r].emplace_back(l, i);
    }
    forn(r, 1, n)
    {
        int& preLast = last[a[r]];
        cover(1, preLast + 1, r, r); //影响的区间范围每个的ri被覆盖
        if (preLast)fa[preLast] = preLast + 1; //并查集向右合并
        preLast = r;
        //更新到r后查询答案
        for (const auto& [l,id] : child[r])
        {
            int L = l, R = find(l); //[l,lmax]
            ans[id] = query(1, L, R);
        }
    }
    forn(i, 1, q)write(endl, ans[i]);
}

signed int main()
{
    Spider
    //------------------------------------------------------
    int test = 1;
    //    read(test);
    // cin >> test;
    forn(i, 1, test)solve();
    //    while (cin >> n, n)solve();
    //    while (cin >> test)solve();
}

\[最终时间复杂度基于线段树和并查集的修改与查询综合考虑为\ O((n+q)\log{n}) \]