HH的项链题解

HH的项链题解

题目描述

HH 有一串由各种漂亮的贝壳组成的项链。HH 相信不同的贝壳会带来好运,所以每次散步完后,他都会随意取出一段贝壳,思考它们所表达的含义。HH 不断地收集新的贝壳,因此,他的项链变得越来越长。

有一天,他突然提出了一个问题:某一段贝壳中,包含了多少种不同的贝壳?这个问题很难回答…… 因为项链实在是太长了。于是,他只好求助睿智的你,来解决这个问题。

【数据范围】

对于 20\% 的数据, 1\le n,m\leq 5000
对于 40\% 的数据, 1\le n,m\leq 10^5
对于 60\% 的数据, 1\le n,m\leq 5\times 10^5
对于 100\% 的数据, 1\le n,m,a_i \leq 10^6 1\le l \le r \le n

本题可能需要较快的读入方式,最大数据点读入数据约 20MB

思路分析

莫队

这一题题意还是很清晰的,给定一组数,求一段区间里有多少不同的数字。

当然解法很多,可以用离线+树状数组,莫队来做,我们先说一下莫队的思路:

我们定义 cnt[i] 为数字 i 在一段区间里存在的个数,我们定义一个双指针 L,R , R 指针每扫过一个便使 cnt[a[i]]++ ,如果此时 cnt==1 ,说明这个数是第一次出现的, ans++ ,这样就能得到 [1,r] 区间每个数字出现的个数了 ,这时我们再让 L 从头开始扫,每扫到一个数便使 cnt[a[i]]-- ,如果此时 cnt==0 说明此时我们扫过的区间这个数已经不存在了, ans-- ,这样我们就能得到 [l,r] 这出现了多少数字。

这是一种暴力而且好想的思路,时间复杂度是 O(nm) ,显然过不去的,于是我们便要对这个算法进行改进。

我们不妨把一个区间 [l,r] 看成平面上的一个点 (x,y) ,此时我们暴力法的移动,便可以看成平面上坐标点的移动了,计算量就是两点之间的曼哈顿距离, m 次查询,相当与从原点出发,用一条直线连接这 m 个点,形成一条 Hamilton 路径,路径的长度就是计算量。如果能找到一条最短路径,计算量就更少。

但很遗憾$Hamilton$ 最短路问题没有多项式复杂度的解法,就算使用了状态压缩,复杂度也会达到 O(n^22^n) 这是一个天文数字。那么有没有一种算法能达到较好的结果呢?

暴力法的思路是按照坐标点 (x,y)x 值来排序,它并不是一种好方法,在 y 值变化大时路径也会变得很长,如图:

这样我们显然是不能接受的,因而莫队算法提出了一个较好的排序方法。

莫队算法的思路:离线+分块+暴力

