莫队

各种莫队

莫队是啥?是暴力 是根号平衡思想在区间信息问题求解上的集中体现。由莫涛总结提出。
莫队算法的实质是 \(k\) 维空间内 \(n\) 点间的路径规划方式,其路径长度被证明达到了期望下界,是 \(O(n^{\frac {2k - 1} k})\) 的。一种规划方案定义为由原点以单次 \(O(1)\) 的花费向某一维度的正/负方向移动单位长度的方式遍历所有整点的移动方法。最优方案即为总花费渐进最小的方案。

以二维空间下莫队算法为例。
对于二维空间内 \(n\) 个整点 \((x_i, y_i)\),记坐标值域为 \(L\)。我们将证明存在一类路径的构造方法,使得自原点可以依次以期望下最优方案遍历所有整点。

具体地,我们首先将值域分为 \(S\) 块,随后按 \(x\) 坐标所在块为第一关键字、\(y\) 坐标为第二关键字排序,随后按照以此方式构造的序列遍历。

分析复杂度。
\(x\) 坐标位于相同块时,\(y\) 方向的位移为 \(O(L)\)\(x\) 坐标在块间转移时,\(y\) 方向的位移为 \(O(L)\)。由于相同块共 \(\frac LS\) 个,因此 \(y\) 方向的位移总共为 \(O(L \times \frac L S)\)
序列中相邻两个整点的 \(x\) 坐标位于相同块时,\(x\) 方向的位移最劣情况是 \(S\) 的。不同时,位移最劣是 \(2S\) 的。由于序列共 \(n\) 个元素,因此 \(x\) 方向的位移总共为 \(O(n S)\)
因此总路径长度为 \(O(nS + \frac {L^2}S)\)
可以取 \(S = \sqrt L\),得到复杂度 \(O(n \sqrt L + L \sqrt L)\)。也可以取 \(S = \frac L {\sqrt n}\),得到复杂度 \(O(L \sqrt n)\)

可以进行外推,当维护 \(k\) 维信息时对前 \(k-1\) 维分大小为 \(S\) 的块,共 \((\frac nS)^{k-1}\) 块。两块间转移的复杂度为 \(n\) ,因此块间位移为 \(\frac {n^k} {S^{k-1}}\)。块内部转移总复杂度是 \(nS\),因此总时间复杂度在 \(S = n^{\frac{k-1}k}\) 时最优,为 \(O(n^{\frac{2k-1}{k}})\)。 - By EntropyIncreaser.

普通莫队

二维整点路径规划问题。

(不)形式化地,给定一个 \(n\) 长度的序列,我们有 \(m\) 个区间,需要查询这些区间内的元素种类。
将区间抽象为二维整点,我们需要的就是在遍历过程中动态维护所有元素的计数器,以及当前答案。当计数器 \(0\rightarrow 1\) 时答案自增;当计数器 \(1\rightarrow 0\) 时答案自减。遍历到整点时将答案记录至整点上即可。
例题:SP3267 D-query on luogu

讨论一下坐标移动问题。由于我们实际上维护的是区间信息,因此需要保证“一个信息被删除前是存在的”。投影到坐标系内,我们需要使得 \(x \le y\)。也即,坐标需要在 \(y = x\) 直线的左上方移动。为此,我们可以先向左上角移动坐标,随后向右下角移动。这样可以避免 \(x > y\) 的情况发生,不会删除不存在的元素。

转换到实现上,我们可以维护 \(l,r\) 指针表示当前覆盖的区间左右端点,随后按 l--, r++, r--, l++ 的顺序尝试移动端点到下一个区间范围。

带修莫队

三维整点路径规划问题。

(不)形式化地,给定一个 \(n\) 长度的序列,我们有 \(m\) 个区间,需要查询这些区间内的元素种类。但是这些查询都是按时间顺序发生的,在两个时间片间可能会发生修改。
将区间抽象成三维整点,我们需要的就是在以如上方式遍历的过程中维护时间维信息。我们可以首先维护前两维信息,在移动到 \(x,y\) 坐标与目标点相同位置后,单独处理时间维修改。普通莫队的自然推广,平凡。
例题:P1903 [国家集训队] 数颜色 / 维护队列

具体实现上,我们可以将修改和查询分两个数组记录,在读入过程中将时间维处理掉。当读入一次询问时,其 \(z\) 坐标大小就是已读入修改数量。

树上莫队

使用欧拉序将树拍成序列。

(不)形式化地,给定一棵 \(n\) 个节点的树,我们有 \(m\) 个点对,需要查询两点间路径上的元素种类。
具体地,我们将树上每个节点的欧拉序记作 \((\text{in}_ i, \text{out}_ i)\),分别代表遍历到这个点到离开这个点时的时间戳。并按时间顺序将节点编号排成长度为 \(2n\) 的序列。
讨论点对 \((x,y)\) 的转化。不妨设 \(\text{in}_ x < \text{in}_ y\)

  1. \(LCA(x, y) = x\)。此时序列中位于 \([\text{in}_ x , \text{in}_ y]\) 区间内仅出现过一次的节点即为路径上节点。
    首先路径上节点都会出现在该区间内,充分性得证。任意出现过两次的节点都是在这条链外的子树,出现过一次的节点都是遍历该段时向下而未向上的节点,即在所求路径上的点,充分性得证。
    因此此类点对可被转化为序列上 \([\text{in}_ x , \text{in}_ y]\) 区间的求解。
  2. \(LCA(x, y) \neq x\)。此时序列中位于 \([\text{out}_ x , \text{in}_ y]\) 区间内仅出现过一次的节点即为路径上节点。
    类似1.证明。
    因此此类点对可被转化为序列上 \([\text{out}_ x , \text{in}_ y]\) 区间的求解。

