一 、位運算例項
1、用乙個表示式,判斷乙個數x是否是2的n次方(2,4,8,16.....),不可用迴圈語句。
x:2,4,8,16轉化成二進位制是10,100,1000,10000。如果減1則變成01,011,0111,01111。兩者做按位與運算,結果如果為0,則x是2的n次方。2、統計乙個整數的二進位制中1的個數
int countnumberofone(int number)
return counter;}二、位運算基礎
很多高階的動態規劃題目或者一些基礎的運算往往需要較高的執行效率和較低的空間需求,或者需要表示一些狀態集合,而位運算剛好能滿足這一切。很多的時候,恰當的位運算使用也能使程式變得更加簡潔和優美。
1、位運算法則
位運算是各位互不影響的,比如a為1010而b為1100,那麼有
a&b=1000a|b=1110
a^b=0110
~a=11110101 (1的個數是取決於a的型別的,這裡認為a的型別是8位整型)
另外兩種位運算是位移運算a<>b。前者表示將a的所有位向左移動b位,後者則表示將a的所有位向右移動b位。對於非負整數(往往這也是我們最關心的),新空出的位將會被0取代。比如a為1001,而b為3,那麼a<>b則為1。
大多數情況下可以簡單地認為左移b位就是乘以2^b,而右移b位則是除以(整除)2^b。當然這是存在例外的——對於負數是不能這麼簡單認為的:比如在gnu gcc/g++ 編譯條件下,若a=-1,你會發現對於任何位移運算a>>b,無論b的取值如何,其結果均為-1。因此請注意,在位移運算下務必確保對非負整數進行運算,以免發生不必要的問題。
對於位移運算最常用的操作就是取乙個特定的位——比如1<2、對於集合的表示
大多數時候,我們可以用乙個整數來表示乙個包含不超過32(當然如果使用64位整型變數也可以是64個)個元素的集合——對於每乙個位,如果元素為1,則表示存在當前位所對應的集合成員,如果是0,則表示這個集合成員是不存在的。比如a=1011 就可以表示集合,而上面提到的1《這裡列舉一些常用的位運算技術。
1、交換技術
即不利用第三方進行數的交換,這裡給出**段。
swap(a, b)
2、提取技術
這裡我們要做的就是找出變數a最低位的1和最高位的1分別在什麼位置。通過這些手段我們就能輕鬆地將乙個集合分解為若干個元素。
(1)低位技術低位技術即lowbit技術。相信熟悉樹狀陣列(bit)的朋友應該並不陌生。
我們對於乙個非0數x,現在提取出其最低位的1。這裡我提三種不同的寫法。
lowbit(x)=x&(x^(x-1))
lowbit(x)=x&~(x-1)
lowbit(x)=x&-x
注意:這裡我們求出的是x中最後乙個1表示的數,而非其位置。
可以發現,這三種低位函式的寫法可謂大同小異——均涉及到了x&和x-1(其實 –x 可以認為是和 ~(x-1) 等價的,這裡利用了負數的儲存原理)。
x-1的性質在於:其將乙個數最後乙個1變成了0,並把原來這個1之後0的位置均變成了1。低位技術正是利用了這個性質。
舉乙個簡單的應用的例子——n<32的全排列問題。
dfs(dep, mask)} 上述程式的複雜度為嚴格的o(n!),而非o(nn)。
這裡只是乙個說明,並沒有特指全排列問題——這種方式在很多地方可以大大提高程式效率。
(2)特殊情況下的簡單想法對於低位技術,乙個最簡單的想法就是按位掃瞄依次判斷當前位是否為1,這個演算法似乎是很慢的——對於乙個n位的二進位制數,最壞情況需要掃瞄n次!
但是這只是最壞而非一般——當情況特殊一些——我們要求的不是x的最低位,而是要求出1…2n-1這所有數的低位!
那麼在這種情況下,看似緩慢的暴力演算法其實是非常不錯的——這種演算法大約只要均攤2次掃瞄即可完成檢索。
這實際上是最快的方式了。
(3)利用分塊思想我們可以打一張1…28-1的表,用於記錄每個數的低位(或者是高位),那麼對於32位的整數,就可以將其分解為4個8位整數利用預處理得到的表迅速求出低位或者高位了。
(4)利用編譯器的內建函式這是c語言的又一大優勢。
這裡略微提一下兩個cpu位處理指令:bsf(前向位掃瞄)和bsr(反向位掃瞄)。這兩種指令都是內建且非常高效率的。而令人高興的是——gnu編譯器就存在這兩種基於這種原理的位處理函式:__builtin_clz(統計最高位0的個數)和__builtin_ctz(統計低位0的個數)。這是對於c和c++程式設計者最方便和快捷的位處理方式了。
不過要注意的是——這兩個函式對於0都沒有定義,因此需要特別小心!
3、計算二進位制表示中1的個數
我們很容易判斷乙個數是不是2的整次冪——比如對於x,那麼只要判斷x^lowbit(x)是否為0就可以了。不過很多時候我們需要統計二進位制位中有多少個1(比如當x表示乙個集合的時候,我們想知道這個集合中元素的個數),這就要麻煩一點了。
(1)暴力方式
我們可以不斷地使用低位技術消去最後乙個1。不過這個方法很慢。
(2)預處理我們可以利用遞推形式計算出我們所需要的答案,方式非常簡單。
用cnt[x] 來記錄x的二進位制表示中1的個數,那麼:
cnt[x] = cnt[x >> 1] + (x & 1)
(3)利用內建函式
gnu有乙個函式 __builtin_popcount 就是實現了我們需要的功能——不過比較遺憾的是,這個函式的實現並不像__builtin_ctz那樣利用硬體指令,相反的,它用了乙個類似基於預處理方式的按位掃瞄的實現方式——不過它仍然很高效。
(4)有趣的**這裡再給出一段可以實現上述功能的**,有興趣的讀者可以自行研究。
pop(xx)
4、列舉子集
當乙個數的二進位制表示乙個集合的時候,位運算的乙個巨大優點在於其可以非常輕鬆和高效地列舉當前集合的所有子集。它的性質在於——如果a是b的真子集,那麼可以保證列舉a子集的次數一定小於列舉b的子集的次數。這一點可以大大提高很多壓位動態規劃題目的實現效率。
(1)暴力的方式最暴力的方式莫過於列舉所有可能的集合,然後一一判斷是否為當前集合的子集。
如果需要列舉的集合是n個元素的集合,那麼對所有可能集合都進行一次列舉操作,花費的時間為o((2n)2)=o(4n)。
(2)高效的方式假設全集有n個元素,那麼所有可能的集合就有2n個,對於任意子集s,用n位2進製簡單列舉s的所有子集,個數就有2n個,因此如果對所有集合都進行這樣一次列舉操作,那麼總的時間複雜度就是o((2n)2)=o(4n)。高效的方式。
這裡的技巧和低位技術的技巧是類似的——當我們取出最後乙個1的時候,這個1將變成0,而比其低位的0將變成1。
與低位技術不同的是,我們並不是要提出某一位1,而是要去除某一位的1,並補上一些我們需要的1。
所以假設當前集合為superset,那麼列舉的**段則為
iterating_all_subset(superset)若當前為n位二進位制的集合,並且對所有可行集合進行上述操作,可以證明,操作的總次數為o(3n)。四、有趣的技巧
1、計算絕對值abs( x )
這裡需要注意的是,上面的x, y 預設為32位有符號整數。
2、按位翻轉
x=((x&0xaaaaaaaa)>>1)|((x&0x55555555)<<1);x=((x&0xcccccccc)>>2)|((x&0x33333333)<<2);
x=((x&0xf0f0f0f0)>>4)|((x&0x0f0f0f0f)<<4);
x=((x&0xff00ff00)>>8)|((x&0x00ff00ff)<<8);
x=((x&0xffff0000)>>16)|((x&0x0000ffff)<<16);
如果無符號32位整數x=311=(100110111)2,那麼經過上述操作後x=3967811584=(11101100100000000000000000000000)2。
3、列舉恰好含有k個元素的集合
我們假設全集為含有n個元素為 ,那麼**段可以寫成:int s = (1 << k) - 1;
while (!(s & 1 << n))
當然最後一句話也可以寫成s |= (lz >> __builtin_ctz(lo << 1)) – 1來避免除法運算。
C語言中的位運算
0 推薦 在電腦程式中,資料的位是可以操作的最小資料單位,理論上可以用 位運算 來完成所有的運算和操作。一般的位操作是用來控制硬體的,或者做資料變換使用,但是,靈活的位操作可以有效地提高程式執行的效率。c語言提供了位運算的功能,這使得c語言也能像組合語言一樣用來編寫系統程式。位運算子c語言提供了六種...
C語言中的位運算
c語言中的位運算有六個 按位與 按位或 按位異或 取反 左移 右移 除 以外其餘均為二元運算子,即要求運算子兩側均有乙個運算量 位運算量只能為整型或字元型,不能為實型資料。1.按位與 0 0 0 0 1 0 1 0 0 1 1 1 3 5 00000011 00000101 00000001 1 的...
C語言中的位運算
在電腦程式中,資料的位是可以操作的最小資料單位,理論上可以用 位運算 來完成所有的運算和操作。一般的位操作是用來控制硬體的,或者做資料變換使用,但是,靈活的位操作可以有效地提高程式執行的效率。c語言提供了位運算的功能,這使得c語言也能像組合語言一樣用來編寫系統程式。位運算子c語言提供了六種位運算子 ...