【树状数组和思维】牛客网第一场的I J.Different Integers(可以规定番茄跟着做任务但要计划好)

题目大概是给你一个串和l和r 找到1~l  r~n里面不同的数字的个数

但是串很长,询问次数也很多,都有1e5,所以需要一些技巧:

(1)显然,把区间增大一倍,去查询(r,l+n)更好。

(2)怎么快速找到(x,y), 不同的数字的个数?

 

1.每个区间里面不同的个数维护一个sum[i] (似乎可以用set)

这样,sum[y]-sum[x-1]就是,在区间(x,y)出现了但是(1,x-1)没有出现的数字的个数

(没错吧!!!  因为那些只会有了新数字才进行更新)

2.再找到,(1,x-1)和(x,y)区间相同的数字的个数

我们可以知道,除了新增的,就是(x,y)区间里面本来就有过了重合的。

那么第二问怎么求呢?欢迎看一下下面的图。

【这个大概就是离线的做法 】

树状数组维护的是g[i]的和

www...   nxt是它们下一次出现的位置,记录一下就好

然后呢,g[i]是贡献值,其实也就是你下一次出现在哪里,哪里就=1

(大概是的)最后求一下这个区间里的和就是他们相同的数字出现的次数了, 只是,需要按每个查询顺序更新,不然可能会出点毛病?

 

其实想想也非常合理,前面所有的数字在后面出现了的话后面的地方 就置1,这样就是前面的在后面的一个影子出现了呀,

哪怕是(3 * **** 3 ***3),前面有个3 (更新了第二个3的位置),第二个3更新了第三个3的位置

1~(x-1)是很长的一段啊,没毛病,没毛病。。

这个数字没出现的话就没有影子~

 

参考文章:找到一篇链接,这写的可真好啊

点击这里 巴布巴布~ (下图来自链接)

需要什么内容呢 

比如我们想一下,(7,9)的时候去更新一下(1,6)的,然后) 换到(8,12)7那边+1, 这样10的位置也是1了(更新移动经过的位置~) 这样查询的和就是3了,还是很棒的,求和是3再加上原来的sum咯

