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