2019xupt-acm校赛 题解(C.给你一个666)by出题组tongtong

重现赛链接 2019 ACM ICPC Xi'an University of Posts & Telecommunications School Contest

前面的话

有幸参与2019XUPT-ACM校赛出题和裁判工作。过程还是蛮有意思的。

转载请注明出处和链接。

正文

                                     

                                 C-给你一个666  (1s)

Description

Tongtong非常喜欢用“say 666”的方式来打招呼,因此热爱数学的他找到了一个说666的新方式。Tongtong构造了一个数学上很6的运算。定义一个6位二进制数上的运算 @ : a@b=(c,d)。其中 c = a的高3位*b的低3位 ; d = a的低3位*b的高3位。例如 010 001 @ 011 001 = (010*001 , 001*011) = (2*1,1*3) = (2,3) 。

Tongtong给出了两个操作数a和b。以及一个数列 x1,x2,x3 ... xn ,假设a@b的结果(c,d),Tongtong非常关心数列在区间 [ min(c,d)*min(a,b) ,max(c,d)*max(a,b) ]上的最小值和最大值,Tongtong认为上述区间上的最大值和最小值可以代表666的程度,所以每组操作数都要计算出这两个最值。由于时间紧迫,他需要你来帮助他完成这个工作。

Input

第一行输入两个正整数 n,q,分别表示数列数字的个数和询问个数.其中1<=n<=50 000,1<=q<=100 000。

第二行输入n个非负整数,表示数列中的元素x1,x2 ... xn, 每个元素都在int类型的范围内。

接下来q行,每行给出一对非负整数,a,b,其意义见题面。本题保证所有的a和b均为6位无符号整数。

Output

对于每个询问,输出一对整数,分别表示目标区间上的最大值和最小值.每个询问的结果单独占一行。

请不要输出多余的空行。

Sample Input

12 1

5 2 3 4 5 6 7 8 1 6 5 1

1 8

Sample Output

8 2

 

Hint

min(x,y)表示x和y的最小值, max(x,y)表示x和y的最大值.区间下标从1开始。

样例:

数列在区间[1,8]上的所有元素为{5 2 3 4 5 6 7 8},最大值为8,最小值为2。

若左边界越界则取1,若右边界越界则取n。

一些废话:

当时出题时满脑子都是骚操作。想坑害一下xupt-acm集训队的成员们。想构造一个题:让集训队的陷入惯性思维(啰啰嗦嗦写一堆代码),让其思维能力强的大一大二同学们可以用简单方法做出来(简简单单一会儿就写完代码)。

然后这个题就出炉了。

虽然最终也没有几个能解出来的,包括集训队成员也是很晚才过题。主要是赛场压力大,比赛题量多导致的。

题目大意和思路:

题目输入了两个无符号整数a和b,这两个数换算成二进制后都保证是6位无符号整数。

然后定义了对这两个数的一个相对比较复杂的运算,算出两个数c和d,并进一步得到一个区间[L,R]

这个运算虽然复杂,但是c语言基础比较好、会算复杂度、并且细心的同学,天时地利人和的话可以很容易弄出来。(这里有个语言上的坑点:就是移位运算'>>'比四则运算优先级低,别忘了加括号)

然后求这个区间上的最大值和最小值。

对于wrong answer(WA)的同学:

由运算结果c和d求区间[L,R]时有一个巨坑

若左边界越界则取1,若右边界越界则取n。

这里的“越界”并没有特指“下越界”和“上越界”,所以应当同时包含了这两个情况。

这里在裁判的时候回答过参赛者的问题:

左边界超过n也取1吗?

当时很不想回答这个问题,因为其他参赛者看到这个题就会想到这个特殊情况,有点算违规提问吧。事实证明这个童鞋提问之后,一直答案错误的大赛第一名立即就过了这个题,可能是受到了提示。

应该是这个意思:

越界包括上越界和下越界。

很多同学对L进行判断时,只判断了L小于1的情况,没有判断L大于n的情况。

也就是说正解应该是:

if(L<1 || L>n)     L=1;

if(R<1 || R>n)   R=n;

参赛者可能会思维惯性忘记了其中一半的判断,但是题目最后一句描述的清清楚楚,描述本身并没有问题。

还有:请仔细检查一下代码,尤其要注意位运算符的优先级要低于四则运算。

对于Time limit error(TLE)的同学:

这个题要是数据量再小一点,暴力可以过的话就太没意思了。

我们知道区间求最值复杂度(用最简单的方法)为O(n)

这里有1e5个询问,每次询问区间最长为4096(至于为什么后面会将),这样如果每次询问都要扫描一遍数列求最值的话,极端情况下计算量为 1e5 * 4096=  4e8,1秒基本就超时了

暴力是别想着过了。

我甚至还看到更过分的代码:每次询问都把数列对应的区间进行排序,然后得到最值。单单区间求最值的复杂度就高达O(nlogn),还不如上面扫一遍O(n)的优越,更过不了了。

 

所以官方题解有两种思路:

