洛谷P3755 [CQOI2017] 老C的任务 题解

前言:这个分块和刚被撤下的不同,因为这个分块时间复杂度正确,能通过所有 hack。

题目传送门。

有没有什么可以不用离线都能解决问题的简单算法?答案是分块!!

60pts

首先遇到这个题目,先写一个比较暴力的 \(O(mn)\) 的算法,先排序,降掉一维,剩下一维询问时直接两个二分找到左端点和右端点,然后遍历从左端点到右端点有多少个数满足在第二维的范围内,求和即可。

代码:

#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;
}

由于特殊的时间限制,我们获得了 \(60\) 分的好成绩。

100pts

遇到这种题,正解并不好想,考虑优化,复杂度瓶颈在于二分完后的遍历,于是我思来想去都没有想出做法,最终在 arrowpoint 这位大佬的指点下茅塞顿开,AC 了此题,他是怎么想的呢,分块!我们依旧将块长调整为 \(\sqrt{n}\),新建两个块长数组 \(s\)\(s1\)\(s\) 处理的是散块,\(s1\) 处理的是整块,为什么要这样呢?因为我们首先要明白,分块为什么比暴力快?原因很简单,它对于整块有整体处理,这样的话对于每次询问区间 \([l,r]\) 内的各种信息,都可以遍历 \([l,r]\) 内的整块,最坏时间为 \(O(\sqrt{n})\),因为最多只会有 \(\sqrt{n}\) 个块,而每个块都有现成的信息,这样一来每个块都只需要 \(O(1)\) 的时间就能知道这个块的信息,那总时间就是 \(O(1 \times \sqrt{n}) = O(\sqrt{n})\),然后剩下的就是散块,由于散块最多两个,而散块的遍历每个块需要 \(O(\sqrt{n})\) 的时间求信息,所以总时间就是 \(O(2 \times \sqrt{n}) = O(\sqrt{n})\),那么整个程序整体复杂度为 \(O(m\sqrt{n})\)。这里的时间复杂度是忽略常数,所以 \(O(2 \times \sqrt{n}) = O(\sqrt{n})\)。知道了分块速度快的原因,所以说这里为什么要搞两个分块数组?因为散块不需要什么处理,不需要任何辅助加快时间的东西,自然就啥也不用动,但是整块就不一样了,它需要预处理出一些信息,对于这个题,arrowpoint 认为可以将整块处理的基本信息先排个序,因为反正都要算整个块,块里面的数怎么排序都无所谓,然后对于每个整块按照询问中的第二维二分出左右端点,然后求和就行,求和用前缀和优化,这样一来,与处理时间复杂度为 \(O(\sqrt{n} \times (\sqrt{n} \times \log \sqrt{n}))=O(n \log \sqrt{n})\)\(\log \sqrt{n}\) 是个常数,大约为 \(9\),所以说可以默认预处理时间复杂度为 \(O(n)\)。上面说完整块后,散块也就很简单了,直接将两个或一个散块全部遍历一遍,判断是否满足询问条件就行了。

所有的流程大概说了一遍,但这题的细节很多,仅根据上面说的写代码是 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-1}{len} \rfloor+1\)。它其实等价于 \(\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-1)/len+1;//计算所在块编号
    }
	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;
}

时间复杂度:\(O(n \log \sqrt{n}+m \sqrt{n} \times \log \sqrt{n})\)。在 3s 内通过绰绰有余。

效率还是不错的,估计卡常一下应该能在一秒内卡过,不过懒得弄了。

如果还有不会的地方,欢迎私信!!

附分块学习网址:这里。

posted @ 2025-02-11 22:22  林晋堃  阅读(16)  评论(0)    收藏  举报