NKOJ 2182 (HEOI 2012) 采花(树状数组/线段树)

P2182【河北OI 2012 DAY1】采花

问题描述

萧芸斓是Z 国的公主,平时的一大爱好是采花。
今天天气晴朗,阳光明媚,公主清晨便去了皇宫中新建的花园采花。花园足够大,容纳
了n 朵花,花有c 种颜色(用整数1-c 表示),且花是排成一排的,以便于公主采花。
公主每次采花后会统计采到的花的颜色数,颜色数越多她会越高兴!同时,她有一癖好,
她不允许最后自己采到的花中,某一颜色的花只有一朵。为此,公主每采一朵花,要么此前
已采到此颜色的花,要么有相当正确的直觉告诉她,她必能再次采到此颜色的花。
由于时间关系,公主只能走过花园连续的一段进行采花,便让女仆福涵洁安排行程。福
涵洁综合各种因素拟定了m 个行程,然后一一向你询问公主能采到多少朵花(她知道你是编
程高手,定能快速给出答案!),最后会选择令公主最高兴的行程(为了拿到更多奖金!)。

输入格式

第一行四个空格隔开的整数n、c 以及m。
接下来一行n 个空格隔开的整数,每个数在[1, c]间,第i 个数表示第i 朵花的颜色。
接下来m 行每行两个空格隔开的整数l 和r(l ≤ r),表示女仆安排的行程为公主经
过第l 到第r 朵花进行采花。

输出格式

共m 行,每行一个整数,第i 个数表示公主在女仆的第i 个行程中能采到的花的颜色数。

样例输入

5 3 5
1 2 2 3 1
1 5
1 2
2 2
2 3
3 5

样例输出

2
0
0
1
0

数据范围

对于20%的数据,n ≤ 10^2,c ≤ 10^2,m ≤ 10^2;
对于50%的数据,n ≤ 10^5,c ≤ 10^2,m ≤ 10^5;
对于100%的数据,1 ≤ n ≤10^5,c ≤ n,m ≤ 10^5。


主要是两种思路,都是离线算法。
做法一:
令$C[i]$表示从1到i总共出现的不同颜色且出现次数大于1的花的数量,那么预处理出每一朵花的下一个和他同颜色的花的位置,记到$next[x]$数组中
令当前讨论的左端点为$x$,那么当x左移的时候,对$C[i]$产生的影响是$next[x]$到$next[next[x]]-1$位置的$C[i]$减一,于是将区间排序,用线段树维护$C[i]$,进行区间操作即可。


代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include<stdio.h>
#include<iostream>
#include<algorithm>
#define N 200005
using namespace std;
struct node{int x,y,id,ans;}B[N];
bool cmp1(node a,node b)
{return a.x<b.x;}
bool cmp2(node a,node b)
{return a.id<b.id;}
int n,c,m,A[N],NE[N],LA[N],C[N],cnt[N];
int ls[N*10],rs[N*10],v[N*10],lazy[N*10],tot;
int BT(int x,int y)
{
int p=++tot;
if(x<y)
{
int mid=x+y>>1;
ls[p]=BT(x,mid);
rs[p]=BT(mid+1,y);
}
else v[p]=C[x];
return p;
}
void PD(int p)
{
v[ls[p]]+=lazy[p];
v[rs[p]]+=lazy[p];
lazy[ls[p]]+=lazy[p];
lazy[rs[p]]+=lazy[p];
lazy[p]=0;
}
void MD(int p,int l,int r,int x,int y,int d)
{
if(lazy[p])PD(p);
if(x<=l&&y>=r){lazy[p]+=d;v[p]+=d;return;}
int mid=l+r>>1;
if(x<=mid&&y>=l)MD(ls[p],l,mid,x,y,d);
if(x<=r&&y>mid)MD(rs[p],mid+1,r,x,y,d);
}
int GS(int p,int l,int r,int k)
{
if(lazy[p])PD(p);
if(l==r)return v[p];
int mid=l+r>>1;
if(k<=mid)return GS(ls[p],l,mid,k);
return GS(rs[p],mid+1,r,k);
}
int main()
{
int i,j,k;
scanf("%d%d%d",&n,&c,&m);
for(i=1;i<=n;i++)scanf("%d",&A[i]);
for(i=n;i>=1;i--)
{
if(!LA[A[i]])NE[i]=n+1;
else NE[i]=LA[A[i]];
LA[A[i]]=i;
}
NE[n+1]=n+1;
for(i=1;i<=n;i++)
{
cnt[A[i]]++;
C[i]=C[i-1];
if(cnt[A[i]]==2)C[i]++;
}
BT(1,n);
for(i=1;i<=m;i++)scanf("%d%d",&B[i].x,&B[i].y),B[i].id=i;
sort(B+1,B+m+1,cmp1);i=1;j=1;
while(j<=m)
{
while(B[j].x>i)MD(1,1,n,NE[i],NE[NE[i]]-1,-1),i++;
while(B[j].x==i)B[j].ans=GS(1,1,n,B[j].y),j++;
}
sort(B+1,B+m+1,cmp2);
for(i=1;i<=m;i++)printf("%d\n",B[i].ans);
}

做法二:
始终将从当前讨论位置往后,每种颜色的第二次出现的位置的地方为1,其他地方为0,那么维护前缀和数组即可求出答案。
因此,同样用$next$数组,然后用树状数组维护前缀和。当讨论到一个询问的左端点时,右端点的前缀和值就是答案,只需要始终维护从当前讨论位置开始,每种颜色第二次出现的位置即可。


代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include<stdio.h>
#include<iostream>
#include<algorithm>
#define N 200005
using namespace std;
struct node{int x,y,id,ans;}B[N];
bool cmp1(node a,node b)
{return a.x<b.x;}
bool cmp2(node a,node b)
{return a.id<b.id;}
int n,c,m,A[N],NE[N],LA[N],C[N],D[N];
void MD(int x,int d)
{for(int i=x;i<=n;i+=(i&-i))D[i]+=d;}
int GS(int x)
{
int i,sum=0;
for(i=x;i;i-=(i&-i))sum+=D[i];
return sum;
}
int main()
{
int i,j,k;
scanf("%d%d%d",&n,&c,&m);
for(i=1;i<=n;i++)scanf("%d",&A[i]);
NE[n+1]=n+1;
for(i=n;i>=1;i--)
{
if(!LA[A[i]])NE[i]=n+1;
else NE[i]=LA[A[i]];
LA[A[i]]=i;
MD(NE[i],1);
MD(NE[NE[i]],-1);
}

for(i=1;i<=m;i++)scanf("%d%d",&B[i].x,&B[i].y),B[i].id=i;
sort(B+1,B+m+1,cmp1);
i=1;k=1;
while(k<=m)
{
while(i<B[k].x)MD(NE[i],-1),MD(NE[NE[i]],1),i++;
while(i==B[k].x)B[k].ans=GS(B[k].y),k++;
}
sort(B+1,B+m+1,cmp2);
for(i=1;i<=m;i++)printf("%d\n",B[i].ans);
}