SPOJ D-query (莫队算法 || 主席树)

本文介绍了莫队算法解决区间查询问题的方法,包括算法原理、Java及C++实现过程,并对比了不同实现方式的效率。此外,还探讨了利用主席树进行区间查询的另一种解决方案。

Given a sequence of n numbers a1, a2, …, an and a number of d-queries. A d-query is a pair (i, j) (1 ≤ i ≤ j ≤ n). For each d-query (i, j), you have to return the number of distinct elements in the subsequence ai, ai+1, …, aj.

Input
Line 1: n (1 ≤ n ≤ 30000).
Line 2: n numbers a1, a2, …, an (1 ≤ ai ≤ 106).
Line 3: q (1 ≤ q ≤ 200000), the number of d-queries.
In the next q lines, each line contains 2 numbers i, j representing a d-query (1 ≤ i ≤ j ≤ n).
Output
For each d-query (i, j), print the number of distinct elements in the subsequence ai, ai+1, …, aj in a single line.

Example
Input
5
1 1 2 1 3
3
1 5
2 4
3 5

Output
3
2
3

问一个区间内有多少种数字。

曾经做过类似的区间问题,当时只有60种数字,所以可以用线段树+位操作(把数字种类用二进制记录在一个数上)来维护,但是这里有1e6种。

聪哥说这种数字特别大的一看就是莫队,所以这里用莫队!

写起来代码量意外的少,比线段树写起来舒服。

思想也很好理解,把查询排序之后就可以让每次的查询结果都基于前一个查询。

唯一不太懂的其实是为什么要分成根号n块,毕竟我没打过分块。接下来去研究一下…

一开始用java打了这题,发现无限tle:

public class Main {
	static int[] list = new int[30005];
	static Query[] queries = new Query[200005];
	static int[] cnt = new int[1000005];
	static int[] res = new int[200005];

	public static void main(String[] args) {
		//Scanner reader = new Scanner(System.in);
		InputReader reader = new InputReader();
		PrintWriter out = new PrintWriter(System.out);
		int n = reader.nextInt();
		for (int i = 0; i < n; i++) {
			list[i] = reader.nextInt();
		}

		int q = reader.nextInt();
		for (int i = 0; i < q; i++) {
			int l = reader.nextInt() - 1;
			int r = reader.nextInt() - 1;
			queries[i] = new Query(l, r, i);
		}

		int sqrtn = (int) Math.sqrt(n);
		Arrays.sort(queries, 0, q, new Comparator<Query>() {
			@Override
			public int compare(Query o1, Query o2) {
				int cmp = Integer.compare(o1.l / sqrtn, o2.l / sqrtn);
				return cmp != 0 ? cmp : o1.r - o2.r;
			}
		});

		int L = 1;
		int R = 0;
		int cur = 0;
		for (int i = 0; i < q; i++) {
			Query get = queries[i];
			while (L < get.l) {
				cur += remove(L++);
			}
			while (L > get.l) {
				cur += add(--L);
			}
			while (R < get.r) {
				cur += add(++R);
			}
			while (R > get.r) {
				cur += remove(R--);
			}
			res[get.i] = cur;
		}

		for (int i = 0; i < q; i++) {
			out.println(res[i]);
		}
		out.close();
	}

	public static int add(int i) {
		return ++cnt[list[i]] == 1 ? 1 : 0;
	}

	public static int remove(int i) {
		return --cnt[list[i]] == 0 ? -1 : 0;
	}

	static class Query {
		int l, r, i;

		public Query(int l, int r, int i) {
			this.l = l;
			this.r = r;
			this.i = i;
		}
	}
}

输入输出挂都上了,还是不行,果然spoj慢得名不虚传…

好吧,还是想AC,所以第一次C++就献给spoj了,以下是ac代码:

#include <bits/stdc++.h>
using namespace std;

int a[30005],cnt[1000005],res[200005];
int n,q,sqrtn;


