P3863 序列 題解

Athanasy發表於2024-03-18

題目連結:序列

挺神仙的好題

關於時間維上的貢獻處理,之前做過一些類似的題,這題是很不錯的體現題。

對於一個數的查詢來說,我們暴力地看看它的變化:

時間維有個很重要的特點,當前時間點的修改只會影響後續的所有時間點。對於某個時間點 \(i\),如果它的修改 \([l,r]+val\) 包括了 \(pos\) 當前討論的點。那麼翻譯一下意思:

\[從當前時間點到後續,pos處的值都應該+val,這個影響持續到結束 \]

如圖所示,每個時間點僅僅只會影響它的後續時間點,在後續時間點單點查詢時,都會受到前面的修改影響。

引入一個好玩的東西

這裡為了幫助你理解這類跟時間維上的貢獻有關的題,我們引入一個新概念----時間陣列,這個概念是我為了幫助讀者理解這類知識點在本文中提出。

關於時間陣列,個人給出定義:

類似常見的序列維作為軸即為常見的序列陣列。數軸作為軸,即為所謂的權值陣列,桶之類的稱呼。這裡我們以時間作為軸,即時間作為下標,那麼它的值即為當前時間點處的值:

\[定義\ tim\ 表示\ x\ 的時間陣列,那麼\ tim[i]\ 表示當時間點為\ i\ 時,x的值為多少 \]

那麼暴力的,對於每個數它的時間陣列可以寫成一個二維的表:

如圖所示,每一行都表示一個元素的時間陣列,比如藍色塊表示的含義即為:在 \(time=4\) 時刻時,\(x_2\) 的值。那麼如果我們能維護這個時間表的變化,就能知道每個需要查的答案了。

我們將 \([x2,x3]\) 上的點在 \(i=4\) 這個時刻進行修改 \(+val\),如圖所示,很顯然,從 \(i=4\) 這個時刻到最後一個時刻都應該 \(+val\),這個顯然是一個二維區域上的 \(+val\) 操作。查詢,查什麼?其實查什麼咱都能查的。詢問:

對於某個 \(x\)\(time<queryTime\ \&\&\ val\ge queryValue\) 的數量。

翻譯成我們的時間陣列:

查詢 \(x\) 在時間表的所在行,即它所對應的時間陣列下標小於 \(queryTime\),值大於等於 \(queryValue\) 的個數,這個是不是就很形象了。

先考慮直接暴力搞下怎麼搞,二維樹狀陣列/線段樹維護區間加演算法,查詢外層對元素限制,內層對時間限制,然後再查權值,這豈不是:二維資料結構套權值樹,一看就是三支 \(\log\) 空間也巨大。回憶下,這個問題本質是什麼?權值計數類問題,也是經典可差性問題。

可差性問題普及

對於一類問題的答案我們是滿足這樣的式子:

\[ans(限制條件,l\le x\le r)=ans(限制條件,x\le r)-ans(限制條件,x\le l-1) \]

這類問題我們稱之為可差性問題。

回到題目

這玩意帶修,而且這個修改不好拆分,但是這個修改仔細觀察是一個怎樣的問題:

\[[i,lastTime]\ 與\ [l,r]\ 維持的矩形\ +val,典型的可差分問題 \]

直接做二維差分?當你看到 \(n=1e5,q=1e5\),就知道這個矩形大小為 \(1e10\),一定不怎麼能直接二維去做,空間一定不夠。這裡安利一道題:P3960 [NOIP2017 提高組] 列隊 題解

這種二維問題,離線演算法最經典,想想離線演算法裡面降維的神器:維度掃描線降維

維度掃描線降維普及

給出一個簡單的常見模型,\([l,r]\) 上查詢 \(\le x\) 的個數,並且查詢共 \(q\) 個,直接做顯然樹套樹啊,由於不帶修且可差性,主席樹也可以的。這裡講講怎麼離線掃描線降維:

對於一個查詢 \([l,r]\) 這是一個可差性問題:

\[ans(val \le x,l \le i \le r)=ans(val \le x,i \le r)-ans(val \le x,i\le l) \]

這意味著一個詢問我們可以拆成兩個,且偏序限制從原來的三個變成了兩個:

