文章組織: 1、基本操作符 2、需要注意的問題 3、一些小應用 4、針對具體題目的應用 1> 搜尋類 2> 字串類 3> 其他類基本操作符 與 & 或 | 異或 ^ 左右移位 << / >> 取反 ~ 需要注意的問題: 1、優先順序,這是個非常嚴重的問題,在進行位元運算的時候優先順序太容易被忽略掉了 尤其要注意的: 移位元運算符, 單目的取反運算子~的優先順序比比較子高。但是,&, |, ^的優先順序是比比較子低的!,這點一定要注意 如6 & 6 > 4的結果是0, 而不是 true; (6 & 6) > 4的結果才是true,所以要注意勤加括弧 2、速度 位元運算的速度是非常快的,你甚至可以忽略他的耗時,但是畢竟操作肯定是有耗損的。 所以,應該盡量使用 ^=, |= ,&= 之類的操作,比如說a ^= b, 速度比 a = a ^ b快,因為前者是直接在a上進行操作,而 a = a ^ b的第二個a,是一個a 的副本,可見在操作中程式對a複製了一次,運算的結果又對原來的a做了一次賦值 3、範圍(要當心移位元運算時發生範圍溢出) 常用操作: 1、交換兩個元素的值:a ^= b ^= a ^= b,不用藉助第三個變數 2、不查表統計1的數量 int BitCnt(unsigned int n) { int value = 0; while (n) ++value, n &= n - 1; return value; } 3、迴圈位移 unsigned shift(unsigned now, int n, int N) { //n正表示左移,否則為右移 if (n > 0) now = (now >> (N - n)) | (now << n); if (n < 0) { n = -n; now = (now << (N - n)) | (now >> n); } now &= base; return now; } 4、k位取反 a=~(~a^(a<<k)) 優勢: 速度快, 節省空間的,又導致了速度快 POJ的題目上: 大類1:搜尋 此處不講題目,只講位元運算是怎樣在這些題中實現和應用的。由於搜尋題往往是基於對狀態的操作,位元運算往往特別有效,最佳化之後的效果可以有目共睹。 例1、POJ 1324 根據題目,確定了對狀態的表示之後(記錄目前狀態的蛇頭x, y值與剩下部分的運動狀態),一般容易想到剩下部分的運動狀態用一個數組(比如x[n-1], n為蛇節點數,n[0]為第二個節點的運動趨勢)去表示,且一個數組元素的值為0,1,2,3,即四個方向,蛇每次移動,這個方向數組需要更新一次,更新是很簡單的,除了n[0]更新之外,剩下的 n[i]等於上一輪的n[i-1](i>1);如果用數組,就必須用一個迴圈去更新它,這樣會導致程式速度不是很理想,所以可以想到用位元運算去類比,簡言之,就是把原來的一個數組壓縮的一個變數上去儲存,原來任何一個數組元素的值範圍0~3, 用二進位表示就是 00 ~ 11,最多佔用2位,這樣,我們就可以通過移位元運算,和或運算去實現所以操作。 基本技巧: 如果給你個一個方向序列 a0, a1, a2, a3, .... a(n-1),放到變數b中,且以最低位表示an,可以用如下迴圈 b = 0; for(i=0; i<n; i++) b = (b<<2) | a[i]; 即,每次把已經處理好的位段向上移兩位,這樣,在低位就會自動補齊兩個0,用一個 | 操作,就吧新的值給放進變數中了。 如果要從b中複原,則可使用 >> 的辦法 for(i=0; i<n; i++) a[n-1-i] = (b >> (i*2)) & 3; 移位會將低位元素除掉,而 & 3則是把移到低位的元素取出。 更高效的方法是(兩種方法的前提都是不改變b的值) t = b; for(i=0; i<n; i++){ a[n-1-i] = t & 3; t >>= 2; } 高效的原因如文章開頭所述。 在表示狀態的位段操作中,最好將變數設為unsigned int (short)型,因為有符號型(負數的最高位(符號位)為1)的 >>操作加在負數上時會在右邊引入1而不是0, 這基本上不會是你想要的操作。 例2、POJ 3460 Booksort 這題可以在對書進行交換的時候應用位元運算,也許你認為用數組去類比更為直觀,但是看看下面的實現,也許你就不那麼認為了。 書最大編號15, 這樣就開一個64位整形,每4位表示一本書 首先預先處理,計算出兩張表 extract[i][j] 和 clear[i][j]分別表示把書架上第i到j本書取出或清空,顯然在i到j一段,位上全是1,而其他部分是0,而clear[i][j]就是extract[i][j]的取反 計算方法如下: typedef unsigned long long longint; const longint base = 15;//這是必須注意的,普通的常數15是整形,在以下的操作中會越界 for (i = 0; i < 15; i++) { extract[i][i] = base << (i * 4); clear[i][i] = ~extract[i][i]; for (j = i + 1; j < 15; j++) { extract[i][j] = extract[i][j-1] | (base << (j * 4)); clear[i][j] = ~extract[i][j]; } } 計算出這些以後,交換書就變得異常簡單 inline void transposition(longint &cur, longint &next, int &i, int &k, int &j) {//i,k段與k+1,c段置換 next = cur & clear[i][j]; next |= (cur & extract[i][k]) << ((j - k) << 2); next |= (cur & extract[k+1][j]) >> ((k - i + 1) << 2); } 先把i到j全部清空後的值賦給next,把 i, k段的書取出,移到高位(差值為(j - k) * 4),同理,再把k+i到j段的值取出移到低位。 如此清楚明了,而如果用數組的話,10+行的語句是少不了的,而且容易搞錯。 例3:HOJ 八數位問題 http://acm.hnu.cn:8080/online/?action=problem&type=show&id=10466 這題用位元運算我倒是沒有達到更快,空間稍微節省了些。 另外,這題的測試資料很強的說,贊一個。 用64位的整形去表示整個狀態,其中每4位表示一個數位(0~8),如果是用char數組,則用9個,用了72位。 取出某一位和清空某一位的掩碼事先要計算好 這樣做比用一個整形數如 123480567 之類的去表示更有優勢,因為顯然取出某一位的速度非常快,不需要 % 10, /10之類耗時的操作,如果這樣實現的話就不要用stl的map了,我無聊地試了次,相當之慢。還是老老實實康托展開的好。不過和開char數組的速度差不多的說(雖然說賦值時間大了,但畢竟別人是數組,呵呵,要是有每4位做一個單位的數組就好了——不要跟我說用struct裡面定義的位段啊,那相當的慢)。 最近還做了一個POJ 2286,可以用位元運算,但貌似有些麻煩,沒用,結果被一個用pascal的小子超在前面,哪天狀態好了要把這題改用位元運算去做,就不信刷不到 0ms. 更新中... 大類二:字串的hash與尋找 大家都知道Rabin-Karp演算法吧,其實Rabin-Karp實現後慢的一個原因是太多的模運算了,實際上,我們可以通過位元運算隱式地模數 原連結如下:http://www-igm.univ-mlv.fr/~lecroq/string/node5.html#SECTION0050 它開了一個int型,每次用移位代替乘法,又由於整形是有範圍的,但移位超過了範圍,效果就相當於做了一次 mod INT_MAX的操作。這樣改進後速度提高了一倍。 POJ 2513 Colored Sticks就可以考慮使用此種方法的hash,他的衝突也是非常小的。當然有更高效的的字串位元運算hash演算法,由於某些原因,我就不在這說了。 //待續 大類三: |