在网上看了好多大神的博客,反正自己是绝对想不到怎么做的,好不容易感觉自己迷迷糊糊懂了个大概,就先记下来,免得以后忘记。有的地方思路很勉强甚至是错的,欢迎大家指教纠正。
题意:给你一个1~n的排列序列,m次查询,每次给出一个区间,求这个区间内使每个分组里面的数字都是连续的最小的分组方法。(题目链接:点击打开原题链接)
题解:因为求的是区间内的连续数字的分组,那么对于当前要查询的区间来说,它前面的数字就是无效的,所以我们可以将所有要查询的区间储存起来然后排序,从第一个区间开始把左边的不会用到的数“删除”,这里说的“删除”不是将这个数从数列里面去掉,而是指把它自身对最终答案的贡献变为0 。怎么做到这一点呢?
首先我们要初始化每个位置的数对当前整个序列来说的分组贡献(在树状数组上操作,方便单点更新和区间查询):
(1)当一个数单独出现而它左右的数字并没有出现的时候,这个数只能单独被分为一个区间,那它对分组数量的贡献就为1;
(2)当一个数出现时,它的前面或者后面的数字已经出现了,那么这个数就能和它组成一个区间,所以不需要新增分组,它对分组数量的贡献为0;
(3)当一个数出现前它的前后的数字都已经出现了,那么这个数就能将前后两个数连起来使两个区间变成一个区间(一个数的前后两个数原本是不能连在一起的),那么区间数就会减少,所以它对分组数的贡献为-1;
初始化以后,我们再根据要查询的区间对区间左边的数进行“删除”,对于区间左边的每一个数,我们分几种情况讨论:
(1)这个数比它前后的数都要后出现,那么它前后的数已经被删掉了,所以这个点对区间的贡献为-1;
(2)这个数比它前后的数都要先出现,原本我们初始化的时候这个点要算一个区间的,现在更新-1,,而此时他前后的数都还在,删掉了它以后它前后的数就分别表示一个区间了,所以前后的数对区间的贡献都更新为1;
(3)这个数只比它前面的数先出现,将他自己的贡献更新为-1,它前面的数在他被删掉以后只能自己做一个分组所以更新贡献为1;
(4)这个数只比它后面的数先出现,将他自己的贡献更新为-1,它后面的数在他被删掉以后只能自己做一个分组所以更新贡献为1;
每次进行完区间删除操作以后就查询树状数组上从序列开始到当前查询区间右端点的和保存下来,最后输出就行了。
附上代码(当然是舶来品,不过加了一些自己的注释):
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=100000+10;
int v[maxn],c[maxn];
int ans[maxn],a[maxn],n;
struct node
{
int s;
int t;
int id;
} q[maxn];
bool cmp(node n1,node n2)
{
return n1.s<n2.s;
}
int lowbit(int x)
{
return x&(-x);
}
void add(int i,int d)//树状数组单点更新
{
while(i<=n)
{
c[i]+=d;
i+=lowbit(i);
}
}
int sum(int i)//树状数组区间求和
{
int ret=0;
while(i>0)
{
ret+=c[i];
i-=lowbit(i);
}
return ret;
}
int main()
{
int T,i,j,m;
scanf("%d",&T);
while(T--)
{
scanf("%d%d",&n,&m);
memset(v,0,sizeof(v));
memset(c,0,sizeof(c));
memset(ans,0,sizeof(ans));
for(i=1; i<=n; i++)
{
scanf("%d",&a[i]);
v[a[i]]=i;//记录每个数的位置
}
v[0]=n+n;
v[n+1]=n+n;
for(i=1; i<=n; i++) //从1~n进行更新
{
if(i>v[a[i]-1]&&i>v[a[i]+1])//i表示位置,大于就是在他前后的数都出来了以后才出来
add(i,-1);
else if(i<v[a[i]-1]&&i<v[a[i]+1])//小于就是表示他出现了他前后的数还没有出现
add(i,1);
}
for(i=1; i<=m; i++)
{
scanf("%d%d",&q[i].s,&q[i].t);
q[i].id=i;
}
sort(q+1,q+m+1,cmp);
i=1;
j=1;
while(j<=m)
{
while(i<=n&&q[j].s>i)//将左边的删除
{
if(i>v[a[i]-1]&&i>v[a[i]+1])//落后它前后的数,那原本是应该减少线段的
//既然后已经落后了那么肯定他前面的数也要删掉
add(i,-1);
else if(i<v[a[i]-1]&&i<v[a[i]+1])//如果该位置本来是超前它前后的数
{
add(i,-1);//原本这个点要算一个新的区间因为他出现的时候它相邻的数没有出现,现在不要这个点了那就要删除
int mi=v[a[i]-1]<v[a[i]+1]? v[a[i]-1]:v[a[i]+1];
int ma=v[a[i]-1]>v[a[i]+1]? v[a[i]-1]:v[a[i]+1];
//add(v[a[i]-1],1);add(v[a[i]+1],1);
add(mi,1);//这个点被删除了那它相邻的两个数就只能是单独的区间不能连续了
add(ma,1);//所以都更新为1
}
else if(i<v[a[i]-1])//如果它比它前面的数先出现
{
add(i,-1);
add(v[a[i]-1],1);
}
else
{
add(i,-1);
add(v[a[i]+1],1);//总结一下,总之就是要删除这个点的话它前后的点只要比它后出现
//所以在它被删除以后不能有一个连续的区间所以它必须自己单独表示1
}
i++;
}
while(j<=m&&q[j].s<=i)//统计每个查询区间的和
{
ans[q[j].id]=sum(q[j].t);
j++;
}
}
for(i=1; i<=m; i++)
printf("%d\n",ans[i]);
}
return 0;
}