什么是莫队?
答:优雅的暴力!!!
重复的数
题目描述:给出一个长度为NNN的序列,有若干查询,每次查询区间[li,ri][l_i, r_i][li,ri]出现kik_iki次的数有多少个?
解题思路:最直观的做法就是拿到区间[l,r][l, r][l,r]后,暴力计算,用cnt[x]cnt[x]cnt[x]保存xxx出现了几个,pos[t]pos[t]pos[t]出现ttt次的数有几个。此时,时间复杂度最坏为:O(n2)\mathcal O(n^2)O(n2)
可以尝试来优化一下这个操作:使用两个指针LLL和RRR(初始化:L=1,R=0L = 1, R = 0L=1,R=0),对于每次查询,依次的把这两个指针移动到目标区间[l,r][l, r][l,r]的同时,更新cntcntcnt和pospospos数组。假设现在目标区间为:[1,5][1, 5][1,5],L=2,R=5L = 2, R = 5L=2,R=5;移动LLL指针左移L--
,并更新cntcntcnt和pospospos数组:
L --;
pos[cnt[a[L]]] --;
cnt[a[L]] ++;
pos[cnt[a[L]]] ++;
移完以后,同理移动RRR指针:
R ++;
pos[cnt[a[R]]] --;
cnt[a[R]] ++;
pos[cnt[a[R]]] ++;
可以发现上述都是把不在指针区间[L,R][L, R][L,R]的数加进来了,因此可以封装成一个函数:
void add(int x) { // 把一个数x加入当指针区间内pos[cnt[x]] --;cnt[x] ++;pos[cnt[x]] ++;
}
有增加就有删除,删除也是一样的:
void del(int x) { // 把一个数从指针区间删除pos[cnt[x]] ++;cnt[x] --;pos[cnt[x]] --;
}
为了减少[L,R][L, R][L,R]这两个指针的移动次数,可以先将序列分成n\sqrt nn块,并从1∼n1 \sim \sqrt n1∼n编号,对于所有的查询按照块编号,从小到大排序,对于同一块,按照区间右端点升序排序。排完序后,我们再按照上述的操作去移动L,RL, RL,R指针,这样的时间复杂度为:O(nn)\mathcal O(n\sqrt n)O(nn)
简单证明一下:
sort
一遍,O(nlogn)\mathcal O(n log n)O(nlogn)综上述:莫队的总时间复杂度为: O(nlogn)+O(nn)+O(nn)=O(nn)\mathcal O(nlogn) + \mathcal O (n \sqrt n) + \mathcal O(n \sqrt n) = \mathcal O(n \sqrt n)O(nlogn)+O(nn)+O(nn)=O(nn)
AC_Code:
#include
using namespace std;
constexpr int N = 1e5 + 10;
int n, len, m, a[N], cnt[N], pos[N];
int bel(int x) {return x / len;
}
struct query {int l, r, k, id;bool operator < (const query &b){return bel(l) == bel(b.l) ? r < b.r : bel(l) < bel(b.l);}
};void add(int x) {pos[cnt[x]] --; cnt[x] ++;pos[cnt[x]] ++;
}void del(int x) {pos[cnt[x]] --;cnt[x] --;pos[cnt[x]] ++;
}int main() {scanf("%d", &n); len = sqrt(n);for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);scanf("%d", &m);vector q(m);vector ans(m);for (int i = 0; i < m; i ++) {q[i].id = i;cin >> q[i].l >> q[i].r >> q[i].k;}sort(q.begin(), q.end());for (int i = 0, l = 1, r = 0; i < m; i ++) {int ql = q[i].l, qr = q[i].r;while (l > ql) add(a[-- l]);while (r < qr) add(a[++ r]);while (l < ql) del(a[l ++]);while (r > qr) del(a[r --]);ans[q[i].id] = pos[q[i].k];}for (int i = 0; i < m; i ++) cout << ans[i] << "\n";
}
P1903 [国家集训队] 数颜色 / 维护队列
题目描述:有长度为NNN的序列,现在有两个操作:
解题思路:查询操作可以用莫队轻松解决,可是这个修改要怎么办呢?我们可以加上一维时间,使之变为待修改莫队!具体操作如下:
增加一维时间t,在把区间移动到目标区间后,检查当前时间是不是目标时间,不是的话,跟修改L,RL, RL,R指针一样去修改时间t。
这里要注意的是,比如你现在把某个位置的数xxx改成了ccc,此时的时间为t1t_1t1,在把xxx修改为ccc后,这个时候你要把原来的数xxx记下来,如果以后询问小于t1t_1t1的时候,这个位置的数是xxx而不是ccc,这里有个简化操作,可以直接把xxx和ccc互换即可,下次操作直接更改就行(具体细节看代码
AC_Code:
#include
using namespace std;constexpr int N = 1e6 + 10;
int n, m, len, a[N], cnt[N];int bel(int x) {return x / len;
}struct query {int l, r, t, id;bool operator < (const query & b) {return (bel(l) ^ bel(b.l)) ? bel(l) < bel(b.l) : (bel(r) ^ bel(b.r)) ? bel(r) < bel(b.r) : t < b.t; }
};
struct M {int pos, color;
};M modify[N];
int now;
void add(int x) {if(!cnt[x]) ++ now;cnt[x] ++;
}
void del(int x) {cnt[x] --;if(!cnt[x]) -- now;
}
int main() {scanf("%d%d", &n, &m); //len = sqrt(n); 此时退化为了O(n^2)len = pow(n, 2.0 / 3);for (int i = 1; i <= n; i ++) scanf("%d", &a[i]);vector q; int cntc = 0;for (int i = 0; i < m; i ++) {char opt[2]; int l, r;scanf("%s%d%d", opt, &l, &r);if(opt[0] == 'Q') {q.push_back({l, r, cntc, (int)q.size()});} else {modify[++cntc] = {l, r};}}sort(q.begin(), q.end());vector ans(q.size());for (int i = 0, l = 1, r = 0, t = 0; i < (int)q.size(); i ++) {int ql = q[i].l, qr = q[i].r, qt = q[i].t;while (l > ql) add(a[-- l]);while (r < qr) add(a[++ r]);while (l < ql) del(a[l ++]);while (r > qr) del(a[r --]);while (t < qt) {++ t;if(ql <= modify[t].pos && qr >= modify[t].pos) {del(a[modify[t].pos]);add(modify[t].color);}swap(a[modify[t].pos], modify[t].color);}while (t > qt) {if(ql <= modify[t].pos && qr >= modify[t].pos) {del(a[modify[t].pos]);add(modify[t].color);}swap(a[modify[t].pos], modify[t].color);-- t;}ans[q[i].id] = now;}for (int &x : ans) cout << x << "\n";
}
COT2 - Count on a tree II
题目描述:给定NNN个节点的树,每个节点有一个颜色。现在有mmm次询问,每次询问给出两个节点u,vu,vu,v,输出u,vu,vu,v路径上的节点颜色个数。颜色为≤2×109\le 2 × 10^9≤2×109的非负整数
解题思路:
先得到树的欧拉序。
欧拉序特点:
每个节点出现两次,且他们中间的节点为这个节点子树上的点
欧拉序的求法:
在dfs的时候,得到一个点加入序列,退出的时候也加入序列,这样就得到了欧拉序
得到欧拉序有什么用呢,可以尝试将两个节点映射成欧拉序上的一段区间,这样问题就可以转换为莫队求区间不同数了。
看上图,1->3很容易得到(就是图中红线)而且之间出现两次的数刚好不会算在其中。但是4->3怎么算?用第一个4指定不行,只能用第二个4,但是我们发现少了1,但是1是什么呢,是4和3的lca!所以,可以得到下面两个结论!(保证first[x] AC_Code:#include
回滚莫队
// TODO 待补
下一篇:python入门——基础语法