ARC127C Binary Strings 思维 二进制 树

C.Binary Strings

题面
题目大意:
给定N,X,要求在1~\(2^N - 1\)的范围内找字典序排名第X小的二进制数,其中X是以2进制给出的
\(1<=N<=10^6, \quad 1<=X<=2^N-1\)

题解:
首先我们注意到N范围非常大,带log都比较艰难,此外X是以2进制给出的,要求的也是个二进制数,而2进制有个显著特点是每一位只有2种可能。
其次我们能凑出的数都是没有前导0的,因此每个数必然以1开头。
我们可以发现,其实这是一个以1为根,一共N层的二叉树。每个节点到根的路径表示一个二进制数。
那么我们只需要考虑如何在这个数上找到第X大的节点。
对于当前节点所在的树形结构,易知,根的排名是最小的,左子树的节点排名都小于右子树排名,右子树排名大于根。
也就是根<左子树<右子树这样一种先序遍历的关系。
所以其实我们只需要根据所需排名在树上走就行了,类似于平衡树找k大,只不过这里的大小关系不是左子树<根<右子树。
具体实现来看,我们之前找k大都是用的10进制,但是10进制在这道题中显然不太现实,因为转换起来麻烦,用起来也还需要高精减法,总之不太合适。
所以我们考虑直接使用2进制在树上走。
1~\(2^N - 1\)我们在二进制上看,其实就是1~\(\overbrace{111...111}^{N个}\)
所以我们容易发现这棵树其实是一个满二叉树,最小的节点为根,最大的节点为一直往右走得到的叶节点。
假设当前在i层,由于这棵树一定是满二叉树,所以左子树+根的节点个数一定等于\(2^{N - i}\),
如果我们将给定的X放在一个长度为N的二进制数组里(即补全前导0),那么我们可以发现,
对于任意第i层的节点,左子树+根的大小 = X所在数组的第i位对应的数字大小(从高位往低位数第i个所代表的数字就是\(2^{N - i}\)
因此我们从第一层开始遍历,
对于第i层,如果二进制数组内的第i位为1,且最后一个1不是当前位,那么说明我们要找的数的排名在当前树中要大于左子树+根,也就是要找的数在右子树中,因此我们将二进制数组内第i位置零(也就相当于减去左子树+根的大小),然后往右走。
如果第i位为1,且当前位就是最后一个1,那么说明我们要找的节点是根+左子树中排名最大的节点,也就是先往左走,再一直往右走到底。
如果第i位为0,且二进制数组中有且仅有1个1,并且最后一个1的位置在第n位,也就是我们现在要找当前树中排名为1的节点,也就是当前节点(当前树的根)
如果第i为为0,且二进制数组剩余的数的大小大于1(即不是上一种情况),那么说明答案在左子树,我们往左子树走,同时我们相当于舍弃掉了根节点,又因为根节点排名小于左子树,因此我们要在剩余数内减去1.
减去1这个操作其实是可以暴力做的,因为我们只需要维护二进制数组和最后一个1的位置。
直接将最后一个1的位置置零,然后把最后一个1后面的位置全部变成1
可以证明这样暴力做的复杂度小于\(Nlog_2N\),据说还能证明这样是线性的,不过我还不知道怎么证,我只会证这样的复杂度低于一个log
证明如下:
假设我们某次暴力修改了第x位,
1,如果\(x<=log_2N\),那么我们寻找的范围是logN级别的.
2,如果\(x>log_2N\),那么我们寻找的范围大于logN,最大可达N。
但是我们注意到,假设有\(t = log_2N+1\)位上有1个1,那么这个位上的1就足以我们全部的暴力删除使用了。因为这个1对应的数量大于等于N,要完全删除这个1(指把删除它之后所有因此新增的1也全部删掉)至少需要N次,而这N次显然都会在小于\(t\)的位置上删1(删除一个1后新增的1不可能比它本身还大),因此每次删除都会小于logN。
\(x>=t\).如果\(x=t\),那上诉已经证明复杂度小于\(NlogN\),如果\(x>t\),那么只要删去1次x,t位上必然新增一个1,然后就变成了刚刚的情况。也就是对于这种x的删除,最多1次,可以视作一个大小为N的常数,而且是加到复杂度里(而不是乘),因此可以忽略。

#include<bits/stdc++.h>
using namespace std;
#define R register int
#define AC 1200000
#define ac 4040000

int n, len, tot, last, have;
int s[AC], ans[AC];
char c[AC];

void pre()//不需要线段树,直接暴力维护最后一个1的位置
{
	scanf("%d", &n);
	scanf("%s", c + 1), len = strlen(c + 1);
	for(R i = n; len ; i --)
	{
		s[i] = c[len--] - '0', have += s[i];//, len --;
		if(s[i] && !last) last = i;
		//printf("%d %d\n", s[i], have);
	}
//	printf("%d\n", have);
}

void work()
{
	int now = 1;
	for(R i = 1; i <= n; i ++)
	{
	//	printf("!!!%d %d %d %d\n", i, s[i], last, have);
	//	printf("-----%d:\n", i);
	//	for(R j = n - 7; j <= n; j ++) printf("%d", s[j]);
	//	printf("\n"); 
		if(s[i] == 1 && last != i) ans[++ tot] = now, now = 1, have --;//如果当前是1,且不是最后一个1 
		else if(last == n && have == 1)  {ans[++ tot] = now; break;}//只有1个1,如果最后一个1在末尾,说明答案就在当前节点   
		else if(s[i] == 1 && last == i)//如果最后一个1就是当前位置 
		{
			ans[++ tot] = now, now = 0, i ++;
			for(; i <= n; i ++) ans[++ tot] = now, now = 1;//左子树找max 
		//	printf("???%d", last);
			break;
		}
		else//s[i] == 0 并且剩下的值>1, 答案在左子树 
		{
			//printf("???\n");
			s[last] = 0;
			for(R j = last + 1; j <= n; j ++) s[j] = 1;
			have --, have += n - last;//暴力减1 
			if(last != n) last = n;
			else for(R j = n; j >= i; j --)
				if(s[j] == 1) {last = j; break;} 
		//	printf("%d %d\n", last, have);
			ans[++ tot] = now, now = 0;//去左子树 
		}
	}
	for(R i = 1; i <= tot; i ++) printf("%d", ans[i]);
	printf("\n");
}

int main()
{
	//freopen("in.in", "r", stdin);
	pre();
	work();
	return 0;
}
posted @ 2021-10-03 11:53  ww3113306  阅读(340)  评论(0编辑  收藏  举报
知识共享许可协议
本作品采用知识共享署名-非商业性使用-禁止演绎 3.0 未本地化版本许可协议进行许可。