(以上两图为转载,出处 这里

 

我的代码:(待续

--------------从前是线段树求最大值最小值,只要add和q一下就可以了

线段树也可以用来求区间的和:(下面是出处代码的解释)

sum是只增加不减少的,所以完全不用set,结合last用也可以,last是上一次这个元素出现的位置,那么:
1.last[a[i]] = i;  a[i]上一次出现的位置是xxx如果没出现过就更新sum
2.nxt[last[a[i]]] = i;
(在上一行运算之前) 
比如2先在3出现,然后在5出现,随后在7出现
然后last[2]=3  
这次其实a[i]=2,   last[2]=3    nxt[3]=5
 and after that 
last[2]=5 
然后
a[i]=2( i=5)
last[2]=5, 更新 nxt[5]=7 
last[2]=7……
这样我们也有了last数组(不这个没用)
我们就有了nxt数组,还有sum数组
剩下的是线段树里面的东西,g[i] 在线段树里内部的,,,, 方便理解了
g[i]是根据nxt来的嘛,nxt有的话就添加一下,询问的时候还是l,r,1,1,n


#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<vector>
using namespace std;
#define ll long long
#define lson id<<1
#define rson id<<1|1
const int maxn = 2e5 + 10;
struct question {
	int l, r, id;
}q[maxn];
int a[maxn];
int sum[maxn];                     //用于记录1~i中存在多少种数
int last[maxn];                    //用于记录每种数最后出现的位置(单纯为nxt数组服务)
int nxt[maxn];                     //用于记录每个数下一个相同数的位置
int tre[maxn<<2];                  //线段树的结果
int ans[maxn];                     //用于保存问题的答案
int n, m;
bool cmp(question aa, question bb) {
	return aa.l < bb.l;           
}
void push(int id) {                //更新
	tre[id] = tre[lson] + tre[rson];
}
void add(int L, int R, int x, int l, int r, int id) {//区间[L,R]的值+x
	if (L <= l&&r <= R)            //l和r是当前边界,id是当前id
	{
		tre[id] += x;
		return;
	}
	int mid = (l + r) >> 1;
	if (L <= mid) add(L, R, x, l, mid, lson);
	if (mid < R) add(L, R, x, mid + 1, r, rson);
	push(id);
}
int query(int L, int R, int l, int r, int id) {//查询区间[L,R]的结果
	if (L <= l&&r <= R)
	{
		return tre[id];
	}
	int mid = (l + r) >> 1;
	int s1 = 0, s2 = 0;
	if (L <= mid) s1 = query(L, R, l, mid, lson);
	if (mid < R) s2 = query(L, R, mid + 1, r, rson);
	return s1 + s2;
}
void input() {                     //输入
	int l, r;
	for (int i = 1; i <= n; i++) { //倍增
		scanf("%d", &a[i]);
		a[i + n] = a[i];
	}
	for (int i = 0; i < m; i++) {  //修改每个询问为[R,L+N]
		scanf("%d%d", &l, &r);
		q[i].l = r;
		q[i].r = l + n;
		q[i].id = i;
	}
}
void work() {                      //处理
	n <<= 1;
	sum[0] = 0;
	for (int i = 1; i <= n; i++) {
		if (last[a[i]] == 0)       //第一次出现的元素
			sum[i] = sum[i - 1] + 1;
		else {
			sum[i] = sum[i - 1];
			nxt[last[a[i]]] = i;   //更新上一次出现的nxt值
		}
		last[a[i]] = i;
	}
	sort(q, q + m, cmp);           //左边界升序排序
	int x = 1;                     //维护的X值表示1~X都为一扫描区间
	for (int i = 0; i < m; i++) {
		while (x < q[i].l) {       //更新X同时对每个元素下个元素不为空值得情况更新
			if (nxt[x])
				add(nxt[x], nxt[x], 1, 1, n, 1);
			x++;
		}                          //计算该询问的答案
		ans[q[i].id] = sum[q[i].r] - sum[q[i].l - 1] + query(q[i].l, q[i].r, 1, n, 1);
	}
}
int main() {
	while (~scanf("%d%d", &n, &m)) {
		memset(last, 0, sizeof(last));
		memset(nxt, 0, sizeof(nxt));
		memset(tre, 0, sizeof(tre));
		input();
		work();
		for (int i = 0; i < m; i++)//输出   
			printf("%d\n", ans[i]);
	}
	return 0;
}

方法很巧妙,思想和写的也很巧妙

我们看另一份代码,求和的话可能树桩数组更好一点:

基础思想是不变的,sum分开求好理解一些。

这份好理解了很多…………

cmd排序,再排序输出,树状数组更新的时候要n,,,,。。。

 

 


#include<bits/stdc++.h>
using namespace std;
const int maxn=200000+10;
const int inf=0x3f3f3f3f;
#define ll long long
struct P {
	int l,r;
	int id;
	int ans;
} p[2*maxn];
bool cmd(P a,P b) {
	return a.l<b.l;
}
bool cmd1(P a,P b) {
	return a.id<b.id;
}
/********树状数组*/
int lowbit(int x) {
	return x&(-x);
}
int c[maxn];
int sum(int i) {
	int s=0;
	while(i>0) {
		s+=c[i];
		i-=lowbit(i);
	}
	return s;
}
void add(int i,int val,int n) {
	if(i==0)
		return;
	while(i<=n) {
		c[i]+=val;
		i+=lowbit(i);
	}
	return;
}
int query_sum(int l,int r){  //求树状数组区间[l,r]的和 
	return sum(r)-sum(l-1);
}
/*********/ 
int n,m;
int pre[2*maxn];     //pre[i]表示区间[1,i]之间有多少个不同的数字 
int last[maxn],nxt[maxn];  //last为辅助数组,为了求nxt[],nxt[]表示每一个点的数值下一次出现的位置 
int num[2*maxn];      //输入的数组 
bool vis[maxn];       //辅助数组 
void init(int n) {    //预处理出pre[]和nxt[] 
	memset(vis,0,sizeof(vis));
	memset(pre,0,sizeof(pre));
	memset(last,-1,sizeof(last));
	memset(nxt,0,sizeof(nxt));
	for(int i=1; i<=n; i++) {
		if(!vis[num[i]]) {
			pre[i]=pre[i-1]+1;
			vis[num[i]]=true;
		}
		else
		pre[i]=pre[i-1];
	}
	for(int i=1; i<=n; i++) {
		if(last[num[i]]==-1) {
			last[num[i]]=i;
		} else {
			int u=last[num[i]];
			nxt[u]=i;
			last[num[i]]=i;
		}
	}
	return;
}
void solve(int n,int m) {  //求m次询问的结果 
	memset(c,0,sizeof(c));
	int top=0;
	for(int i=1; i<=m; i++) {
		int l=p[i].l;
		int r=p[i].r;
		for(top; top<l; top++) {
			add(nxt[top],1,2*n);
		}
		int u=sum(r);
		int v=sum(l-1);
		p[i].ans=pre[r]-pre[l-1]+query_sum(l,r);
	}
	return;
}
void debug(int n){
	for(int i=1;i<=2*n;i++){
		printf("%d%c",pre[i],i==2*n?'\n':' ');
	}
	for(int i=1;i<=2*n;i++){
		printf("%d%c",nxt[i],i==2*n?'\n':' ');
	}
	for(int i=1;i<=m;i++){
		printf("%d %d\n",p[i].l,p[i].r);
	}
}
int main() {
	while(~scanf("%d %d",&n,&m)) {
		for(int i=1; i<=n; i++) {
			scanf("%d",&num[i]);
			num[i+n]=num[i];
		}
		init(2*n);
		int l,r;
		for(int i=1; i<=m; i++) {    //查询[1,l],[r,n]区间有多少个不同的数字转换成查询[r,l+n]区间有多少个不同的数字的问题
			scanf("%d %d",&l,&r);
			p[i].l=r;
			p[i].r=l+n;
			p[i].id=i;
		}
		sort(p+1,p+m+1,cmd);         //离线处理 
		solve(n,m);
		sort(p+1,p+m+1,cmd1);
		for(int i=1; i<=m; i++) {
			printf("%d\n",p[i].ans);
		}
		//debug(n);
	}
	return 0;

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值