[loj3528] [IOI2021] 位移寄存器 - 交互(提答?) - 高精度 - 并行计算 - 排序算法

orz dls AK IOI2021

传送门:https://loj.ac/p/3528

Warning: 此题非常有(du)趣(liu)

题目大意:你有 m 个长度为 B 位的寄存器,一开始 0 号寄存器里存了 n 个 k 位的数,你要通过寄存器的复制、赋值、按位与、按位或、按位异或、按位取非、左移、右移、加法操作实现对这些数进行求最小值或排序。

(太长不看版可以直接跳到文末的步骤总结。)

这是今年(2021年)IOI的d2t3,我甚至不知道该怎么把它分类……这显然是个非传统题,也许可以勉强算个交互,但是这一点也不“交互”好吧,因为你除了最开始的参数以外根本不会再从交互库获得任何信息……也许这可以算作是类似于提交答案里的造计算机题(就像NOI2016 d2t3那样的)?

说实话我看到这个题的第一反应是懵逼的,倒不是因为它很难或者很非主流之类的(话说非主流不是ioi的常规操作吗),而是……喂!为什么我之前见过几乎一模一样的东西!

这个名叫“寄存器”的东西配上这一堆运算,反正我第一眼想到的不是寄存器,实际上看到前面一堆位运算操作的第一反应应该是个类似bitset之类的玩意,直到最后发现居然还有加法,那这其实就是个高精度(大整数)啊。

我们都知道计算机对于高精度的运算当然不是O(1),而是跟数据位数有关的,然而这个题的评判标准恰恰是只看操作次数(虽然高精度有2000位的上限,但是下面会看到这对于这道题来说是相当充裕的),也就是说(至少在这道题的数据范围下)我们获得了一个可以看作是单次运算O(1)的高精度!

虽然支持的运算数比较有限(比如不支持乘法除法取模之类的),不过对于这道题而言已经足够了。

你一定以为这个把题设看作高精度的模型是我脑洞大开想出来的?当然不是,著名的matrix67大牛早在2009年就写过一篇类似的博文。“巧的是”,这篇文章说的恰好就是假设高精度运算都是O(1)的情况下的排序算法。

我了个乖乖,IOI出原题!

在那篇文章以及后续的一篇文章里,作者一共介绍了两种O(n)算法以及(没错,你没看错)一种O(1)算法。当然后两种算法因为题设限制都用不了(下文会说这是为什么),我们重点来看第一种算法:(以下引用自原博文)

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. 如何实现奇偶移项排序

    

最后,由于奇偶移项排序只有O(n)层,因此整个算法是O(n)的。

信息量比较大让我们一条一条来看:

1. 如何用位运算来取绝对值

 作者在文中给出的链接里其实已经给出了操作方法:

xw位补码表示的整数,则|x| = (x \wedge(\thicksim(x >> (w - 1)) + 1)) + (x >> (w - 1))

看起来很复杂 ,我们拆开一点一点看:

x>>(w-1)取出符号位,对其进行取反再+1的操作相当于取负,此时如果原先x为负,则符号位为1,取反再+1之后变成-1,也就是补码的111...1;而若原先x非负,得到的是0

接下来用x异或那个数,效果是如果x为负就取反,否则不变。

最后一步,相当于如果x为负就+1,否则不变。

折腾下来,如果如果x非负,我们相当于什么也没做;否则会对x取反再+1 ,相当于取负。

2. 给出两个正整数a, b,不用比较运算和判断语句如何把小数赋给a,大数赋给b?
    提示:和加差除以2等于大数,和减差除以2等于小数

 \max(a,b)=(a+b+|a-b|)>>1

\min(a,b)=(a+b-|a-b|)>>1

这道题里没有减法操作,可以用加上相反数(原数取反再+1)来代替。

3. 如何利用位运算把整数序列编码成一个超大整数?
    例如把(二进制数)11, 1011, 1110, 1编码为一个数00011 01011 01110 00001

这道题的输入已经帮我们搞定了。

 4. 如何用位运算给超大整数中的所有数同时取绝对值?

 假设大整数b里压了nw位补码表示的整数。

首先取符号位,我们先取b>>(w-1),对于一个整数来说这就已经是符号位了,但是多个整数压在一起的情况还有很多多余的位(因为只有一个整数的话,更低位已经在右移操作中抹去了,但是多个整数的情况会把上一个整数的低位移到下一个整数管辖的范围内),我们接下来要把多余的位“清理干净”:只需要按位与一个00...0100...01......00...01(每个整数的最低位是1,其余全0)即可,我们把用到的那个数记作a_1

然后正常进行其他操作即可,注意+1操作的这个“1”也应当是上面这个a_1

当然在实际执行时,b这个压位后的数可能有一些没用的“垃圾位”,这时取反操作就用按位与一个......00...011...1......(在实际表示数的位置全1其余全0)来代替即可,我们把这个数记作a_2

为了方便起见,将这种“对b中所有数取绝对值”的操作记作exabs(b)

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

 a'=(a+b+exabs(a-b))>>1

b'=(a+b-exabs(a-b))>>1

当然会有一个小问题:这个a+b操作可能会让值溢出。不过我们不妨暂时不考虑这一点(实际上后面有解决办法)。

6. 如何实现奇偶移项排序

奇偶移项排序?这是什么船新的排序算法?为什么不用我们熟悉的快排/归并/冒泡/......?

 相信你看到这个词的第一瞬间跟我一样是一脸懵逼的。

作者在原文中给出了一篇文章的链接,我们顺着点进去会发现一个更懵逼的词汇:“排序网络”。

