2019 湘潭CCPC :Chika and Friendly Pairs(莫队 + 树状数组)

4 篇文章 0 订阅
1 篇文章 0 订阅

题目描述:

大意是有一个序列,然后问你[L,R]区间内 ,满足 i < j 并且ai 和 aj 的差值不超过K的 数字对有多少对。

数据范围是1 <= n,m <= 27000 , 1 <= k <= 10^9 。

当时比赛写了一发主席树,最后15秒调出来交了T了。
正解是莫队,学了莫队后就来把这题补了。

对于区间[L,R]的答案,容易发现可以暴力套线段树或树状数组之类的东西,从左往右遍历这个区间,依次求出[a[i] + k,a[i] - k]的个数,并维护更新。

套用莫队算法:莫队移动指针的过程中,涉及到将某个数字移出当前区间和加入当前区间的操作,因此要求这两种操作对当前莫队维护的区间[L,R]的答案的贡献。

首先分类讨论,因为左右指针共有4种移动方向,分别对应区间扩展和区间收缩,对区间收缩和区间扩展时答案的变化进行分类讨论即可:

在草稿纸上打下草稿可以发现,求贡献的式子都是一样的,这就很好处理了

例如L向左移动一个位置,这时相当于要把a[L] 加入 维护的区间,并更新答案,最左边的数字怎么对答案有贡献呢?最左边的数字对在它右边的所有数字中数值范围在[a[L] - k,a[L] + k] 内的数字有影响,使得它们对答案的贡献 + 1,只要用树状数组求一下这个范围内数字有多少个,答案加上这么多,再把这个数字放入区间维护区间信息。其它的可以用类似的方式,会发现影响都是一样的。

乍一看以为需要用到主席树,此时维护的区间[L,R]虽然没有下标而言,但我们是按顺序"加入树状数组"的,原因是因为我们总是按顺序移动左指针和右指针,使得这两个指针永远不交叉,答案永远合法,永远可以用这个式子来更新而不破坏下标序列的问题。

题目乍一看只能 n * sqrt(n),实际上允许n * sqrt(n) * logn,需要优化掉离散化的那个log。
不难想到可以预处理,有用到的数字是a[i],a[i] + K,a[i] - K,将这3 * n 个数字离散化就行了,但是如何预处理存在外面呢? 后两个都是a[i]有关的,可以对离散化后的a[i]开个桶,将这两个数字存到桶内,要用的时候拿处理即可。

代码:

#include<bits/stdc++.h>
using namespace std;
#define lowbit(i) (i & (-i))
const int maxn = 3e5 + 10;
const int maxm = 2e5 + 10;
int n,m,k,block;
int t[maxn],tot,p,a[maxn],b[maxn];
int val[maxm + 10],L,R,res,ans[maxn];
vector<int> g[maxn];
void init() {
	sort(t + 1,t + tot + 1);
	p = unique(t + 1,t + tot + 1) - t - 1;
}
int Hash(int x) {
	return lower_bound(t + 1,t + p + 1,x) - t;	
}
void update(int x,int v) {
	for(; x <= maxm; x += lowbit(x)) 
		val[x] += v;
}
int ask(int x) {
	int ans = 0;
	for(; x > 0; x -= lowbit(x))
		ans += val[x];
	return ans;
}
struct ss{
	int l,r,id;
}qes[maxn];
bool cmp(ss a,ss b) {
	return a.l / block == b.l / block ? a.r < b.r : a.l < b.l;
}
void query(int l,int r) {
	while(L > l) {
		L--;
		res += ask(g[a[L]][1]) - ask(g[a[L]][0]);
		update(a[L],1);
	}
	while(R < r) {
		R++;
		res += ask(g[a[R]][1]) - ask(g[a[R]][0]);
		update(a[R],1);	
	}
	while(L < l) {
		update(a[L],-1);
		res -= ask(g[a[L]][1]) - ask(g[a[L]][0]);
		L++;
	}
	while(R > r) {
		update(a[R],-1);
		res -= ask(g[a[R]][1]) - ask(g[a[R]][0]);
		R--;
	}	
}
int main() {
	scanf("%d%d%d",&n,&m,&k);
	for(int i = 1; i <= n; i++) {
		scanf("%d",&a[i]);
		t[++tot] = a[i];
		t[++tot] = a[i] - k - 1;
		t[++tot] = a[i] + k;
	}
	init();
	for(int i = 1; i <= n; i++) {
		int tmp = Hash(a[i]);
		g[tmp].push_back(Hash(a[i] - k - 1));
		g[tmp].push_back(Hash(a[i] + k));
		a[i] = tmp;
	}
	for(int i = 1; i <= m; i++) {
		scanf("%d%d",&qes[i].l,&qes[i].r);
		qes[i].id = i;
	}
	block = sqrt(n);
	sort(qes + 1,qes + 1 + m,cmp);
	L = R = 1;update(a[1],1);
	
	for(int i = 1; i <= m; i++) {
		int p = qes[i].id;
		query(qes[i].l,qes[i].r);
		ans[p] = res;
	}
	for(int i = 1; i <= m; i++) {
		printf("%d\n",ans[i]);
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值