[学习笔记]莫队->树状数组

版权声明:Fashion Education https://blog.csdn.net/ModestCoder_/article/details/88876358

ps:本博纯属博主自己瞎搞

  • 例:给定长为n的一列数,以及q个询问(l,r),要求输出al…ar中有多少个不同的数
  • 不要求在线
  • 洛谷原题
  • 离线算法,显然莫队,然而,本题卡莫队

先上一发莫队的TLE代码:

#include <bits/stdc++.h>
#define maxn 500010
using namespace std;
struct node{
	int a, b, id;
}a[maxn];
struct ques{
	int l, r, id, pos, ans;
}q[maxn];
int ans, n, m, block, cnt[maxn], c;

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

bool cmp1(node x, node y){ return x.a < y.a; }
bool cmp2(node x, node y){ return x.id < y.id; }
bool cmp3(ques x, ques y){ return x.pos == y.pos ? x.r < y.r : x.pos < y.pos; }
bool cmp4(ques x, ques y){ return x.id < y.id; }
void add(int x){ ans += ++cnt[a[x].b] == 2 ? 1 : 0; }
void del(int x){ ans -= --cnt[a[x].b] == 1 ? 1 : 0; }

void solve(){
	int l = 1, r = 1; add(1);
	for (int i = 1; i <= m; ++i){
		for (; r < q[i].r; add(++r));
		for (; r > q[i].r; del(r--));
		for (; l < q[i].l; del(l++));
		for (; l > q[i].l; add(--l));
		q[i].ans = ans;
	}
}

int main(){
	n = read(), c = read(), m = read();
	for (int i = 1; i <= n; ++i) a[i].a = read(), a[i].id = i;
	sort(a + 1, a + 1 + n, cmp1);
	for (int i = 1; i <= n; ++i) a[i].b = a[i].a == a[i - 1].a ? a[i - 1].b : a[i - 1].b + 1;
	sort(a + 1, a + 1 + n, cmp2);
	block = sqrt(n);
	for (int i = 1; i <= m; ++i){
		q[i].l = read(), q[i].r = read();
		q[i].id = i, q[i].pos = q[i].l / block;
	}
	sort(q + 1, q + 1 + m, cmp3);
	solve();
	sort(q + 1, q + 1 + m, cmp4);
	for (int i = 1; i <= m; ++i) printf("%d\n", q[i].ans);
	return 0;
}
  • n<=5e5, m<=5e5,莫队根号的时间复杂度也大了
  • 想想把根号级别的复杂度优化成log级别, 上数据结构
  • 本博用树状数组(博主对树状数组情有独钟)
  • 假设有一段数:1 3 2 3 6, 这里有两个3,分别是第2个位置和第4个位置,但是若询问区间包含这两个,这两个只对答案贡献1
  • 什么意思?没关系,等会细细道来。先讲做法,再证明正确性
  • 因为可以离线考虑将询问按照l升序排序,维护一定的单调性
  • 预处理出一个数组nxt[i], 表示数值上与第i个数相同的数下一个出现的位置。比如:1 3 2 3 1, 那么nxt数组就为5, 4, 0, 0, 0;又比如:1 2 2 2 1, 那么nxt数组就为5, 3, 4, 0, 0
  • 维护树状数组,tree[i]表示第1个数到第i个数中不同的数的个数
  • 首先,对每个不同的数出现的第一个位置first加到树状数组里,add(first, 1)
  • 然后,循环询问,记一个last变量,因为按照l升序,所以要把上一个询问的l到这一个询问的l中的那一段数删掉(即抹除它对答案的贡献), add(last, -1)
  • 删除数的同时,把它的nxt加入,统计贡献,add(nxt[i], 1)
  • 没错就是这样,那么这个询问的答案就是query( r )

然后上代码

#include <bits/stdc++.h>
#define maxn 500010
using namespace std;
struct node{
    int a, b, id;
}a[maxn];
struct ques{
    int l, r, id;
}q[maxn];
int n, nxt[maxn], flag[maxn], tree[maxn], m, ans[maxn], pos[maxn];

int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

bool cmp1(node x, node y){ return x.a < y.a; }
bool cmp2(node x, node y){ return x.id < y.id; }
bool cmp3(ques x, ques y){ return x.l < y.l; }
int lowbit(int x){ return x & -x; }
void add(int x, int y){ for (; x <= n; x += lowbit(x)) tree[x] += y; }
int query(int x){ int sum = 0; for (; x; x -= lowbit(x)) sum += tree[x]; return sum; }