这是个啥呢?说白了就是我们要对一个长为n的序列进行排序,我事先安排好一堆比较操作(就是形如把a_ia_j (i < j)进行比较,把小数赋给a_i大数赋给a_j),要求这些比较操作是在我见到具体的序列之前就定好的!

于是你会发现,在这个设定下很多排序算法都不能用了——比如归并排序、快速排序等(事实上我现在知道的所有基于O(n \log n)次比较的排序算法貌似都跪了……)。同时一些排序算法仍然坚挺,比如冒泡排序。

为什么要提这个看起来就很自废武功的设定呢?因为我们还有另一个大杀器——并行算法。

因为排序网络中所有的比较操作是与数据无关的,我们就可以事先开一堆线程,然后告诉每个线程你应该在什么时间进行哪个比较,说白了我们可以把不相干的比较一起进行(比如a_1a_2的比较就能和a_3a_4的比较同时进行),于是时间复杂度就变成了排序网络的总“层数”。

由此思想,我们可以设计出很多并行排序算法:

首先,冒泡排序显然可以改造为并行排序——每轮冒泡进行到第3次时,同时开启下一轮的冒泡就行了。虽然它的总比较次数是O(n^2)级别,但并行之后可以变成O(n)的时间复杂度。

其次是奇偶移项排序,它会在第奇数层同时进行所有下标为奇数的位置跟其后面一个数的比较,第偶数层同时进行所有下标为偶数的位置跟其后面一个数的比较,这样重复n次即可。

它的正确性在上面那篇文章里有证,这里略去。时间复杂度仍为O(n),但是常数略优秀一些。

(ps:这个题其实由于上界很松,写一个奇偶移项排序就能过了)

更优秀的排序算法还有很多,有许多需要O(n \log ^2 n)次比较的排序算法都可以改造为排序网络,并通过并行计算将时间复杂度将为O(\log^2 n)。这里介绍一种“奇偶归并排序”(就是上面那篇文章最后提到的)。

首先,如果n不是2的幂,就向上补成2的幂,这对复杂度没有影响。以及下面假设下标从0开始。

然后进行递归,将左侧和右侧分别递归地进行排序。显然两侧的排序过程可以直接并行。

然后进行合并,操作如下:

首先,将下标按奇偶进行分类,发现每一类其实都是一个合并的子问题,于是递归下去。这两个子问题可以直接并行。

最后塞回原序列中,可以证明只需要在所有下标为奇数的位置和它的后一个位置进行比较即可完成排序。具体证明详见原文。

总比较次数为O(n \log^2 n),但是由于并行,实际上只需要\frac{1}{2}\log n(\log n + 1) = O(\log^2 n)次并行操作。

最后,我们再把上面所有的内容串起来——前文的步骤5恰好就是利用“大整数O(1)计算”这个性质,完成一次并行比较!

而对于一个序列和排序网络的一层,我们也可以设计一套大整数运算进行并行比较:需要注意到上面介绍的几种算法中,同一层之间的各次比较的两数下标的间隔是相等的。这有利于我们直接用位运算进行操作。

首先用按位与把需要比较的a_ia_j分别取出来,得到两个大整数记作AB。接下来,设这个相等的间隔是d,则把B右移d\times k位,以使得需要比较的两个数在同一位置。然后用步骤5进行比较,比较完后再左移回去然后塞回原序列即可。

于是我们就可以利用大整数运算实现一个并行排序算法,而实现这个算法需要的一切运算,都是我们的题设所允许的。

另外,如果只是需要找全局最小值,只需要每次对于i=0,2,4,......a_ia_{i+1}进行比较,每次都能扔掉一半的数,最后只需要O(\log n)次并行比较即可。

最后填上前文的几个坑:

1、为什么另外两种利用大整数运算实现比较的算法(尤其是那种O(1)算法)不能用?

其中一种算法是把大整数当成数组来跑计数排序,你在不知道序列的前提下显然玩不了;另一种就非常遗憾了——题目中给的B还不够大,如果给到10^5说不定能试试看(手动/斜眼笑)。

2、怎么解决在用大整数同时比较很多对数时的溢出问题?

一个可行的操作是“拓宽字长”,即将原先k位的字长变成k+2位,其中还要留出最高位作为符号位(输入数据由于都非负所以是没有符号位的)。然后在每两个数中间再空出2位防止上一个数的运算结果溢出到下一个数中。

这个操作以及最后输出时的“压缩字长”的逆操作并不难实现,先将后一半数统一左移若干位,再递归(其实是并行)地处理左右两边即可。只需要O (\log n)次操作。

虽然操作不多,但对于只有150次操作限制的求最小值任务来说还是很致命的。幸好我们仔细分析,发现由于这时的比较都是每个位置和下一个位置比,在实际比较时已经将下一个位置取出并右移,此时每个数前面其实至少空出了一个数的空隙,因此“拓宽字长”操作是不必要的。

3、感觉这一堆又是减法又是取绝对值的操作好烦啊,有没有更给力一点的?

有一个matrix67没有提到的办法可以让比较操作简单很多:取a-b的符号位之后取反再+1,此时若a\geq b得到的是000...0,反之是111...1,将其记作x,再将x按位取反得到y。接下来,只需要取(x \wedge a) + (y \wedge b)就可以得到较小值,(y \wedge a) + (x \wedge b)可以得到较大值。

这样的一次比较只需要20多次基本运算,如果只需要取最小值的话只用十几次。

最后,让我们总结一下全部步骤:

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)次操作进行“压缩字长”。

代码如下(这一版代码两个任务的最大询问次数分别为120次左右和800次左右,可以进行一定的常数优化):

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

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值