因此树上问题被转化为序列问题。注意在本段内可能存在出现两次的点,这些点的贡献不应出现在答案中,可以考虑记录每个编号的出现次数奇偶性,当出现偶次时不处理,实现时 xor 解决。其余同上两种莫队求解方式。

回滚莫队

二次离线莫队

模板题代码为例。

#include <cmath>
#include <tuple>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
#define MXIM 100010 
#define ll long long
#define rep(i,a,b) for ( register int (i) = (a); (i) ^ ((b)+1); ++(i) )
#define pre(i,a,b) for ( register int (i) = (a); (i) ^ ((b)-1); --(i) )
int n, m, k, len;
int a[MXIM], cntb[MXIM], pref[MXIM];
ll ans[MXIM]; 

#define st(block_id) ( n / len * ((block_id)-1) + 1)
#define ed(block_id) ( block_id == len ? n : ( n / len * (block_id) ) )
#define bl(value_pos) ( value_pos == n ? len : ((value_pos) - 1) / len + 1 )
#define sz(block_id) ( ed( (block_id) ) - st( (block_id) ) + 1 )

struct query {
    int l, r, id;
    ll ans;
    inline bool operator < (const query & b) {
        return bl(l) == bl(b.l) ? r < b.r : l < b.l;
    }
}q[MXIM];

vector<tuple<int, int, int>> vec[MXIM];

int main () {

    /* 设点 x 对区间 [l,r] 的贡献是 f(x, [l,r]) 
     * 莫队嘛,肯定得要讨论端点变化带来的影响 
     * 那么我们看往左边加点的影响 
     * 即快速地求得 f(x, [l, x-1])
     * 于是我们差分一下 
     *  f(x, [l, x-1]) = f(x, [1, x-1]) - f(x, [1, l-1])
     * 于是就成要快速地求得 x 对一个前缀的贡献 
     * 于是我们发现有两种前缀 
     * 1. f(x, [1, x-1])
          一个前缀和它后面一个点对它的贡献
          一共 n 个,预处理即可
       2. f(x, [1, l-1])
          其实我们要求的东西并不是简单的一个 f(x, [l, x-1])
          而是 x 对 [l, r] -> [l, r+k] 的贡献
          具体点说就是 
          任意 x \in [r+1, r+k], f(x, [l, x-1]) 
          换到2.前缀上来说,就是  x \in [r+1, r+k], f(x, [1, l]) 
          我们会发现一个性质,就是,x点变了,但它所贡献的区间不变 
          于是打一下x移动左右端点的标记,再莫一遍[1,l]所得到的贡献,于是二离计算 
     */

    cin >> n >> m >> k;
    if (k > 14) {
        rep(i,1,m) cout << 0 << endl;
        return 0;
    } len = sqrt(n);
    rep(i,1,n) cin >> a[i];
    rep(i,1,m) cin >> q[i].l >> q[i].r, q[i].id = i;
    sort(q+1, q+1+m);
    vector<int> buk;
    rep(i,0,16383) if (__builtin_popcount(i) == k) buk.emplace_back(i);
    rep(i,1,n) {
        for (auto x : buk) ++cntb[a[i] ^ x];    // 看前缀里和 i 异或得 k 的数的数量 
        pref[i] = cntb[a[i+1]];                 // 讨论 x 对 [1, x-1] 的贡献 
    } memset(cntb, 0, sizeof cntb);
    for (register int i = 1, L = 1, R = 0, l, r; i <= m; ++i) {
        // 我莫队一下
        // 1.前缀就出来了,我再给2.前缀打好标记 
        l = q[i].l, r = q[i].r;
        if (L < l) {
            vec[R].emplace_back(L, l-1, -i);
            while (L < l) q[i].ans += pref[(L++) - 1];
        } 
        if (L > l) {
            vec[R].emplace_back(l, L-1, i);
            while (L > l) q[i].ans -= pref[(L--) - 2];
        } 
        if (R < r) {
            vec[L-1].emplace_back(R+1, r, -i);
            while (R < r) q[i].ans += pref[(R++)];
        }
        if (R > r) {
            vec[L-1].emplace_back(r+1, R, i);
            while (R > r) q[i].ans -= pref[(R--) - 1];
        }
    } 
    for(register int i = 1, id, l, r; i <= n; ++i) {
        /* 计算2.前缀 
         * 我们既然有贡献区间不变,那就直接把 x 的贡献搞出来
         * 给对应前缀区间加上就行 
         */
        for (auto x : buk) ++cntb[a[i] ^ x];
        for (auto x : vec[i]) {
            tie(l, r, id) = x; 
            // tie(arg ...) = [tuple] 使得前面tie里提供的变量被依次赋值为这个 tuple 里面的元素
            for (register int j = l, tmp = 0; j <= r; ++j) {
                tmp = cntb[a[j]];
                // 这是 x = j 时的贡献 
                if (j <= i and k == 0) --tmp;
                if (id < 0) q[-id].ans -= tmp;
                // id 存着 得到贡献的前缀区间 
                else q[id].ans += tmp;
            }
        }
    }  
    rep(i,1,m) q[i].ans += q[i-1].ans;
    // 既然是前缀区间 那就前缀和 
    rep(i,1,m) ans[q[i].id] = q[i].ans;
    rep(i,1,m) cout << ans[i] << endl;
    return 0;
} 

待补

posted @ 2022-08-24 20:38  joke3579  阅读(160)  评论(1编辑  收藏  举报