zoukankan      html  css  js  c++  java
  • 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
    ", s[i], have);
    	}
    //	printf("%d
    ", have);
    }
    
    void work()
    {
    	int now = 1;
    	for(R i = 1; i <= n; i ++)
    	{
    	//	printf("!!!%d %d %d %d
    ", i, s[i], last, have);
    	//	printf("-----%d:
    ", i);
    	//	for(R j = n - 7; j <= n; j ++) printf("%d", s[j]);
    	//	printf("
    "); 
    		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("???
    ");
    			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
    ", last, have);
    			ans[++ tot] = now, now = 0;//去左子树 
    		}
    	}
    	for(R i = 1; i <= tot; i ++) printf("%d", ans[i]);
    	printf("
    ");
    }
    
    int main()
    {
    	//freopen("in.in", "r", stdin);
    	pre();
    	work();
    	return 0;
    }
    
    本文不允许商业性使用,个人转载请注明出处! 知识共享许可协议
    本作品采用知识共享署名-非商业性使用-禁止演绎 3.0 未本地化版本许可协议进行许可。
  • 相关阅读:
    2014 非常好用的开源 Android 测试工具
    Android 开发最佳实践
    Java_综合案例DAO设计模式(重要)
    Java_Set接口
    Java_List
    Java_类集框架简介
    Java_对象序列化
    Java_打印流
    Java_IO编程
    Java_文件操作
  • 原文地址:https://www.cnblogs.com/ww3113306/p/15361208.html
Copyright © 2011-2022 走看看