首页 > 编程语言 >【离线算法】- 莫队

【离线算法】- 莫队

时间:2023-10-13 10:36:43浏览次数:33  
标签:int 离线 ++ 算法 端点 区间 -- 莫队

莫队

简介

莫队是可以支持多次询问区间 \([l,r]\) 的信息的 离线 算法。通过将询问范围以块长为 \(\sqrt n\) 分块后按端点所属分块排序的方式优化复杂度。

普通莫队

定义

普通莫队针对的是序列上的区间询问。常见形式为:对于一个长度为 \(n\) 的序列,提出 \(m\) 次询问,每次询问区间 \([l,r]\) 中数值的个数。总之,我们可以简单求得从区间 \([l,r]\) 转移到相邻区间 \([l,r-1]、[l+1,r]、[l,r+1]、[l-1,r]\) 后的答案即可使用莫队。

过程

自然,我们定义两个指针 \(l、r\) ,通过移动双指针 \(l、r\),使之与询问区间端点 \(L、R\) 重合,并在移动过程中更改答案即可。

image

而为了处理众多询问,就有了对这样的暴力的优化:普通莫队——通过将询问值域分块后以区间左端点所在的块的序号为第一关键字,以右端点为第二关键字排序后再依次处理询问即可得到 \(O(n\sqrt n)\) 的复杂度。

这里对于右端点的比较有一处优化——奇偶化排序:当左端点属与奇数块时从小到大排序,否则从大到小排序。感性理解,右指针先从左往右移动后,左指针跳到下一分块时就又得移回区间左端点在这一分块下时最小的右端点。与其先移回最小处再往前推至其他区间,不如在回退时就将这些区间先行处理掉。而左端点最多只会在块内移动,保证了复杂度。

struct Q {
	int l, r, id;
} Q[M];

bool cmp(Q a, Q b) {
	return (bel[a.l] != bel[b.l]) ? a.l < b.l :
    (a.r == b.r ? 0 : ((bel[a.l] & 1) ? a.r < b.r : a.r > b.r));
}

但是要注意指针移动顺序:先扩大区间再缩小区以避免指针非法。并且扩大区间时应先移动再处理,缩小则反之。针对某一位的处理:对于数字种类问题即判断加入前/删除后此种类是否「灭绝」来决定总数增减。


sort(Q+1, Q+m+1, cmp);
int l = 1, r = 0;
for (int i = 1; i <= m; i ++) {
    while (l > Q[i].l) sum += !cnt[a[-- l]] ++; //ins(-- l);
    while (r < Q[i].r) sum += !cnt[a[++ r]] ++; //ins(++ r);
    while (l < Q[i].l) sum -= !-- cnt[a[l ++]]; //del(l ++);
    while (r > Q[i].r) sum -= !-- cnt[a[r --]]; //del(r --);
    ans[Q[i].id] = sum;
}

带修莫队

定义

带修莫队针对的是序列上有修改的区间询问。常见形式为:对于一个长度为 \(n\) 的序列,\(m\) 次询问区间 \([l,r]\) 中数值的个数并间或有 \(t\) 次对某一位的修改操作。总之,我们可以通过加入时间轴求得从区间 \([l,r,t]\) 转移到相邻区间 \([l,r-1,t]、[l+1,r,t]、[l,r+1,t]、[l-1,r,t]、[l,r,t-1]、[l,r,t+1]\) 来维护信息即可使用带修莫队。

过程

直接加入时间轴的一维,并维护每次修改对序列的影响。比普通莫队排序多了一个关键字,区间转移需要保证三个指针都完美重合。据说此时的块长为 \(n^{2/3}\) 时更优。

struct Q{
	int l, r, t, id;
} Q[N];
struct R{
	int pos, col;
} R[N];

bool cmp(Q a, Q b) {
	return (bel[a.l] == bel[b.l]) ?	((bel[a.r] == bel[b.r]) ?
            a.t < b.t :	a.r < b.r) : a.l < b.l;
}

时间推移时需要暴力更改时间点至询问时间,实时改变区间覆盖到的修改处,并记录序列位置的原值。可以用 \(swap\) 兼顾赋值与记忆历史值的功能。