struct Query{
    int l,r,id;
}qs[200005];

bool cmp(Query q1,Query q2){
    if(q1.l/sqrtn == q2.l/sqrtn){
        return q1.r<q2.r;
    }
    return q1.l/sqrtn < q2.l/sqrtn;
}

int add(int i){
    return ++cnt[a[i]] == 1?1:0;
}

int remove(int i){
    return --cnt[a[i]] == 0?-1:0;
}


int main(){
    scanf("%d",&n);
    for(int i = 0;i<n;i++){
        scanf("%d",&a[i]);
    }

    scanf("%d",&q);
    for(int i =0;i<q;i++){
        scanf("%d%d",&qs[i].l,&qs[i].r);
        qs[i].l--;
        qs[i].r--;
        qs[i].id = i;
    }

    sqrtn = sqrt(n);
    sort(qs,qs+q,cmp);

    int L = 1;
    int R = 0;
    int cur = 0;
    for(int i = 0;i<q;i++){
        Query get = qs[i];
        while(L<get.l){
            cur+= remove(L++);
        }
        while(L>get.l){
            cur+= add(--L);
        }
        while(R<get.r){
            cur+= add(++R);
        }
        while(R>get.r){
            cur+=remove(R--);
        }
        res[get.id] = cur;
    }

    for(int i = 0;i<q;i++){
        printf("%d\n",res[i]);
    }
    return 0;
}

感谢聪哥。

2018.10.10日:
最近又在补主席树,之前稍稍了解过一点,现在深入做一下套题训练。
想起来这题似乎也可以用主席树解,于是又回来打。

使用线段树维护整个1到n的区间里的数字种类。
每次以前一颗树为last版本,把新出现的位置加进去,如果数字重复出现,则在新的线段树中,先把之前出现的那个位置更新到现在的位置。
这样保证了对于询问[l,r],root[r]这棵树,存储了足够的信息。
进入这棵树,去找到询问的左区间,线段树内右区间的值则全部符合要求,累加得到答案。

ac代码:

#include<bits/stdc++.h>
using namespace std;

const int maxn = 30005;
int n, m, num[maxn];
int vis[1000005];

struct Node {
	int val;
	Node *lc, *rc;
} *root[maxn], pool[maxn * 20], *tail = pool, *null;

void init() {
	null = ++tail;
	null->val = 0;
	null->lc = null->rc = null;
	root[0] = null;
	memset(vis, 0, sizeof(vis));
}

Node *update(Node *pre, int l, int r, int pos, int val) {
	if(pos < l || pos > r) {
		return pre;
	}
	Node *nd = ++tail;
	if(l == r) {
		nd->val = pre->val + val;
		return nd;
	}
	int mid = (l + r) >> 1;
	nd->lc = update(pre->lc, l, mid, pos, val);
	nd->rc = update(pre->rc, mid + 1, r, pos, val);
	nd->val = nd->lc->val + nd->rc->val;
	return nd;
}

int query(Node *t, int l, int r, int pos) {
	if(l == r) {
		return t->val;
	}
	int mid = (l + r) >> 1;
	if(pos <= mid) {
		return t->rc->val + query(t->lc, l, mid, pos);
	}
	return query(t->rc, mid + 1, r, pos);
}

int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i++) {
		scanf("%d", &num[i]);
	}

	init();
	for(int i = 1; i <= n; i++) {
		if(!vis[num[i]]) {
			root[i] = update(root[i - 1], 1, n, i, 1);
		} else {
			int pre = vis[num[i]];
			Node *tmp = update(root[i - 1], 1, n, pre, -1);
			root[i] = update(tmp, 1, n, i, 1);
		}
		vis[num[i]] = i;
	}

	int l, r;
	scanf("%d", &m);
	while(m--) {
		scanf("%d%d", &l, &r);
		int ans = query(root[r], 1, n, l);
		printf("%d\n", ans);
	}
	return 0;
}
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值