int main(){
    n = read();
    for (int i = 1; i <= n; ++i) a[i].a = read(), a[i].id = i;
    
    sort(a + 1, a + 1 + n, cmp1);
    for (int i = 1; i <= n; ++i) a[i].b = a[i].a == a[i - 1].a ? a[i - 1].b : a[i - 1].b + 1;
    sort(a + 1, a + 1 + n, cmp2);
    //这部分是离散化
    for (int i = n; i > 0; --i) nxt[i] = pos[a[i].b], pos[a[i].b] = i;
    //预处理nxt数组
    for (int i = 1; i <= n; ++i)
        if (!flag[a[i].b]){
            add(i, 1); flag[a[i].b] = 1;
        }
    //先对每个不同的数出现的第一个位置加入
    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 + 1 + m, cmp3);
    //对q进行排序
    int last = 0;
    for (int i = 1; i <= m; ++i){
        while (last < q[i].l - 1){
            add(++last, -1);
            if (nxt[last]) add(nxt[last], 1);
        }
        //last扫到当前询问的l
        ans[q[i].id] = query(q[i].r);
        //更新答案
    }
    for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
    return 0;
}
  • 你肯定还没懂,没关系,我现在解释一下为什么这么做
  • 首先,如果数列中如果有一堆相同的数,对答案的贡献也只能是1对吧,那我们不妨在每个数在数列中出现的第一个位置add
  • 然后last往后扫到当前询问的l,当然要把扫到的数del一下,因为以后再也用不到了(对l升序排序),那这个位置del了,是不是它的nxt要add呀,因为每个不同的数我只add了一次
  • 你一定还有诸多疑问,怀疑我的算法的正确性,接下来我口头证明一下
  • 保证不多减:感性理解一下,last扫到的点是一定在以前add过且正对答案起着作用,所以现在不能用了del一下完全没毛病
  • 保证不多加:因为每个不同的数只加一次,这个位置的数del了才轮到它的nxt,所以也是没毛病的
  • 保证不漏加:一开始我对每个数出现的第一个位置add,保证了没有疏漏的情况
  • 所以,我的做法是正确的

成功将卡莫队的题优化成了树状数组的做法(当然线段树亦可)
不过这只是蓝题难度,接下来难度升级到紫题(其实没差)

  • 例:给定长为n的一列数,以及q个询问(l,r),要求输出al…ar中有多少个出现至少两次的数
  • 同样有原题
  • 发现了吗,上题是至少出现一次,这下是至少出现2次,难度就从蓝题升到紫题
  • 同样不要求在线,同样卡莫队

还是先放一发莫队TLE代码:

#include <bits/stdc++.h>
#define maxn 2000010
using namespace std;
struct node{
    int a, b, id;
}a[maxn];
struct ques{
    int l, r, id, pos, ans;
}q[maxn];
int ans, n, m, block, cnt[maxn], c;

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

bool cmp1(node x, node y){ return x.a < y.a; }
bool cmp2(node x, node y){ return x.id < y.id; }
bool cmp3(ques x, ques y){ return x.pos == y.pos ? x.r < y.r : x.pos < y.pos; }
bool cmp4(ques x, ques y){ return x.id < y.id; }
void add(int x){ ans += ++cnt[a[x].b] == 2 ? 1 : 0; }
void del(int x){ ans -= --cnt[a[x].b] == 1 ? 1 : 0; }

void solve(){
    int l = 1, r = 1; add(1);
    for (int i = 1; i <= m; ++i){
        for (; r < q[i].r; add(++r));
        for (; r > q[i].r; del(r--));
        for (; l < q[i].l; del(l++));
        for (; l > q[i].l; add(--l));
        q[i].ans = ans;
    }
}

int main(){
    n = read(), c = read(), m = read();
    for (int i = 1; i <= n; ++i) a[i].a = read(), a[i].id = i;
    sort(a + 1, a + 1 + n, cmp1);
    for (int i = 1; i <= n; ++i) a[i].b = a[i].a == a[i - 1].a ? a[i - 1].b : a[i - 1].b + 1;
    sort(a + 1, a + 1 + n, cmp2);
    block = sqrt(n);
    for (int i = 1; i <= m; ++i){
        q[i].l = read(), q[i].r = read();
        q[i].id = i, q[i].pos = q[i].l / block;
    }
    sort(q + 1, q + 1 + m, cmp3);
    solve();
    sort(q + 1, q + 1 + m, cmp4);
    for (int i = 1; i <= m; ++i) printf("%d\n", q[i].ans);
    return 0;
}
  • 然后我们把两道题放在一起比较,发现莫队做法其实基本相同!就是在add与del里改了两个数字的功夫
  • 想到树状数组做法是不是也一样
  • 上题是当前数对答案有影响,那么本题就是当前数的nxt对答案有影响
  • 上题是del当前数,add当前数的nxt;那么本题就是del当前数的nxt,add当前数的nxt的nxt
  • 然后就是放代码了,由于代码与上题惊人的相似,我就不弄注释了

Code:

