洲哥的询问
Time Limit
6s
Memory Limit
262144KB
Judge Program
Standard
Ratio(Solve/Submit)
Description:
洲哥有n条线段,第i条线段可以用一个闭区间[li, ri]表示,现在他想让你回答q个问题。
对于每个问题,洲哥会给出m个点,他想知道在他拥有的n条线段中,有几条线段包含的点的个数为质数。
点用一个数x表示其位置。对于某个点x,如果li <= x <= ri,则第i条线段包含了点x。
Input:
第一行T,表示有T组数据。(1 <= T <= 10)
对于每组数据,第一行为n,表示洲哥拥有的线段条数。(1 <= n <= 5e4)
然后紧跟n行,第i行有两个数li, ri,用一个空格隔开。(1 <= li <= ri <= n)
接下来一行为一个数q,表示洲哥给定的q个询问。(1 <= q <= n)
然后紧跟的q行为q个询问。
在每个询问中,第一个数为m,表示该询问包含的点的个数。(1 <= m <= n)
然后紧跟着m个数x1, x2, x3, ......, xm,表示每个点的坐标(1 <= xi <= n)。
输入保证1 <= ∑m <= n
Output:
对于每组数据,输出q行,表示q个问题的答案。每组数据之间不用空行隔开。
Sample Input:
1
6
1 2
1 3
1 5
2 3
3 5
3 6
3
3 2 3 5
2 3 4
1 6
Sample Output:
5
3
0
Source:
解析:
主席树的作用:
主席树根据每一条线段(排序后)根据左端点的值为树的编号进行可持久化插入右端点(新的树是在前面的树(左端点<i)基础上再插入左端点为i的线段的树)
本题通过区间的左右端点建树
这样第i棵树查询[r1,r2]的结果就是右端点在[r1,r2],左端点<=i的线段的数量。
查询时因为线段的连续性:在x从小到大排序后,包含x[2],x[8]的线段一定包含x[3..7]
这样一旦我们得到一个质数个数的区间[x[i],x[j]],那么左端点在(x[i-1],x[i]],右端点在[x[j],x[j+1])的线段都是满足的
得到左右端点的范围我们就可以将左端点范围作为根节点(注意这里因为主席树的前缀和性质只需要一次查询就够了,优化了查询),查询右端点符合条件的线段数目。
在本题中主席树处理的复杂度为m^2*logn
因此就因为这个复杂度里面包含m,所以就可以用它来处理m较小的情况。
正是因为这个复杂度跟m有关,我们才可以分块来分摊询问的复杂度,详细见下面的复杂度计算
较大的情况仍然用O(n)的复杂度处理
只要选取合适的边界,就可以使复杂度可控
O(n)的更新:根据线段来找,在[li,ri]的点的数量可以用点的前缀和来获得
先根据x[]得出他的前缀数组,注意这个前缀数组s[i]表示x[k]<=i的数的个数
然后我们再把每一条线段套到s[]这个数组上prime[s[a[j].r]-s[a[j].l-1]]==0就将ans++。
既然这个处理的复杂度为O(n)明显优于主席树,那为什么不用这个处理询问呢?
因为他的询问次数也是n!!!!,这样O(n*n)的复杂度就T了,所以需要用另一种操作来分摊询问次数
且这个操作的复杂度不跟n有关
这里取的临界是(这里分块对于小的一块是用临界来代替n,大的一块是n/临界——大的部分出现的最大次数,来代替出现次数的n)
这样总的时间复杂度是
把常数拿掉的话总的复杂度就接近于
上面是题解给的临界,可以1s以内过,大概在0.7-0.8左右
我测试了的临界,需要1.2s左右,虽然时间复杂度算出来还小一点,
但是我感觉大概是因为临界变大了,跟多的样例进入到小的那部分(),而小的那部分的复杂度比大的那部分()
还大,这样临界增大,小的那部分复杂度增大,并且进入的样例更多了,所以总的时间增加了。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <cmath>
using namespace std;
const int MAXN = 1e5+10;
typedef struct node
{
int l,r;
}node;
node a[MAXN];
int rt[MAXN*60],ls[MAXN*60],rs[MAXN*60];
int sum[MAXN*60];
int x[MAXN];
int prime[MAXN],s[MAXN],tot;
bool cmp(node a,node b)
{
if(a.l==b.l) return a.r<b.r;
return a.l<b.l;
}
void build(int& root,int l,int r)
{
root=++tot;
sum[root]=0;
if(l==r) return;
int mid=(l+r)>>1;
build(ls[root],l,mid);
build(rs[root],mid+1,r);
}
void update(int& root,int l,int r,int last,int x)
{
root=++tot;
ls[root]=ls[last];
rs[root]=rs[last];
sum[root]=sum[last]+1; //从上往下更新主席树的节点的值
if(l==r) return ;
int mid=(l+r)>>1;
if(mid>=x) update(ls[root],l,mid,ls[last],x);
else update(rs[root],mid+1,r,rs[last],x);
}
int query(int ss,int tt,int s,int e,int l,int r)
{
if(s<=l&&r<=e)
{
return sum[tt]-sum[ss];
}
int mid=(l+r)>>1;
int ans=0;
if(mid>=s)
ans+=query(ls[ss],ls[tt],s,e,l,mid);
if(mid<e) //!该区间是[mid+1,r]
ans+=query(rs[ss],rs[tt],s,e,mid+1,r);
return ans;
}
int main()
{
int t;
scanf("%d",&t);
prime[0]=1;
prime[1]=1;
for(int i=2;i<MAXN;i++)
{
if(!prime[i])
{
for(int j=i+i;j<MAXN;j+=i) //!
{
prime[j]=1;
}
}
}
while(t--)
{
tot=0;
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d%d",&a[i].l,&a[i].r);
sort(a+1,a+1+n,cmp);
build(rt[0],1,n);
for(int i=1,j=1;i<=n;i++)
{
rt[i]=rt[i-1];
for(;j<=n&&a[j].l<=i;j++)
{
update(rt[i],1,n,rt[i],a[j].r);
}
}
//int crisis=sqrt(n/log(n))*0.8; //分块根据影响时间复杂度的量进行分块
int crisis=sqrt(n); //上面是题解的边界,这个是我自己试的边界
int m,y;
scanf("%d",&m);
int ans=0;
for(int i=1;i<=m;i++)
{
ans=0;
scanf("%d",&y);
for(int j=1;j<=y;j++)
scanf("%d",x+j);
sort(x+1,x+y+1); //!!不然下面求前缀的时候会出错
x[0]=0;x[y+1]=n+1;
if(y>crisis)
{
s[0]=0;
for(int j=1, k=1;j<=n;j++)
{
s[j]=s[j-1];
for(;k<=y&&x[k]<=j;k++)
{
s[j]++;
}
}
for(int j=1;j<=n;j++)
{
if(prime[s[a[j].r]-s[a[j].l-1]]==0)
ans++;
}
}
else
{
//构造包含点的个数为质数个的区间
for(int j=1;j<=y;j++) //左端点
{
for(int k=j+1;k<=y;k++) //右端点
{
if(prime[k-j+1]==0)
{
ans+=query(rt[x[j-1]],rt[x[j]],x[k],x[k+1]-1,1,n); //上面边界设置,左端点是(x[j-1],x[j]],右端点是[x[k],x[k+1])
}
}
}
}
printf("%d\n",ans);
}
}
return 0;
}