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},新建两个块长数组 ss1s 处理的是散块,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 不了的,现在大概说一下分块的一些公式和注意事项(个人原创):

说完这些之后,再说本题的注意事项:

讲这么详细,写代码应该没问题了,这里直接放代码了(当然,会有注释):

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

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

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

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

附分块学习网址:这里。