sort(Q+1, Q+cntq+1, cmp);
int l = 1, r = 0, t = 0;
for (int i = 1; i <= cntq; i ++) {
    while (l > Q[i].l) sum += !cnt[a[-- l]] ++;
    while (r < Q[i].r) sum += !cnt[a[++ r]] ++;
    while (l < Q[i].l) sum -= !-- cnt[a[l ++]];
    while (r > Q[i].r) sum -= !-- cnt[a[r --]];
    while (time < Q[i].t) {
        ++ time;
        if (Q[i].l <= R[time].pos && R[time].pos <= Q[i].r)
            sum -= !-- cnt[a[R[time].pos]] - !cnt[R[time].col] ++;
        swap(a[R[time].pos], R[time].col);
    }
    while (time > Q[i].time) {
        if (q[i].l <= R[time].pos && R[time].pos <= q[i].r)
            sum -= !-- cnt[a[R[time].pos]] - !cnt[R[time].col] ++;
        swap(a[R[time].pos], R[time].col);
        time --;
    }
    ans[Q[i].id] = sum;
}

树上莫队

定义

树上莫队针对的是序列上的区间询问……诶?序列?。莫队是针对序列的,对象变成树之后我们就得再把树「拍扁」成序列。

树上莫队针对的是树上的路径询问。常见形式为:给定一棵 \(n\) 个节点的树,每个节点有权值,\(m\) 次询问路径 \(u\to v\) 上权值的种数。总之,可以把树压为序列进行操作来维护信息的即可使用树上莫队。

过程

首先,一般树都能遍历得到DFS序、欧拉序和 括号序 。而对于路径操作,DFS序是不可做的,欧拉序同一节点会出现多次,不好操作,但括号序就有良好性质支持在序列上找出路径。因为括号序是 \(dfs\) 过程中在 进点和出点 时才将当前节点加入序列(for循环前后将E[++tot]=u,长度为\(2n\))得到的,而非欧拉序的 访问和回溯for循环内外E[++tot]=u,长度为\(2n-1\))。

所以括号序性质包括但不限于:

  1. 一个点会且只会出现两次,而该点的两次出现位置之间出现的数也都出现两次,且都属于该点子树。下文记节点 \(x\) 在括号序中第一次出现的位置为 \(fr(x)\),第二次出现的位置为 \(la(x)\)。出现节点 \(u\) 和 \(v\) 时默认使 \(u\) 在序列的位置靠前,
  2. 对于两个点,它们首次出现的位置之间的序列中出现奇数次的数都在它们的路径上。(路径上的点只会出现一次嘛,出现两次的就都是兄弟、祖先兄弟及其子树等节点,不会在路径上)下文“询问区间”意即统计该区间出现了一次的节点贡献。
  3. 对于“|”类型的路径,考虑两个点 \(u\) 和 \(v\)。此时路径 \(u\to v\) 构成一条链,\(lca(u,v)=u\)。那么询问区间 \([fr(u),fr(v)]\),因为此时 \(v\) 在 \(u\) 子树内,从 \(u\) 往下搜索子树时自然统计到第一次找到 \(v\) 时的序列即可。
  4. 对于“︿”类型的路径,考虑两个点 \(u\) 和 \(v\)。此时路径 \(u\to v\) 可拆分为 \(u\to lca、lca\to v\)。那么询问区间 \([la(u),fr(v)]\),再加入\(lca\) 的贡献。因为 \(v\) 在 \(u\) 子树外,自然从 \(u\) 往上返回的过程才有贡献,而 \([fr(u),la(u)]\) 的区间是从 \(u\) 往下搜索时子树产生的序列,所以可以忽略,节省时间。而 \(lca\) 会将 \([fr(u),la(v)]\) 整块区间括起来,所以需要通过倍增/树剖单独求出再加入贡献。

image

void dfs(int u, int f) {
	fa[u][0] = f; dep[u] = dep[f] + 1;
	ord[++ tim] = u; fr[u] = tim;
	for (int i = h[u]; i; i = ne[i]) {
		int v = e[i];
		if (v == f) continue;
		dfs(v, u);
	}
	ord[++ tim] = u; la[u] = tim;
}
void pre() {
	for (int i = 1; i <= 25; i ++)
		for (int j = 1; j <= n; j ++)
			fa[j][i] = fa[fa[j][i-1]][i-1];
}
int getlca(int x, int y) { //倍增求lca
    if (dep[x] < dep[y]) swap(x, y);
    for (int i = 25; i >= 0; i --)
    	if (dep[fa[x][i]] >= dep[y])
        	x = fa[x][i];
    if (x == y) return x;
    for (int i = 25; i >= 0; i --)
        if (fa[x][i] != fa[y][i]) {
            x = fa[x][i]; y = fa[y][i];
        }
    return fa[x][0];
}

