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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章