三個偏序分別為下標左右限制,值的限制,共三個限制。

兩個偏序限制分別為下標的限制和值的限制,共兩個限制。

所以一個常見的偏序條件限制,我們可以透過不斷地可差性問題減少限制條件。兩個偏序限制就是一個最純粹的二維偏序問題,這類問題想必你一定不陌生,你在學習逆序對的時候有過這種類似寫法。

對於 \(i \le idx\) 的這個限制是屬於序列維度上的限制,對於 \(val \le x\),這個是屬於值域維度上的限制,這是兩個維度限制,比較暴力的做法就是直接二維資料結構做,講講離線掃描線怎麼降維:

  1. 選擇一個維度進行正確的行駛方向。

  2. 將另一個維度的查詢掛載在這個維度的限制最大點上。

  3. 選擇的維度開始更新,每次到一個點就進行掛載點的更新與詢問回答。

實戰:

方案1:

  1. 選擇序列維作為我們的掃描線維,我們從下標 \(1\)\(n\) 的方向走,因為詢問的限制是 \(i \le idx\),所以我們應該從小往大更新。

  2. 如果一個查詢的限制為 \(i\le idx\),那麼我們將這個詢問掛載在 \(i=idx\) 的點上,這樣當我們從小到大更新到 \(i=idx\) 時,這個時候所以的滿足這個限制的點都已經進入我們的維護當中。

  3. 從下標 \(1\) 開始更新與查詢,我們選擇權值樹狀陣列或者權值線段樹維護當前情況,我們每訪問一個數,我們就加入到權值資料結構中,對於一個掛載查詢,我們發現:它的第一個限制一定滿足,我們只需要關注第二個值域限制,並且所有滿足權值限制的數已經進入權值 ds 中,正確查詢即可。

程式碼描述
constexpr int N = 1e5 + 10;
typedef pair<int, int> pii; //(val<=v,queryId)
vector<pii> seg[N];
int n;
int a[N];
void add(int x); //權值ds加入一個數
int query(int x); //權值dds查詢<=x的數的個數
int ans[N];

inline void solve()
{
    forn(i, 1, n)
    {
        add(a[i]);
        for (const auto [val,queryId] : seg[i])ans[queryId] += query(val);
    }
}

這樣一來我們就完成了所有查詢,當然可以根據實際需求增加更多引數,比如需要 \(-\) 之類的。我們發現降維是降了我們選擇的序列維,序列維的限制沒了,只有值域限制了。當然我們還有一種寫法,不需要掛載,直接記錄所有查詢,按照查詢下標進行排序即可。

程式碼描述
constexpr int N = 1e5 + 10;
typedef tuple<int, int, int> tii; //(queryIdx,val<=v,queryId)
vector<tii> seg;
int n;
int a[N];
void add(int x); //權值ds加入一個數
int query(int x); //權值dds查詢<=x的數的個數
int ans[N];

inline void solve()
{
    sort(seg.begin(), seg.end()); //按照查詢的下標排序
    int curr = 1;
    forn(i, 1, n)
    {
        add(a[i]);
        while (curr < seg.size())
        {
            auto [queryIdx,val,id] = seg[curr];
            if (queryIdx > i)break;
            ans[id] += query(val);
            curr++;
        }
    }
}

是不是感覺有雙指標的味道了,這個降維大大降低了空間維護和時間複雜度。

方案2:

考慮權值維作為掃描線軸,我們採用剛剛說的第二種,按照值域把原陣列帶著下標排序,即當前陣列元素 \(a[1]\) 表示值為最小的元素的下標,可以不用離散化。我們按照值域從小到大加入下標貢獻,然後令一維按照值域雙指標限制,進行下標限制查詢,完全與上述一致。

程式碼描述
constexpr int N = 1e5 + 10;
typedef tuple<int, int, int> tii; //(val,queryId,queryId)
vector<tii> seg;
int n;
pii a[N]; //(值,下標)
void add(int x); //權值ds加入一個數
int query(int x); //權值dds查詢<=x的數的個數
int ans[N];