#include <bits/stdc++.h>
#define maxn 2000010
using namespace std;
struct node{
    int a, b, id;
}a[maxn];
struct ques{
    int l, r, id;
}q[maxn];
int n, nxt[maxn], flag[maxn], tree[maxn], m, ans[maxn], pos[maxn], c;

int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

bool cmp1(node x, node y){ return x.a < y.a; }
bool cmp2(node x, node y){ return x.id < y.id; }
bool cmp3(ques x, ques y){ return x.l < y.l; }
int lowbit(int x){ return x & -x; }
void add(int x, int y){ for (; x <= n; x += lowbit(x)) tree[x] += y; }
int query(int x){ int sum = 0; for (; x; x -= lowbit(x)) sum += tree[x]; return sum; }

int main(){
    n = read(), c = read(), m = read();
    for (int i = 1; i <= n; ++i) a[i].a = read(), a[i].id = i;
    sort(a + 1, a + 1 + n, cmp1);
    for (int i = 1; i <= n; ++i) a[i].b = a[i].a == a[i - 1].a ? a[i - 1].b : a[i - 1].b + 1;
    sort(a + 1, a + 1 + n, cmp2);
    for (int i = n; i > 0; --i) nxt[i] = pos[a[i].b], pos[a[i].b] = i;
    for (int i = 1; i <= n; ++i)
        if (!flag[a[i].b]){
            if (nxt[i]) add(nxt[i], 1);
            flag[a[i].b] = 1;
        }
    for (int i = 1; i <= m; ++i) q[i].l = read(), q[i].r = read(), q[i].id = i;
    sort(q + 1, q + 1 + m, cmp3);
    int last = 0;
    for (int i = 1; i <= m; ++i){
        while (last < q[i].l - 1){
            ++last;
            if (nxt[last]) add(nxt[last], -1);
            if (nxt[nxt[last]]) add(nxt[nxt[last]], 1);
        }
        ans[q[i].id] = query(q[i].r);
    }
    for (int i = 1; i <= m; ++i) printf("%d\n", ans[i]);
    return 0;
}

那么我们推而广之,一列数中,l到r至少出现k次的数的个数是不是也可以求?
当然是可以哒
令ne[i]表示,从第i个数开始,第i个数算第1个,往后数到第k个与当前数值相同的数的位置
那么我们一开始是不是把每个不同的数字出现的第一个位置的ne加入统计
然后last更新到当前询问的左端点时,总是del(ne[last]) add(nxt[ne[last]])
比较上题差不多

现在的问题是:ne数组怎么求?
这个k的范围也是1e6,总不能暴力跳吧?
这个时候,我们可以倍增跳啊!
把nxt数组改装成st表的形式就行啦


那么,我现在可以做到求至少出现k次的数,是不是也可以求刚好出现k次的数?

其实很简单,刚好出现k次的数=至少出现k次的数的个数-至少出现(k+1)次的数的个数
等于就是上题做两遍,一遍做k,一遍做k+1

好,现在把这两个新的询问合并起来,形成一道新题目!

这其实是我自己瞎搞的题目

solution:

题目大意:给定长为n的数列a1-an,m个询问,正整数k,询问(opt,l,r),opt=1,输出al~ar中有多少个数出现刚好k次;opt=2,输出al-ar中有多少个数出现至少k次

  • 30分暴力,很好打,开桶,直接循环搞,时间复杂度O(nm),over
    Code:
#include <bits/stdc++.h>
#define maxn 5010
using namespace std;
int n, m, k, a[maxn], cnt[maxn + 1];

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

int main(){
	n = read(), m = read(), k = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	while (m--){
		int opt = read(), l = read(), r = read();
		memset(cnt, 0, sizeof(cnt));
		for (int i = l; i <= r; ++i) ++cnt[a[i]];
		int ans = 0;
		for (int i = 1; i <= maxn; ++i)
			if (opt == 1) ans += cnt[i] == k; else ans += cnt[i] >= k;
		printf("%d\n", ans);
	}
	return 0;
}
  • 80分莫队,莫队模板题,add与del的操作稍微有点复杂
    Code:
#include <bits/stdc++.h>
#define maxn 1000010
using namespace std;
int ans, n, m, k, a[maxn], cnt[maxn], block, ans1;
struct ques{
	int l, r, id, ans, pos, opt;
}q[maxn];

inline int read(){
	int s = 0, w = 1;
	char c = getchar();
	for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
	for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
	return s * w;
}

bool cmp1(ques x, ques y){ return x.pos == y.pos ? x.r < y.r : x.pos < y.pos; }
bool cmp2(ques x, ques y){ return x.id < y.id; }
void add(int x){ ans += ++cnt[a[x]] == k; ans -= cnt[a[x]] == (k + 1); ans1 += cnt[a[x]] == k;}
void del(int x){ ans += --cnt[a[x]] == k; ans -= cnt[a[x]] == (k - 1); ans1 -= cnt[a[x]] == (k - 1); }
//ans统计多少个出现次数恰好为k的数,ans1统计多少个出现次数至少为k的数