像普通莫队一样,先依照括号序下标分块后按左右端点排序。询问区间时有个小trick,使用一个used[]数组通过简单异或运算达到去重目的。

struct Q{
	int l, r, lca, id;
} Q[N];
bool cmp(Q x, Q y) {
	return (bel[x.l] == bel[y.l]) ? ((bel[x.l] & 1) ?
	x.r < y.r : x.r > y.r) : bel[x.l] < bel[y.l];
}
void work(int x) {
	!used[x] ? sum += !cnt[a[x]] ++ : sum -= !-- cnt[a[x]];
	used[x] ^= 1;
}

最后提醒一点,查询时记得将输入的节点转为括号序中的下标,在统计贡献时再使用节点编号,不然树上莫队做了个寂寞……

for (int i = 1, u, v, lca; i <= m; i ++) {
    scanf("%d%d", &u, &v);
    if (fr[u] > fr[v]) swap(u, v); lca = getlca(u, v);
    if (lca == u) Q[i].lca = 0, Q[i].l = fr[u];
    else Q[i].lca = lca, Q[i].l = la[u];
    Q[i].r = fr[v]; Q[i].id = i;
}
sort(Q+1, Q+m+1, cmp);
int l = 1, r = 0;
for (int i = 1; i <= m; i ++) {
    int L = Q[i].l, R = Q[i].r, LCA = Q[i].lca;
    while (l > L) work(ord[-- l]);
    while (r < R) work(ord[++ r]);
    while (l < L) work(ord[l ++]);
    while (r > R) work(ord[r --]);
    if (LCA) work(LCA);
    ans[Q[i].id] = sum;
    if (LCA) work(LCA);
}

回滚莫队

定义

普通莫队针对的是序列上的区间询问。常见形式为:对于一个长度为 \(n\) 的序列,提出 \(m\) 次询问,每次询问区间 \([l,r]\) 中相同数值的最大距离。总之,我们无法简单求得从区间 \([l,r]\) 转移到相邻区间 \([l,r-1]、[l+1,r]\) 后的答案即可考虑回滚莫队。ヽ(・_・;)ノ

像最大值这种东西的更新,加入易删除难啊!但如果状态(譬如这里只需维护数值首次/最后出现位置)容易改变的话,回滚莫队(在这里是不删除莫队)便应运而生。

过程

不再使用奇偶化排序,一贯按右端点从小到大排序。这样一来我们发现左端点在同块中的区间,它们右端点单调递增。

bool cmp(Q x, Q y) {
	return bel[x.l] == bel[y.l] ? 
	x.r < y.r : x.l < y.l;
}

于是得益于块长的限制,我们可以这样按块处理:对于每一分块,枚举所有左端点隶属于它的区间。先特判掉左右端点在同块的情况暴力做,对于之后的区间再顺次将 \(r\) 指针右移。而对于左端点,左移后转移到下一区间肯定有需要右移的情况。那么既然无法在删除时快速更新答案,我们就直接记录左端点在该块右界尚未移动时的答案,每次进入下一询问时将状态回退到只移动了右端点的状态即可。这里有个小trick:开一个stack记录计算此分块贡献时更改的数值,在枚举完区间后通过记录的stack删除状态,避免了滥用memset

sort(Q+1, Q+m+1, cmp);
for (int i = 1, j = 1; i <= (n-1)/len+1 && j <= m; i ++) {
    int r = min(i*len, n), l = r+1; res = 0;
    for (; bel[Q[j].l] <= i && j <= m; j ++) {
        int L = Q[j].l, R = Q[j].r;
        if (bel[L] == bel[R]) {
            for (int k = L; k <= R; k ++) temp[a[k]] = 0; tmp = 0;
            for (int k = L; k <= R; k ++) {
                if (!temp[a[k]]) temp[a[k]] = k;
                else tmp = max(tmp, k - temp[a[k]]);
            }
            res[q[j].id] = tmp; continue;
        }
        while (r < R) {
            ++ r;
            mx[a[r]] = r;
            if (!mn[a[r]]) mn[a[r]] = r, la.push(a[r]);
            res = max(res, r - mn[a[r]]);
        }
        int last = res;
        while (l > L) {
            -- l;
            if (mx[a[l]]) res = max(res, mx[a[l]] - l);
            else mx[a[l]] = l;
        }
        ans[Q[j].id] = res;
        while (l < min(i*len, n) + 1) {
            if (mx[a[l]] == l) mx[a[l]] = 0;
            l ++;
        }
        ans = last;
    }
    while (!la.empty()) {
        int pos = la.top(); la.pop();
        mx[pos] = mn[pos] = 0;
    }
}