inline void solve()
{
    sort(a + 1, a + n + 1); //按照值排序
    sort(seg.begin(), seg.end()); //按照查詢的值排序
    int curr = 1;
    forn(i, 1, n)
    {
        const auto [v,idx] = a[i];
        add(idx); //加入下標貢獻
        while (curr < seg.size())
        {
            auto [val,queryIdx,id] = seg[curr];
            if (val > v)break;
            ans[id] += query(queryIdx); //查詢下標限制
            curr++;
        }
    }
}

這就是離線掃描線的降維思想,透過思想,我們可以很輕鬆地降維。

回到本題

查詢幾個維度?三個,對下標的限制,對時間的限制,對值域的限制。選哪個維?都差不多,我們選擇最好寫的序列維。序列維限制沒了,剩個啥?時間維上的值域維查詢。先寫出原問題:

查詢序列維 \(=idx\) 的時間維 \(<time\) 的值域維 \(\ge val\) 的數量。

降維後的問題,查詢當前資料結構中,時間維 \(<time\),值域維 \(\ge val\) 的數量。

嚯,很簡單吧。現在來處理最棘手的區間修改的影響。區間修改我們也翻譯成:

對序列維在 \([l,r]\) 上且時間維為 \([time,lastTime]\)時間陣列 \(+val\)。這咋做,這玩意可以差分,這是可差性問題,對於可差性問題的修改而言,我們常常考慮差分修改:

\(idx=l\) 做時間維的修改增加,對 \(idx=r+1\) 做時間維的修改減少:

掃描線是序列維,我們從上往下更新,當前的資料結構即為 \(i=idx\) 這一時刻的時間陣列資訊,我們將原來的區改拆成了:兩個序列時刻的變化,掛載在對應的序列點上就行。這樣問題就變為了:

時間陣列上的:

區間增加 \(+val\),區間查詢 \([l,r]\)\(\ge x\) 的數量。這個就是比較經典的分塊入門題教主的魔法:每個塊維護有序陣列,然後逐塊二分就行了,區改就維護一個標記永久化就行了。時間複雜度為。\(1e5\) 根號帶 \(\log\) 跑滿差不多 \(2e8\) 左右,\(2s\) 綽綽有餘。

細節

查詢是從 \(0\) 時刻開始,我們時間陣列分塊維護喜歡從 \(1\) 開始,可以整體時間點右移一位,減少查詢常數的一些方式:

同時維護塊的 \(max\)\(min\) 來輔助查詢不跑二分,減少重構常數可以使用布林陣列,當需要查詢的時候再重構塊。

參照程式碼
#include <bits/stdc++.h>

// #pragma GCC optimize(2)
// #pragma GCC optimize("Ofast,no-stack-protector,unroll-loops,fast-math")
// #pragma GCC target("sse,sse2,sse3,ssse3,sse4.1,sse4.2,avx,avx2,popcnt,tune=native")

#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 = 1e5 + 10;
//塊下標,塊開始,塊結束
int pos[N], s[N], e[N];
int n, q;
//原陣列,時間陣列,時間陣列分塊後每個塊整體加的數量,每個塊的有序陣列
ll a[N], tim[N], tag[N], ord[N];
bool vis[N]; //減少常數,該塊是否發生變化,需要重構

//重構有序塊
inline void rebuild(const int id)
{
    if (!vis[id])return;
    forn(i, s[id], e[id])ord[i] = tim[i];
    sort(ord + s[id], ord + e[id] + 1);
    vis[id] = false;
}

//[l,r]+val
inline void add(const int l, const int r, const int val)
{
    const int L = pos[l], R = pos[r];
    if (L == R)
    {
        forn(i, l, r)tim[i] += val;
        vis[L] = true;
        return;
    }
    forn(i, l, e[L])tim[i] += val;
    forn(i, s[R], r)tim[i] += val;
    forn(i, L+1, R-1)tag[i] += val;
    vis[L] = vis[R] = true;
}

//二分有序塊>=val的個數,記得tag表示整個塊+了多少,需要去掉
inline int binarySize(const int id, const int val)
{
    rebuild(id);
    const ll v = val - tag[id];
    if (v > ord[e[id]])return 0;
    return e[id] - (ranges::lower_bound(ord + s[id], ord + e[id] + 1, v) - ord) + 1;
}

