P3755 [CQOI2017] 老C的任务 题解
前言:这个分块和刚被撤下的不同,因为这个分块时间复杂度正确,能通过所有 hack。
有没有什么可以不用离线都能解决问题的简单算法?答案是分块!!
60pts
首先遇到这个题目,先写一个比较暴力的
代码:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
struct node
{
int x;
int y;
int p;
}a[N];
int cmp(node x,node y)
{
return x.x == y.x?x.y<y.y:x.x<y.x;//排序规则
}
signed main()
{
int n,m;
scanf("%d %d",&n,&m);
for(int i = 1;i<=n;i++)
{
scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].p);
}
sort(a+1,a+n+1,cmp);//排序
for(int i = 1;i<=m;i++)
{
int x1,y1,x2,y2;
scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
int l = 1,r = n,num = 0;
while(l<=r)
{
int mid = l+r>>1;
if(a[mid].x>=x1)//找到左端点
{
num = mid;
r = mid-1;
}
else
{
l = mid+1;
}
}
l = 1,r = n;
int num1 = 0;
while(l<=r)
{
int mid = l+r>>1;
if(a[mid].x<=x2)//找到右端点
{
num1 = mid;
l = mid+1;
}
else
{
r = mid-1;
}
}
long long sum = 0;//这里得开long long
for(int i = num;i<=num1;i++)//遍历
{
int t = a[i].y;
if(t>=y1&&t<=y2)//如果满足
{
sum+=a[i].p;//加上这个基站
}
}
printf("%lld\n",sum);
}
return 0;
}
由于特殊的时间限制,我们获得了
100pts
遇到这种题,正解并不好想,考虑优化,复杂度瓶颈在于二分完后的遍历,于是我思来想去都没有想出做法,最终在 arrowpoint 这位大佬的指点下茅塞顿开,AC 了此题,他是怎么想的呢,分块!我们依旧将块长调整为
所有的流程大概说了一遍,但这题的细节很多,仅根据上面说的写代码是 A 不了的,现在大概说一下分块的一些公式和注意事项(个人原创):
- 求第
x 个块的左端点和右端点,设len 为块长,n 为区间长度,则第x 个块的左端点为(x-1) \times len+1 ,右端点为\min(x \times len,n) 。这里重点说为什么右端点不是x \times len ,而是\min(x \times len,n) ,因为当n 并不是完全平方数时,那这时就会发生len<\sqrt{n} ,k>\sqrt{n} ,k 指的是最大的块编号,这个时候第k 个块的长度绝对小于len ,所以使用k \times len 是有可能发生越界行为的。 - 求编号为
x 的数在块长为len 时所在的块编号为\lfloor \frac{x+len-1}{len} \rfloor 。它其实等价于\lceil \frac{x}{len} \rceil 的,如果不懂就在草稿本上分类讨论一下就能明白了。 - 处理散块时当询问的左端点
l 和右端点r 所在同一个块时,直接从l 遍历到r ,否则把l 这个块从l 到l 当前这个块结尾的编号全部遍历一遍,和从r 当前这个块的开头的编号到r 全部遍历一遍。
说完这些之后,再说本题的注意事项:
- 十年 OI 一场空,不开 long long 见祖宗!
- 程序里的二分(对于这道题),可能会有左端点找不到,或者右端点找不到,再或者左端点虽然满足大于等于询问的左端点,但却大于询问的右端点,也或者右端点虽然满足小于等于询问的右端点,但却小于询问的左端点,这些情况都说明对于这个询问,找不到满足询问要求的数,此时这次询问(或求值)答案(或贡献)已经确定为
0 ,无需继续查下去。
讲这么详细,写代码应该没问题了,这里直接放代码了(当然,会有注释):
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
struct node
{
int x;
int y;
int p;
}a[N];
int cmp(node x,node y)
{
return x.x == y.x?x.y<y.y:x.x<y.x;//照样排序
}
struct node1
{
int y;
int p;
}s[N],s1[N];
long long sum[N];//前缀和得开long long
int id[N];
int cmp1(node1 x,node1 y)//这个是整块的排序
{
return x.y<y.y;
}
signed main()
{
int n,m;
scanf("%d %d",&n,&m);
int len = sqrt(n);//记录块长
for(int i = 1;i<=n;i++)
{
scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].p);
id[i] = (i+len-1)/len;//计算所在块编号
}
sort(a+1,a+n+1,cmp);
for(int i = 1;i<=n;i++)
{
s[i].y = a[i].y;//设置
s[i].p = a[i].p;//设置
s1[i] = s[i];//设置
}
for(int i = 1;i<=id[n];i++)
{
sort(s1+(i-1)*len+1,s1+min(i*len,n)+1,cmp1);//排个序
}
for(int i = 1;i<=n;i++)
{
sum[i] = sum[i-1]+s1[i].p;//求个前缀和
}
for(int i = 1;i<=m;i++)
{
int x1,y1,x2,y2;
scanf("%d %d %d %d",&x1,&y1,&x2,&y2);
int l = 1,r = n,num = 0;
while(l<=r)
{
int mid = l+r>>1;
if(a[mid].x>=x1)//这边寻找第一维的左端点
{
num = mid;
r = mid-1;
}
else
{
l = mid+1;
}
}
if(!num||a[num].x>x2)//如果找不到或者不满足条件
{
printf("0\n");
continue;
}
l = 1,r = n;
int num1 = 0;
while(l<=r)
{
int mid = l+r>>1;
if(a[mid].x<=x2)//找第一维右端点
{
num1 = mid;
l = mid+1;
}
else
{
r = mid-1;
}
}
if(!num1||a[num1].x<x1)//如果找不到或者不满足条件
{
printf("0\n");
continue;
}
long long ss = 0;//得开long long
for(int i = id[num]+1;i<=id[num1]-1;i++)//求整块
{
int l = (i-1)*len+1,r = i*len,num2 = 0;
while(l<=r)
{
int mid = l+r>>1;
if(s1[mid].y>=y1)//在整块中二分找到第二维的左端点
{
num2 = mid;
r = mid-1;
}
else
{
l = mid+1;
}
}
if(!num2||s1[num2].y>y2)//同上
{
continue;
}
l = (i-1)*len+1,r = i*len;
int num3 = 0;
while(l<=r)
{
int mid = l+r>>1;
if(s1[mid].y<=y2)//在整块中二分找到第二维的右端点
{
num3 = mid;
l = mid+1;
}
else
{
r = mid-1;
}
}
if(!num3||s1[num3].y<y1)//同上
{
continue;
}
ss+=sum[num3]-sum[num2-1];//求左端点到右端点的和
}
if(id[num] == id[num1])//如果在同一个块
{
for(int i = num;i<=num1;i++)//直接遍历
{
if(s[i].y>=y1&&s[i].y<=y2)
{
ss+=s[i].p;
}
}
}
else
{
for(int i = num;i<=id[num]*len;i++)//分两次,第一次
{
if(s[i].y>=y1&&s[i].y<=y2)
{
ss+=s[i].p;
}
}
for(int i = (id[num1]-1)*len+1;i<=num1;i++)//第二次
{
if(s[i].y>=y1&&s[i].y<=y2)
{
ss+=s[i].p;
}
}
}
printf("%lld\n",ss);//输出
}
return 0;
}
时间复杂度:
效率还是不错的,估计卡常一下应该能在一秒内卡过,不过懒得弄了。
如果还有不会的地方,欢迎私信!!
附分块学习网址:这里。