第一种标准程序(标程std)的思路:

这种思路代码量非常非常少。

这是给没有算法基础的同学预留的解题思路。相比于线段树、树状数组这种高级数据结构来说算是一条活路吧,但由于本人对难度把握有些欠缺,还是出难了,几乎没人想得到这种方法(比赛时见过一个类似方法的代码,不过没来得及细看具体实现是不是这个思路)

我们观察题目的运算:

由a和b两个6位整数运算出一个区间,然后计算最值。给出一组a和b就一定能确定唯一的区间。而且整个数列是全程没有没修改的。

那么:6位二进制可以有多少种情况?自然是:  2的6次方=64种情况,那两个6位二进制数一共有多少种情况呢? 那就是64*64=4096种情况。也就是不同区间[L,R]的个数不超过4096个————————关键点1

也就是输入的区间[L,R]最多有四千多种情况。而题目的询问有高达5万个询问。那是不是肯定有大量的重复询问呢?

所以当我们第一次回答某个区间[L,R]上的最值时,通过最简单暴力的方法扫描该区间得到答案,输出,并且记录下来,以后再次遇到这个询问的时候就可以直接查表,不需要重新扫描数组。

(标程是直接提前枚举所有的a和b,然后询问之前就把答案算出来,然后询问就直接查表不扫描数列。)

再观察一下区间[L,R]的生成公式: [ min(c,d)*min(a,b) ,max(c,d)*max(a,b) ] ,6位二进制最大值为:63,两个6位二进制乘起来的范围不超过:0~64*64  =>  【0,4096】,也就是说[L,R]的区间长度不超过4096————————关键点2

这样复杂度又是多少呢?

关键点1可知:最多扫描数列的次数为:4096次

关键点2可知:每个区间扫描一遍求最值,扫描长度不超过4096

也就是说,回答询问之前我们枚举所有的[L,R]区间,对于每个区间暴力求解区间最值,保存到内存中,然后回答q次询问,每次询问直接查表不重新计算最值。

复杂度  <=  区间个数 * 区间长度 + 询问次数  <  4096*4096  + 1e5  <  2e7

而实际情况下没有这么多运算量,标程只跑了不到100+ms就运行完毕了。所以比赛设置1s的时间是完全够用的。

当然,这种思路在时间上并没有达到理论极限,还可以继续优化不过代码会变复杂,没必要。

参考代码:

#include
#include
#include
using namespace std;
int n,q;
void checkl(int &x)
{
    if(x<=0 || x>n)
        x=1;
}
void checkr(int &x)
{
    if(x>n || x<=0)
        x=n;
}

void func(int a,int b,int &x,int &y)
{
    int ah,al;
    int bh,bl;
    ah=a>>3;
    al=a-(ah<<3);//这里一定要加括号 
    bh=b>>3;
    bl=b-(bh<<3);
    x=ah*bl;
    y=al*bh;
    if(x>y)
    {
        swap(x,y);
    }
    x*=min(a,b);
    y*=max(a,b);
    checkl(x); //既要判上限也要判下限
    checkr(y);
}
int const maxn=1e6+5;
int co[maxn];
int bookMax[5005][5005];
int bookMin[5005][5005];
int findMax(int l,int r)
{
    int maxv=-1;
    for(int i=l;i<=r;i++)
    {
        maxv=max(maxv,co[i]);
    }
    return maxv;
}
int findMin(int l,int r)
{
    int minv=INT_MAX;
    for(int i=l;i<=r;i++)
    {
        minv=min(co[i],minv);
    }
    return minv;
}
int main()
{

    int l,r;
    scanf("%d %d",&n,&q);
    //cin>>n>>q;
    for(int i=1;i<=n;i++)
    {
        scanf("%d",co+i);
    }
    for(int i=0;i<=64;i++)
    {
        for(int j=0;j<=64;j++)
        {
            func(i,j,l,r);
            bookMax[l][r] = findMax(l,r);
            bookMin[l][r] = findMin(l,r);
        }
    }
    int a,b,x,y;
    while(q--)
    {
        scanf("%d%d",&a,&b);
        func(a,b,l,r);
        printf("%d %d\n",bookMax[l][r],bookMin[l][r]);
    }

}

第二种思路:高级数据结构

我看到集训队成员在赛场上基本就是用的线段树(树状数组同理)。

线段树初始化时间复杂度为O(nlogn),查询区间最值的复杂度为O(logn),这样:

log( 5e4)<11

复杂度=建树 + 每次查询所需要的时间*查询次数 < 5e4*11  + 11 *1e5  < 2e6

当然了,其他可以求区间最值的高级数据结构/算法都可以。

多说一句:求最大值和最小值的方法都是一样的,其实求其中一个就可以了。

不过:第一种方法(std)多求一个值并不会增加太多的代码量,而第二种方法(线段树)要多维护一棵树(还是稍微麻烦一点)。考虑到基本是只有集训队的人才会写线段树,所以为了坑一下他们就..........手动捂脸。

代码略

 

 

 

你可能感兴趣的:(ACM)