参考资料1
万分感谢这个大佬,祝他报送清华北大!html
莫队由莫涛神仙首次提出,是一种区间操做算法。git
即使是板子题,难度也很高(差评)算法
因此,在阅读后文以前,请你先深呼吸,喝杯咖啡,吃点饼干,听听本身喜欢的歌数组
而后,中止呼吸,放下杯子,扔开饼干,摘下耳机,接受莫涛大神思想光辉的洗礼优化
先别谈莫队,咱们来回顾一下,遇到区间问题通常怎么解决?spa
很好,暴力线段树3d
也就是说,咱们一直在经过维护两个序列——左序列\([l,mid]\)与右序列\([mid + 1,r]\),从而来维护\([l, r]\),固然,这个操做会一直递归下去指针
然而,当题目这么问:code
令数组\(Q\)大小为\(n\)且每一个元素\(Q_i < n\),有\(m\)个询问,每次询问给定\(l,r\),请找出\([l,r]\)中至少重复出现\(k\)此的数字的个数htm
换句话说:
在\(Q_l\)到\(Q_r\)内找出现次数多余\(k\)的数字的个数
of course,你能够暴力,但你会暴零
那么咱们试着用线段树,首先,你须要维护左边的序列,而后你须要维护右边的序列,而后……
而后你会发现很难作到短期甚至\(O(1)\)的时间完成对线段树单一节点的维护,由于你老是要层层递进向上叠加。
淦!这不是欺负人吗
咱们先试试暴力吧,用个\(count\)记录一下出现次数,而后在扫一遍
暴力是万能的,答案固然正确,可是你的时间复杂度哭了——\(O(n^2)\)
那么咱们能够看看是否能够改进一下,用上\(t(wo)p(oints)\)算法:
假设有两个指针,\(l\)和\(r\),每次询问的时候用移动\(l\)和\(r\)的方式来尝试和要求区间重合
是否是有点蒙?我举个栗子
此图中,两个Q是待求的区间
初始化\(r = 0,l = 1\)
此时,发现\(l\)和要求的区间左端重合了,而\(r\)没有,那么咱们把\(r\)往右边移动一位
此时,\(r\)发现了一个新的值\(0\),总数记录一下,继续右移动
\(r\)又发现了一个新数值\(2\),总数记录一下,继续右移动
此处\(2\)被记录过了,总数值不变
一直到\(r\)与右端点重合,获得下图:
第一个区间就算处理完了,咱们来看下一个
首先,\(l\)不在左端点,咱们把它右移
这一次,\(l\)所遇到的数值在区间\([l, r]\)只可以存在,总数不变
下一次也是如此,一直到
你会发现,这时,区间\([l,r]\)将(也就是在下一次移动后)不会有\(2\)存在了,那么总数就一个\(-1\),而正好本题须要统计的就是区间内数值的个数,总数改变:
如此循环往复,获得最终答案,因此咱们能够得出这个代码
int arr[maxn], cnt[maxn] // 每一个位置的数值、每一个数值的计数器 int l = 1, r = 0, now = 0; // 左指针、右指针、当前统计结果(总数) void add(int pos) { // 添加一个数 if(!cnt[arr[pos]]) ++ now; // 在区间中新出现,总数要+1 ++ cnt[arr[pos]]; } void del(int pos) { // 删除一个数 -- cnt[arr[pos]]; if(!cnt[arr[pos]]) -- now; // 在区间中再也不出现,总数要-1 } void work() { for(int i = 1; i <= q; i ++) { int ql, qr; scanf("%d%d", &ql, &qr); while(l < ql) del(l++); // 左指针在查询区间左方,左指针向右移直到与查询区间左端点重合 while(l > ql) add(--l); // 左指针在查询区间左端点右方,左指针左移 while(r < qr) add(++r); // 右指针在查询区间右端点左方,右指针右移 while(r > qr) del(r--); // 不然左移 printf("%d\n", now); // 输出统计结果 } }
嗯,干得漂亮,可是这是莫队吗?不是
若是区间特别多,\(l,r\)反复横跳,结果皮断了腿,时间复杂度\(O(nm)\)
那么如今的问题已经变成了:如何尽可能减小\(l,r\)移动的次数?
首先,看到尽可能减小\(l,r\)移动的次数,咱们会想到排个序
排序排什么的顺序呢?是排端点吗?显然不是,哪怕左端点有序,右端点就会杂乱无章;右端点有序,左端点就会杂乱无章……
这里,咱们运用一下分块的思想,把序列分为\(\sqrt{n}\)块,把查询区间按照左端点所在块的序号排个序,若是左端点所在块相同,再按右端点排序。
这个算法须要的时间复杂度为\(sort+move_{\texttt{左指针}}\)
因为\(sort\)的时间复杂度为\(O(n\log n)\),\(move_{\texttt{作指针}}\)的时间复杂度为\(O(n\sqrt{n})\),那么总的时间复杂度为\(O(n\sqrt{n})\)
好耶!降了一个根号!鼓掌!
其次,咱们须要考虑一下更新的策略
通常来讲,咱们只要找到指针移动一位之后,统计数据与当前数据的差值,找出规律(能够用数学方法或打表),而后每次移动时用这个规律更新就行
最后给出总代码:
#include <cstdio> #include <cstring> #include <cmath> #include <algorithm> using namespace std; #define maxn 1010000 #define maxb 1010 int aa[maxn], cnt[maxn], belong[maxn]; int n, m, size, bnum, now, ans[maxn]; struct query { int l, r, id; } q[maxn]; int cmp(query a, query b) { return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r); } #define isdigit(x) ((x) >= '0' && (x) <= '9') int read() { int res = 0; char c = getchar(); while(!isdigit(c)) c = getchar(); while(isdigit(c)) res = (res << 1) + (res << 3) + c - 48, c = getchar(); return res; } void printi(int x) { if(x / 10) printi(x / 10); putchar(x % 10 + '0'); } int main() { scanf("%d", &n); size = sqrt(n); bnum = ceil((double)n / size); for(int i = 1; i <= bnum; ++i) for(int j = (i - 1) * size + 1; j <= i * size; ++j) { belong[j] = i; } for(int i = 1; i <= n; ++i) aa[i] = read(); m = read(); for(int i = 1; i <= m; ++i) { q[i].l = read(), q[i].r = read(); q[i].id = i; } sort(q + 1, q + m + 1, cmp); int l = 1, r = 0; for(int i = 1; i <= m; ++i) { int ql = q[i].l, qr = q[i].r; while(l < ql) now -= !--cnt[aa[l++]]; while(l > ql) now += !cnt[aa[--l]]++; while(r < qr) now += !cnt[aa[++r]]++; while(r > qr) now -= !--cnt[aa[r--]]; ans[q[i].id] = now; } for(int i = 1; i <= m; ++i) printi(ans[i]),putchar('\n'); return 0; }
卡常数做为OIer的屡见不鲜,相信你们必定不陌生了
卡常数包括:
而莫队的神奇之处在于他的独特优化:奇偶性排序
原代码:
int cmp(query a, query b) { return belong[a.l] == belong[b.l] ? a.r < b.r : belong[a.l] < belong[b.l]; }
改成
int cmp(query a, query b) { return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r); }
别人说跑的很快我还不信,本身跑了一下才知道……
真的跑的很快啊……
我知道,你拿着上面别个大佬写的代码(再次膜拜写这个代码的大佬orz)兴冲冲的去刷题,一路上披荆斩棘,直到你看到了Luogu1903——国家集训队-数颜色,你完全傻了眼
妈耶,他要是这么一修改我岂不是要从新sort?跑了跑了
因为莫队自己就是离线的,而你须要修改,得想个办法让他在线,具体作法是:“就是再弄一指针,在修改操做上跳来跳去,若是当前修改多了就改回来,改少了就改过去,直到次数恰当为止。” (再次感谢这个大佬,,好喜欢这个解释)