void solve(){
	int l = 1, r = 1; add(1);
	for (int i = 1; i <= m; ++i){
		for (; r < q[i].r; add(++r));
		for (; r > q[i].r; del(r--));
		for (; l < q[i].l; del(l++));
		for (; l > q[i].l; add(--l));
		q[i].ans = q[i].opt == 1 ? ans : ans1;
	}
}

int main(){
	n = read(), m = read(), k = read();
	for (int i = 1; i <= n; ++i) a[i] = read();
	block = sqrt(n);
	for (int i = 1; i <= m; ++i) q[i].opt = read(), q[i].l = read(), q[i].r = read(), q[i].id = i, q[i].pos = q[i].l / block;
	sort(q + 1, q + 1 + m, cmp1);
	solve();
	sort(q + 1, q + 1 + m, cmp2);
	for (int i = 1; i <= m; ++i) printf("%d\n", q[i].ans);
	return 0;
}
  • 100分树状数组,就用上面说的方法

Code:

#include <bits/stdc++.h>
#define maxn 600010
using namespace std;
int n, m, k, tree[maxn], tree1[maxn], flag[maxn], ne[maxn], ne1[maxn], nxt[maxn][22], pos[maxn], a[maxn], power[30], ans1[maxn], ans2[maxn];
struct ques{
    int l, r, id, ans1, ans2, opt;
}q[maxn];

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

inline int write(int x){
    if(x<0){putchar('-');x=~(x-1);}
    int s[20],top=0;
    while(x){s[++top]=x%10;x/=10;}
    if(!top)s[++top]=0;
    while(top)putchar(s[top--]+'0');
    puts("");
}


inline bool cmp1(ques x, ques y){ return x.l < y.l; }
inline bool cmp2(ques x, ques y){ return x.id < y.id; }
inline void add(int x, int y){ for (; x <= n; x += (x & -x)) tree[x] += y; }
inline int query(int x){ int sum = 0; for (; x; x -= (x & -x)) sum += tree[x]; return sum; }
inline void add1(int x, int y){ for (; x <= n; x += (x & -x)) tree1[x] += y; }
inline int query1(int x){ int sum = 0; for (; x; x -= (x & -x)) sum += tree1[x]; return sum; }

int main(){
    n = read(), m = read(), k = read();
    for (register int i = 1; i <= n; ++i) a[i] = read();
    for (register int i = n; i; --i) nxt[i][0] = pos[a[i]], pos[a[i]] = i;
    for (register int j = 1; j <= 20; ++j)
        for (register int i = 1; i <= n; ++i) nxt[i][j] = nxt[nxt[i][j - 1]][j - 1]; 
    power[0] = 1;
    for (register int i = 1; i <= 20; ++i) power[i] = power[i - 1] << 1;
    for (register int i = 1; i <= n; ++i){
        int sum = k - 1; ne[i] = i;
        for (register int j = 20; j >= 0; --j) if (sum >= power[j]) ne[i] = nxt[ne[i]][j], sum -= power[j];
        ne1[i] = nxt[ne[i]][0];
    }
    for (register int i = 1; i <= m; ++i) q[i].opt = read(), q[i].l = read(), q[i].r = read(), q[i].id = i;
    sort(q + 1, q + 1 + m, cmp1);
    for (register int i = 1; i <= n; ++i)
        if (!flag[a[i]]){
            flag[a[i]] = 1;
            if (ne[i]) add(ne[i], 1);
            if (ne1[i]) add1(ne1[i], 1);
        }
    int last = 0;
    for (register int i = 1; i <= m; ++i){
        while (last < q[i].l - 1){
            ++last;
            if (ne[last]) add(ne[last], -1);
            if (nxt[ne[last]][0]) add(nxt[ne[last]][0], 1);
            if (ne1[last]) add1(ne1[last], -1);
            if (nxt[ne1[last]][0]) add1(nxt[ne1[last]][0], 1);
        }
        ans1[q[i].id] = query(q[i].r);
        if (q[i].opt == 1) ans2[q[i].id] = query1(q[i].r);
    }
    for (register int i = 1; i <= m; ++i) write(ans1[i] - ans2[i]);
    return 0;
}

当然,能用树状数组做也可以用线段树做,不过你有没有发现,这边数据结构维护的仅仅是一个前缀罢了?这种简单的运算不是交给树状数组这个支持的运算不能太复杂但是码量超级小的算法吗?

接下来的问题是,这个k能不能把它放到询问里?这个以后再想,先去颓文化课作业了~~

猜你喜欢

转载自blog.csdn.net/ModestCoder_/article/details/88876358