简单来说,就是把数组分块,然后把查询的区间按左端点所在的块的序号排序,如果块相同,再按右边端点排序。放图解释下(图是书上的,我不想画了

我们发现莫队算法把暴力法 O(n) 的震动幅度降到了 O(\sqrt{n}) ,形成了一条较短的路径,从而降低了时间复杂度。具体说明参考 oiwiki

总时间复杂度 O(n\sqrt{n})

代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+7;
inline int read(){
	int f=1,x=0;char s=getchar();
	while(s>'9'||s<'0'){if(s=='-')f=-1;s=getchar();};
	while(s<='9'&&s>='0'){x=x*10+s-'0';s=getchar();}
	return x*f;
} 
struct node{
	int l,r,k;
}q[N];
int cnt[N],a[N],Ans[N],n,m,block,pos[N],ans;
inline bool cmp2(node a,node b){
	return pos[a.l]^pos[b.l]?pos[a.l]<pos[b.l]:(pos[a.l]&1)?a.r<b.r:a.r>b.r;
}
void add(int x){
	cnt[a[x]]++;
	if(cnt[a[x]]==1) ans++;
} 
void del(int x){
	cnt[a[x]]--;
	if(cnt[a[x]]==0) ans--;
}
int main(){
	n=read();
	block=sqrt(n);
	for(register int i=1;i<=n;i++) {
		a[i]=read();
		pos[i]=(i-1)/block+1;//所属块的位置 
	}
	m=read();	
	for(register int i=1;i<=m;i++) {
		q[i].l=read(),q[i].r=read();
		q[i].k=i;
	}
	sort(q+1,q+m+1,cmp2);int L=1,R=0;
	for(register int i=1;i<=m;++i){
		while(L<q[i].l) del(L++);
		while(R>q[i].r) del(R--);
		while(L>q[i].l) add(--L);
		while(R<q[i].r) add(++R);
		Ans[q[i].k]=ans;
	}
	for(register int i=1;i<=m;i++){
		printf("%d\n",Ans[i]);
	}
	return 0;
}

顺带一提

​ 如果把这份代码交上去,就会发现被TLE40了,没错这题特意加强了数据,即使加了一些玄学卡常也只能拿个 TLE92 ,如果有卡常大佬也可以尝试把上面代码卡过去。

​ 这题的正解是离线+树状数组或线段树或者扫描线,都可以让时间复杂度达到 O(nlogn)

​ 但不难发现,这些解法都是离线,作为一道题他也不可能要求强制在线,但如果我们上点强度,强制在线,比如要求每次答案异或上次答案,那此时还没有一种在线算法可以在通过这题呢?

​ 嘿!还真有,接下来出场的便是:

可持续化权值线段树(主席树)

主席树

浅谈一下

​ 我们知道有很多离线算法的时间复杂度相当优秀,但如果强制在线时间复杂度就并不是那么优异。为了解决这种问题,持续化思想则给了另一种想法,即记录每个时间轴的变动,保留每个历史版本,这样我们就能维护历史版本了。

​ 简单来说,相较于普通的数据结构,可持续化数据结构便是可以走“回头路”的。

具体实现

​ 这题的思路是倒序建立 n 棵线段树。每棵线段树有 n 个叶子节点,第 i 个叶子节点记录第 i 个元素是否出现过。

​ 倒序建立线段树时,每个元素都要建立一棵线段树,用 a_n 建立第一颗线段树,此时第 n 个叶子节点权值为 1 ,再用 a_{n-1} 建立第二棵线段树 ,与上一棵树连接,此时第 n-1 个叶子节点权值为 1 ,…这样第 i 棵线段树就可以表示 [n-i+1,n] 中的元素是否出现过。那如果遇到出现过的元素怎么办?我们可以通过在线段树中剔除这个重复的元素,只在最新一次出现时记录权值。

​ 具体实现我们可以定义一个 mp[] , mp[a[i]]=i 表示 a[i] 在第 n-i+1 棵线段树中第 i 个位置出现过,当我们用 a[k] 建立线段树时,如果此时 mp[a[k]]>0 说明这个元素曾经出现过,先把第 mp[a[k]] 个叶子节点更新为 0 ,然后再把第 k 个叶子节点更新为 1 ,注意第二次更新可以直接把第一次更新的线段树覆盖就行了,并不需要重新再建立一棵线段树。(由于这题1e6的毒瘤数据加上线段树本身的大常数,所以要用 unordered\_map ,但事后发现直接开大数组好像就行了)

​ 查询区间 [l,r] 时,由于 n-l+1 棵线段树只记录了 a_la_n 的不同数字情况,所以只需要在这棵线段树上查询 [1,r] 区间和就行了 (查询 [l,r] 也行的,但奈何常数太大只能查询 [1,r] 了,玄学优化两者相差了0.2秒

屏幕截图 2024-10-24 203116屏幕截图 2024-10-24 203142

​ 总复杂度为 O(nlogn) ,而且是在线的。但因为线段树本身自带的大常数,在离线情况下并不如树状数组的。当然主席树还有另外一种做法,详见

//没想到吧,主席树只需要60行,码量算很小了
#include<bits/stdc++.h> 
using namespace std;
const int N=1e6+3;
int cnt,n,m,a[N],b[N],root[N];
unordered_map<int,int> mp;
inline int read(){
	int f=1,x=0;char s=getchar();
	while(s>'9'||s<'0'){if(s=='-')f=-1;s=getchar();};
	while(s<='9'&&s>='0'){x=x*10+(s-'0');s=getchar();}
	return x*f;
} 
inline void write(int x){
    if(x<0)putchar('-'),x=-x;
    if(x>9)write(x/10);
    putchar(x%10+'0');
    return;
}
struct tree{
	int l,r,val;
}pst[N<<5];
int update(int pre,int pl,int pr,int x,int val){
	int rt=++cnt;
	pst[rt].l=pst[pre].l;
	pst[rt].r=pst[pre].r;
	pst[rt].val=pst[pre].val+val;
	int mid=(pl+pr)>>1;
	if(pl<pr){
		if(x<=mid) pst[rt].l=update(pst[pre].l,pl,mid,x,val);
		else pst[rt].r=update(pst[pre].r,mid+1,pr,x,val);
	}
	return rt;
}
int query(int node,int pl,int pr,int l,int r){
	if(l<=pl&&pr<=r) return pst[node].val;
	int res=0,mid=(pl+pr)>>1;
	if(l<=mid) res+=query(pst[node].l,pl,mid,l,r);
	if(mid+1<=r) res+=query(pst[node].r,mid+1,pr,l,r);
	return res;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++) a[i]=read();
	for(int i=n;i>=1;i--){
		if(mp[a[i]]){//编号从大到小来也是可以的
			root[i]=update(root[i+1],1,n,mp[a[i]],-1);//-1就是把那个节点更新为0
			root[i]=update(root[i],1,n,i,1); //直接覆盖上一棵线段树就行了
		}
		else root[i]=update(root[i+1],1,n,i,1);
		mp[a[i]]=i;
	} 
	cin>>m;
	while(m--){
		int l,r;
		l=read();r=read();
		write(query(root[l],1,n,l,r)); 
		putchar('\n');
	}
	return 0;
}
3 个赞

这篇题解还没写完哈,等会我把主席树思路补一下

1 个赞

%%%膜巨佬

1 个赞

%%%膜巨佬

1 个赞

强!!

1 个赞

主席树?!

1 个赞

时间不早得睡觉了,这篇明天我再补吧,等10月26考完给大家出一个可持续化数据结构讲解

1 个赞

巨佬会?

OK,这篇也是写好了好吧

其实不算特别难的,不要神化了,而且码量跟普通的也差不多的