orz dls AK IOI2021
Warning: 此题非常有(du)趣(liu)
题目大意:你有 个长度为 位的寄存器,一开始 号寄存器里存了 个 位的数,你要通过寄存器的复制、赋值、按位与、按位或、按位异或、按位取非、左移、右移、加法操作实现对这些数进行求最小值或排序。
(太长不看版可以直接跳到文末的步骤总结。)
这是今年(2021年)IOI的d2t3,我甚至不知道该怎么把它分类……这显然是个非传统题,也许可以勉强算个交互,但是这一点也不“交互”好吧,因为你除了最开始的参数以外根本不会再从交互库获得任何信息……也许这可以算作是类似于提交答案里的造计算机题(就像NOI2016 d2t3那样的)?
说实话我看到这个题的第一反应是懵逼的,倒不是因为它很难或者很非主流之类的(话说非主流不是ioi的常规操作吗),而是……喂!为什么我之前见过几乎一模一样的东西!
这个名叫“寄存器”的东西配上这一堆运算,反正我第一眼想到的不是寄存器,实际上看到前面一堆位运算操作的第一反应应该是个类似bitset之类的玩意,直到最后发现居然还有加法,那这其实就是个高精度(大整数)啊。
我们都知道计算机对于高精度的运算当然不是,而是跟数据位数有关的,然而这个题的评判标准恰恰是只看操作次数(虽然高精度有位的上限,但是下面会看到这对于这道题来说是相当充裕的),也就是说(至少在这道题的数据范围下)我们获得了一个可以看作是单次运算的高精度!
虽然支持的运算数比较有限(比如不支持乘法除法取模之类的),不过对于这道题而言已经足够了。
你一定以为这个把题设看作高精度的模型是我脑洞大开想出来的?当然不是,著名的matrix67大牛早在2009年就写过一篇类似的博文。“巧的是”,这篇文章说的恰好就是假设高精度运算都是的情况下的排序算法。
我了个乖乖,IOI出原题!
在那篇文章以及后续的一篇文章里,作者一共介绍了两种算法以及(没错,你没看错)一种算法。当然后两种算法因为题设限制都用不了(下文会说这是为什么),我们重点来看第一种算法:(以下引用自原博文)
1. 如何用位运算来取绝对值?
2. 给出两个正整数a, b,不用比较运算和判断语句如何把小数赋给a,大数赋给b?
提示:和加差除以2等于大数,和减差除以2等于小数3. 如何利用位运算把整数序列编码成一个超大整数?
例如把(二进制数)11, 1011, 1110, 1编码为一个数00011 01011 01110 000014. 如何用位运算给超大整数中的所有数同时取绝对值?
5. 给出两个超大整数a, b,不用比较运算和判断语句如何把对应位置上的小数赋给a的对应位置,大数赋给b的对应位置? 例如把
a = 000010 000111 000100 001001
b = 000001 001011 000011 011111
变成
a = 000001 000111 000011 001001
b = 000010 001011 000100 0111116. 如何实现奇偶移项排序?
最后,由于奇偶移项排序只有O(n)层,因此整个算法是O(n)的。
信息量比较大让我们一条一条来看:
1. 如何用位运算来取绝对值?
作者在文中给出的链接里其实已经给出了操作方法:
设为位补码表示的整数,则
看起来很复杂 ,我们拆开一点一点看:
取出符号位,对其进行取反再的操作相当于取负,此时如果原先为负,则符号位为,取反再之后变成,也就是补码的;而若原先非负,得到的是。
接下来用异或那个数,效果是如果为负就取反,否则不变。
最后一步,相当于如果为负就,否则不变。
折腾下来,如果如果非负,我们相当于什么也没做;否则会对取反再 ,相当于取负。
2. 给出两个正整数a, b,不用比较运算和判断语句如何把小数赋给a,大数赋给b?
提示:和加差除以2等于大数,和减差除以2等于小数
这道题里没有减法操作,可以用加上相反数(原数取反再)来代替。
3. 如何利用位运算把整数序列编码成一个超大整数?
例如把(二进制数)11, 1011, 1110, 1编码为一个数00011 01011 01110 00001
这道题的输入已经帮我们搞定了。
4. 如何用位运算给超大整数中的所有数同时取绝对值?
假设大整数里压了个位补码表示的整数。
首先取符号位,我们先取,对于一个整数来说这就已经是符号位了,但是多个整数压在一起的情况还有很多多余的位(因为只有一个整数的话,更低位已经在右移操作中抹去了,但是多个整数的情况会把上一个整数的低位移到下一个整数管辖的范围内),我们接下来要把多余的位“清理干净”:只需要按位与一个(每个整数的最低位是,其余全)即可,我们把用到的那个数记作。
然后正常进行其他操作即可,注意操作的这个“”也应当是上面这个。
当然在实际执行时,这个压位后的数可能有一些没用的“垃圾位”,这时取反操作就用按位与一个(在实际表示数的位置全其余全)来代替即可,我们把这个数记作。
为了方便起见,将这种“对中所有数取绝对值”的操作记作。
5. 给出两个超大整数a, b,不用比较运算和判断语句如何把对应位置上的小数赋给a的对应位置,大数赋给b的对应位置? 例如把
a = 000010 000111 000100 001001
b = 000001 001011 000011 011111
变成
a = 000001 000111 000011 001001
b = 000010 001011 000100 011111
当然会有一个小问题:这个操作可能会让值溢出。不过我们不妨暂时不考虑这一点(实际上后面有解决办法)。
6. 如何实现奇偶移项排序?
奇偶移项排序?这是什么船新的排序算法?为什么不用我们熟悉的快排/归并/冒泡/......?
相信你看到这个词的第一瞬间跟我一样是一脸懵逼的。
作者在原文中给出了一篇文章的链接,我们顺着点进去会发现一个更懵逼的词汇:“排序网络”。
这是个啥呢?说白了就是我们要对一个长为的序列进行排序,我事先安排好一堆比较操作(就是形如把和 进行比较,把小数赋给大数赋给),要求这些比较操作是在我见到具体的序列之前就定好的!
于是你会发现,在这个设定下很多排序算法都不能用了——比如归并排序、快速排序等(事实上我现在知道的所有基于次比较的排序算法貌似都跪了……)。同时一些排序算法仍然坚挺,比如冒泡排序。
为什么要提这个看起来就很自废武功的设定呢?因为我们还有另一个大杀器——并行算法。
因为排序网络中所有的比较操作是与数据无关的,我们就可以事先开一堆线程,然后告诉每个线程你应该在什么时间进行哪个比较,说白了我们可以把不相干的比较一起进行(比如与的比较就能和与的比较同时进行),于是时间复杂度就变成了排序网络的总“层数”。
由此思想,我们可以设计出很多并行排序算法:
首先,冒泡排序显然可以改造为并行排序——每轮冒泡进行到第次时,同时开启下一轮的冒泡就行了。虽然它的总比较次数是级别,但并行之后可以变成的时间复杂度。
其次是奇偶移项排序,它会在第奇数层同时进行所有下标为奇数的位置跟其后面一个数的比较,第偶数层同时进行所有下标为偶数的位置跟其后面一个数的比较,这样重复次即可。
它的正确性在上面那篇文章里有证,这里略去。时间复杂度仍为,但是常数略优秀一些。
(ps:这个题其实由于上界很松,写一个奇偶移项排序就能过了)
更优秀的排序算法还有很多,有许多需要次比较的排序算法都可以改造为排序网络,并通过并行计算将时间复杂度将为。这里介绍一种“奇偶归并排序”(就是上面那篇文章最后提到的)。
首先,如果不是的幂,就向上补成的幂,这对复杂度没有影响。以及下面假设下标从开始。
然后进行递归,将左侧和右侧分别递归地进行排序。显然两侧的排序过程可以直接并行。
然后进行合并,操作如下:
首先,将下标按奇偶进行分类,发现每一类其实都是一个合并的子问题,于是递归下去。这两个子问题可以直接并行。
最后塞回原序列中,可以证明只需要在所有下标为奇数的位置和它的后一个位置进行比较即可完成排序。具体证明详见原文。
总比较次数为,但是由于并行,实际上只需要次并行操作。
最后,我们再把上面所有的内容串起来——前文的步骤5恰好就是利用“大整数计算”这个性质,完成一次并行比较!
而对于一个序列和排序网络的一层,我们也可以设计一套大整数运算进行并行比较:需要注意到上面介绍的几种算法中,同一层之间的各次比较的两数下标的间隔是相等的。这有利于我们直接用位运算进行操作。
首先用按位与把需要比较的和分别取出来,得到两个大整数记作和。接下来,设这个相等的间隔是,则把右移位,以使得需要比较的两个数在同一位置。然后用步骤5进行比较,比较完后再左移回去然后塞回原序列即可。
于是我们就可以利用大整数运算实现一个并行排序算法,而实现这个算法需要的一切运算,都是我们的题设所允许的。
另外,如果只是需要找全局最小值,只需要每次对于把和进行比较,每次都能扔掉一半的数,最后只需要次并行比较即可。
最后填上前文的几个坑:
1、为什么另外两种利用大整数运算实现比较的算法(尤其是那种算法)不能用?
其中一种算法是把大整数当成数组来跑计数排序,你在不知道序列的前提下显然玩不了;另一种就非常遗憾了——题目中给的还不够大,如果给到说不定能试试看(手动/斜眼笑)。
2、怎么解决在用大整数同时比较很多对数时的溢出问题?
一个可行的操作是“拓宽字长”,即将原先位的字长变成位,其中还要留出最高位作为符号位(输入数据由于都非负所以是没有符号位的)。然后在每两个数中间再空出位防止上一个数的运算结果溢出到下一个数中。
这个操作以及最后输出时的“压缩字长”的逆操作并不难实现,先将后一半数统一左移若干位,再递归(其实是并行)地处理左右两边即可。只需要次操作。
虽然操作不多,但对于只有次操作限制的求最小值任务来说还是很致命的。幸好我们仔细分析,发现由于这时的比较都是每个位置和下一个位置比,在实际比较时已经将下一个位置取出并右移,此时每个数前面其实至少空出了一个数的空隙,因此“拓宽字长”操作是不必要的。
3、感觉这一堆又是减法又是取绝对值的操作好烦啊,有没有更给力一点的?
有一个matrix67没有提到的办法可以让比较操作简单很多:取的符号位之后取反再,此时若得到的是,反之是,将其记作,再将按位取反得到。接下来,只需要取就可以得到较小值,可以得到较大值。
这样的一次比较只需要多次基本运算,如果只需要取最小值的话只用十几次。
最后,让我们总结一下全部步骤:
1、(s=1时)用O(log n)次操作进行“拓宽字长”;
2、建立比较网络(s=0时基于每次减半,有O(log n)层;s=1时基于奇偶归并排序,有O(log^2 n)层);
3、依次执行每层比较网络:
3.1 用位运算取出需要比较的数并对齐;
3.2 用上面提到的步骤5的改进版进行比较操作;
3.3 用位运算将比较后的数塞回原序列。
4、(s=1时)用O(log n)次操作进行“压缩字长”。
代码如下(这一版代码两个任务的最大询问次数分别为次左右和次左右,可以进行一定的常数优化):
#include<bits/stdc++.h>
using namespace std;
#include "registers.h"
#define pb push_back
#define amv append_move
#define afz append_store
#define aand append_and
#define aor append_or
#define axor append_xor
#define anot append_not
#define ashl append_left
#define ashr append_right
#define aadd append_add
int s,n,k,lgo,kk,k0,zc[1010],b = 2000;
vector<int> wz[1010];
vector<bool> tp;
void bj(int id){
int i,j,x,zz = zc[id];
sort(wz[id].begin(),wz[id].end());
for(i = 0;i < b;++i) tp[i] = 0;
for(i = 0;i < wz[id].size();++i){
x = wz[id][i];
for(j = 0;j < k0 + (!s && zz != 1);++j) tp[x * kk + j] = 1;
}
afz(99,tp);
for(i = 0;i < b;++i) tp[i] = 0;
aand(1,99,0);
axor(0,1,0);
if(s){
amv(89,0);
ashl(90,99,zz * kk);
aand(0,0,90);
axor(89,89,0);
}
else if(zz == 1){
for(i = 0;i < wz[id].size();++i){
x = wz[id][i];
for(j = 0;j < k0 + 1;++j) tp[x * kk + j] = 1;
}
afz(99,tp);
for(i = 0;i < b;++i) tp[i] = 0;
}
ashr(0,0,zz * kk);
for(i = 0;i < wz[id].size();++i){
x = wz[id][i];
tp[x * kk] = 1;
}
afz(98,tp);
for(i = 0;i < b;++i) tp[i] = 0;
axor(2,1,99);
aadd(2,2,98);
aadd(3,2,0);
ashr(4,3,k0 - s);
aand(5,4,98);
axor(6,5,99);
aadd(7,6,98);
axor(8,7,99);
aand(9,7,0);
aand(10,8,1);
if(!s) aor(0,9,10);
else{
aadd(11,9,10);
aand(12,7,1);
aand(13,8,0);
aor(14,12,13);
ashl(15,14,zz * kk);
aor(0,11,15);
aor(0,0,89);
}
}
int merge(int dpt,int l,int r,int d2,int ym,int nw){
int nxt = nw,q1 = 1 << dpt - 1,q2 = 1 << d2;
if(dpt == d2 + 1){
++nxt;zc[nxt] = q1;wz[nxt].pb(l | ym);
return nxt;
}
nxt = merge(dpt,l,r,d2 + 1,ym,nw);
merge(dpt,l,r,d2 + 1,ym | q2,nw);
++nxt;zc[nxt] = q2;
for(int i = l + ym + q2;i + q2 <= r;i += q2 << 1) wz[nxt].pb(i);
return nxt;
}
int dfs(int dpt,int l,int r,int nw){
if(l == r) return nw;
int mid = l + r >> 1;
int nxt = dfs(dpt - 1,l,mid,nw);dfs(dpt - 1,mid + 1,r,nw);
return merge(dpt,l,r,0,0,nxt);
}
void construct_instructions(int _s, int _n, int _k, int _q){
if(_n == 1 || !_k) return;
s = _s;k = _k;
int i,j,l;
for(n = 1,lgo = 0;n < _n;n <<= 1,++lgo);
tp.clear();for(i = 0;i < b;++i) tp.pb(0);
for(i = 0;i < b;++i) tp[i] = (i / k >= _n && i / k < n);
afz(99,tp);
aor(0,99,0);
if(s == 0){
kk = k0 = k;
for(i = 0;i < lgo;++i){
zc[i] = 1 << i;
for(j = 0;j < n;++j) if(j % (1 << i + 1) == 0) wz[i].pb(j);
}
for(i = 0;i < lgo;++i) bj(i);
}
else{
kk = k + 4;k0 = k + 2;
for(i = lgo - 1;i >= 0;--i){
for(j = 0;j < b;++j) tp[j] = 0;
for(j = 0;j < n;++j) if(j & (1 << i)){
int nw = j * k;
for(l = lgo - 1;l > i;--l) if(j & (1 << l)) nw += (1 << l) * 4;
for(l = 0;l < k;++l) tp[nw + l] = 1;
}
afz(99,tp);
aand(1,99,0);
axor(0,1,0);
ashl(1,1,(1 << i) * 4);
aor(0,1,0);
}
int tot = dfs(lgo,0,n - 1,0);
for(i = 1;i <= tot;++i) bj(i);
for(i = 0;i < lgo;++i){
for(j = 0;j < b;++j) tp[j] = 0;
for(j = 0;j < n;++j) if(j & (1 << i)){
int nw = j * k;
for(l = lgo - 1;l >= i;--l) if(j & (1 << l)) nw += (1 << l) * 4;
for(l = 0;l < k;++l) tp[nw + l] = 1;
}
afz(99,tp);
aand(1,99,0);
axor(0,1,0);
ashr(1,1,(1 << i) * 4);
aor(0,1,0);
}
}
}