二维莫队

挖坑

莫队二离

挖坑

标签:int,离线,++,算法,端点,区间,--,莫队
From: https://www.cnblogs.com/Arson1st/p/17761476.html

相关文章

  • 10.13算法
    你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算你不触动警报装置的情况下,一夜之内能......
  • 算法学习笔记(30):Kruskal 重构树
    Kruskal重构树这是一种用于处理与最大/最小边权相关的一个数据结构。其与kruskal做最小生成树的过程是类似的,我们考虑其过程:按边权排序,利用并查集维护连通性,进行合并。如果我们在合并时,新建一个节点,其权值为当前处理的边的权值,并将合并的两个节点都连向新建的节点,那么就......
  • 算法训练day30 LeetCode93.78.90
    算法训练day30LeetCode93.78.9093.复原IP地址题目93.复原IP地址-力扣(LeetCode)题解代码随想录(programmercarl.com)使用'.'切割字符串、结束条件为字符串中有三个'.'、同时要确定字符串符合的条件长度为不为1时,首字符不能是0数值大小在[0,255]单个字符在'0'......
  • 树上莫队
    20231012树上莫队由于联考考到,又直接爆0,于是来学习。树上莫队——把莫队放到树上。但我是真的不知道把莫队怎么放到树上。。。于是我们考虑一个东西叫做欧拉序,就是再dfs的时候在进栈和出栈的地方都记录一下。而在区间查询的时候,我们只对区间出现一次的数统计答案,用一个......
  • 树上的最大权连通块:一种换根动态规划与贪心算法的结合
    树上的最大权连通块:一种换根动态规划与贪心算法的结合在计算机科学中,树是一种非常特殊的数据结构,不仅因为它们在存储数据时的效率,还因为它们提供了一种非常直观且强大的方式来解决各种问题。今天,我们将探讨一种特殊类型的问题,即在一棵树中找到一个特殊的子集或连通块,该子集中的节......
  • 串的模式匹配-KMP算法
    一个古老的模式匹配算法。优点在于不需要回溯主串指针。在整个匹配过程中,只需要从头到尾扫描主串一次,方便处理那种大文件。具体实现方法是对子串进行预处理,求得next数组。这个数组记录的信息是:如果子串的当前比较位与主串不匹配,那么接下来应该把子串的哪个位与主串的当前位(因......
  • 10.12算法
    最大子序和给你一个整数数组nums,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组是数组中的一个连续部分。 示例1:输入:nums=[-2,1,-3,4,-1,2,1,-5,4]输出:6解释:连续子数组 [4,-1,2,1]的和最大,为 6。示例2:输入:nums=[1]输出:1示例3......
  • 二次离线莫队笔记
    前言莫队可以解决许多其他数据结构无法完成的问题,正在很多其他问题上也可以拿部分分甚至满分,只因其复杂度为小常数\(O(n\sqrtn\timesk)\)其中\(k\)是单次扩张以及收缩的复杂度,而二离莫队可以在答案可差分的情况下达到\(O(n\sqrtn+n\timesk)\)起源lxl把二次离线莫......
  • 动物识别系统python+Django网页界面+TensorFlow算法模型+数据集训练
    一、简介动物识别系统。基于Python+TensorFlow+Django网页框架+ResNet50算法模型实现实现步骤如下:收集多种动物的图片数据集,并整理归类然后使用TensorFlow搭建ResNet50算法模型网络对数据集进行多次迭代训练最后得到一个精度较高的H5模型文件基于训练好的模型,使用Django开......
  • java算法之排序算法大全
    ①排序所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。排序算法,就是如何使得记录按照要求排列的方法。排序算法在很多领域得到相当地重视,尤其是在大量数据的处理方面。一个优秀的算法可以节省大量的资源。在各个领域中考虑到数据的各种限制......