//[l,r]>=val的數量
inline int query(const int l, const int r, const ll val)
{
    if (l > r)return 0;
    const int L = pos[l], R = pos[r];
    int ans = 0;
    if (L == R)
    {
        forn(i, l, r)ans += tim[i] >= val - tag[L];
        return ans;
    }
    forn(i, l, e[L])ans += tim[i] >= val - tag[L];
    forn(i, s[R], r)ans += tim[i] >= val - tag[R];
    forn(i, L+1, R-1)ans += binarySize(i, val);
    return ans;
}

//修改和查詢分別掛載在序列掃描線上
vector<pii> segUpdate[N];
vector<tii> segQuery[N];
int ans[N];
int ansIdx;

inline void solve()
{
    cin >> n >> q;
    q++; //查詢時間點右移從1開始
    //時間陣列分塊
    const int siz = sqrt(q);
    const int cnt = (q + siz - 1) / siz;
    forn(i, 1, n)cin >> a[i];
    forn(i, 1, q)pos[i] = (i - 1) / siz + 1;
    forn(i, 1, cnt)s[i] = (i - 1) * siz + 1, e[i] = i * siz;
    e[cnt] = q;
    //第一次修改時間點從2開始
    forn(i, 2, q)
    {
        int op;
        cin >> op;
        if (op == 1)
        {
            int l, r, val;
            cin >> l >> r >> val;
            //差分掛載在序列掃描線上,[i,q]上時間陣列修改
            segUpdate[l].emplace_back(i, val);
            segUpdate[r + 1].emplace_back(i, -val);
        }
        else
        {
            int pos, val;
            cin >> pos >> val;
            //查詢掛載在掃描線上,<i <=><=[1,i-1]
            segQuery[pos].emplace_back(++ansIdx, i - 1, val);
        }
    }
    forn(i, 1, n)
    {
        //當前數加入當前行所在時間陣列
        add(1, q, a[i]);
        for (const auto [curr,val] : segUpdate[i])add(curr, q, val); //先修改
        for (const auto [id,curr,val] : segQuery[i])ans[id] = query(1, curr, val); //再查詢另外兩個限制
        add(1, q, -a[i]);
        //當前數從當前行所在時間陣列刪除
    }
    forn(i, 1, ansIdx)cout << ans[i] << endl;
}

signed int main()
{
    // MyFile
    Spider
    //------------------------------------------------------
    // clock_t start = clock();
    int test = 1;
    //    read(test);
    // cin >> test;
    forn(i, 1, test)solve();
    //    while (cin >> n, n)solve();
    //    while (cin >> test)solve();
    // clock_t end = clock();
    // cerr << "time = " << double(end - start) / CLOCKS_PER_SEC << "s" << endl;
}

\[時間複雜度為:\ O((n+q)\sqrt{q}\log{\sqrt{q}}) \]

最後的一個普及

常見的二維數點實用方法總結

常見的模型:

\(x \le idx,l\le y \le r\) 的點的數量,其他模型都能透過可差性問題差分轉變為這個模型。可差性問題:

\(ans(限制條件,l\le x \le r)=ans(限制條件,x \le r)-ans(限制條件,x\le l-1)\)

二維數點不帶修,詢問離線,將 \(x\) 序列掃描線轉化 \(+\) 權值樹狀陣列統計 \(y\) 貢獻。

二維數點不帶修,詢問線上,主席樹維護 \([0,x]\) 上關於 \(y\) 權值樹。

二維數點帶修,詢問離線,將 \(time\) 作為第一序,\(x\) 作為第二序,cdq 分治算 \(y\) 的貢獻。

二維數點帶修,詢問線上,樹套樹,對 \(x\) 為第一維,\(y\) 為第二維。

如果要帶根號:

二維數點不帶修,詢問離線,序列掃描線轉化 \(+\) 值域分塊計數。

其他情況,序列分塊套值域分塊。當然也有 \(KD-Tree\)、多叉樹、\(bitset\)、歸併樹等等的一些其他解法。

PS:離線掃描線還有諸多應用,它對可差性問題的離線是出奇的好用,比如著名的莫隊二次離線演算法,就是藉助可差性問題的差分,將查詢再次掛載在掃描線上去進行二次離線地更優查詢。

相關文章