重现赛链接 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语言基础比较好、会算复杂度、并且细心的同学,天时地利人和的话可以很容易弄出来。(这里有个语言上的坑点:就是移位运算'>>'比四则运算优先级低,别忘了加括号)
然后求这个区间上的最大值和最小值。
由运算结果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;
参赛者可能会思维惯性忘记了其中一半的判断,但是题目最后一句描述的清清楚楚,描述本身并没有问题。
还有:请仔细检查一下代码,尤其要注意位运算符的优先级要低于四则运算。
这个题要是数据量再小一点,暴力可以过的话就太没意思了。
我们知道区间求最值复杂度(用最简单的方法)为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)多求一个值并不会增加太多的代码量,而第二种方法(线段树)要多维护一棵树(还是稍微麻烦一点)。考虑到基本是只有集训队的人才会写线段树,所以为了坑一下他们就..........手动捂脸。
代码略