-
位運算
鎖定
程序中的所有數在計算機內存中都是以二進制的形式儲存的。位運算就是直接對整數在內存中的二進制位進行操作。比如,and運算本來是一個邏輯運算符,但整數與整數之間也可以進行and運算。舉個例子,6的二進制是110,11的二進制是1011,那麼6 and 11的結果就是2,它是二進制對應位進行邏輯運算的結果(0表示False,1表示True,空位都當0處理)。
110
AND 1011
---------------
0010 --> 2
- 中文名
- 位運算
- 外文名
- bitwise operation
- 方 法
- 對整數進行操作
- 用 途
- 優化程序
位運算運算符號
下面的a和b都是整數類型,則:
含義 | Pascal語言 | C/C++語言 | Java | Php |
---|---|---|---|---|
按位與 | a and b | a & b | a & b | a & b |
a or b | a | b | a | b | a | b | |
按位異或 | a xor b | a ^ b | a ^ b | a ^ b |
按位取反 | not a | ~a | ~a | ~a |
左移 | a shl b | a <<b | a <<b | a << b |
帶符號右移 | a shr b | a >> b | a >> b | a >> b |
無符號右移 | / | / | a>>> b | / |
位運算運算説明
位運算and運算
相同位的兩個數字都為1,則為1;若有一個不為1,則為0。
00101
11100
(&;或者and)
----------------
00100
位運算or運算
or運算通常用於二進制特定位上的無條件賦值,例如一個數or 1的結果就是把二進制最末位強行變成1。如果需要把二進制最末位變成0,對這個數or 1之後再減一就可以了,其實際意義就是把這個數強行變成最接近的偶數。
相同位只要一個為1即為1。
00101
11100
(|或者or)
----------------
11101
位運算xor運算
xor運算的逆運算是它本身,也就是説兩次異或同一個數最後結果不變,即(a xor b) xor b = a。xor運算可以用於簡單的加密,比如我想對我MM説1314520,但怕別人知道,於是雙方約定拿我的生日19880516作為密鑰。1314520 xor 19880516 = 20665500,我就把20665500告訴MM。MM再次計算20665500 xor 19880516的值,得到1314520。
相同位不同則為1,相同則為0。
00101
11100
(^或者xor)
----------------
11001
運算結果
x <- x # y
y <- x @ y
x <- x @ y
執行了第一句後x變成了x # y。那麼第二句實質就是y <- x # y @ y,由於#和@互為逆運算,那麼此時的y變成了原來的x。第三句中x實際上被賦值為(x # y) @ x,如果#運算具有交換律,那麼賦值後x就變成最初的y了。這三句話的結果是,x和y的位置互換了。
加法和減法互為逆運算,並且加法滿足交換律。把#換成+,把@換成-,我們可以寫出一個不需要臨時變量的swap過程(Pascal)。
//pascal代碼 procedure swap(var a,b:longint); begin a:=a + b; b:=a - b; a:=a - b; end;
好了,剛才不是説xor的逆運算是它本身嗎?於是我們就有了一個看起來非常詭異的swap過程:
//pascal代碼 procedure swap(var a,b:longint); begin a:=a xor b; b:=a xor b; a:=a xor b; end;
位運算not運算
not運算的定義是把內存中的0和1全部取反。使用not運算時要格外小心,你需要注意整數類型有沒有符號。如果not的對象是無符號整數(不能表示負數),那麼得到的值就是它與該類型上界的差,因為無符號類型的數是用00到$FFFF依次表示的。下面的兩個程序(僅語言不同)均返回65435。
//pascal代碼 var a:word; begin a:=100; a:=not a; writeln(a); end.
#include<stdio.h> int main() { unsigned short a=100; a=~a; printf("%d\n",a); return 0; }
如果not的對象是有符號的整數,情況就不一樣了,稍後我們會在“整數類型的儲存”小節中提到。
位運算shl運算
a shl b就表示把a轉為二進制後左移b位(在後面添b個0)。例如100的二進制為1100100,而110010000轉成十進制是400,那麼100 shl 2 = 400。可以看出,a shl b的值實際上就是a乘以2的b次方,因為在二進制數後添一個0就相當於該數乘以2。
通常認為a shl 1比a * 2更快,因為前者是更底層一些的操作。因此程序中乘以2的操作請儘量用左移一位來代替。
定義一些常量可能會用到shl運算。你可以方便地用1 shl 16 - 1來表示65535。很多算法和數據結構要求數據規模必須是2的冪,此時可以用shl來定義Max_N等常量。
位運算shr運算
和shl相似,a shr b表示二進制右移b位(去掉末b位),相當於a除以2的b次方(取整)。和上面一樣的例子,那麼400 shr2 = 100。我們也經常用shr 1來代替div 2,比如二分查找、堆的插入操作等等。想辦法用shr代替除法運算可以使程序效率大大提高。最大公約數的二進制算法用除以2操作來代替慢得出奇的mod運算,效率可以提高60%。
位運算優先級
優先級別 | 運算符 |
1 | ~(按位取反) |
2 | 以下皆為同級: <<(左移) >>(右移) |
3 | &(按位與) |
4 | ^(按位異或) |
5 | |(按位或) |
6 | 以下皆為同級: &=(先按位與再賦值) ^=(先按位異或再賦值) |=(先按位或再賦值) <<=(先按位左移再賦值) >>=(先按位右移再賦值) ~=(先按位取反再賦值) |
位運算簡單應用
有時我們的程序需要一個規模不大的Hash表來記錄狀態。比如,做數獨時我們需要27個Hash表來統計每一行、每一列和每一個小九宮格裏已經有哪些數了。此時,我們可以用27個小於2^9的整數進行記錄。例如,一個只填了2和5的小九宮格就用數字18表示(二進制為000010010),而某一行的狀態為511則表示這一行已經填滿。需要改變狀態時我們不需要把這個數轉成二進制修改後再轉回去,而是直接進行位操作。在搜索時,把狀態表示成整數可以更好地進行判重等操作。這道題是在搜索中使用位運算加速的經典例子。以後我們會看到更多的例子。
下面列舉了一些常見的二進制位的變換操作。
功能 | 示例 | 位運算 |
---|---|---|
去掉最後一位 | (101101->10110) | x shr 1 |
在最後加一個0 | (101101->1011010) | x shl 1 |
在最後加一個1 | (101101->1011011) | x (shl 1)+1 |
把最後一位變成1 | (101100->101101) | x or 1 |
把最後一位變成0 | (101101->101100) | x or 1-1 |
最後一位取反 | (101101->101100) | x xor 1 |
把右數第k位變成1 | (101001->101101,k=3) | x or (1 shl (k-1)) |
把右數第k位變成0 | (101101->101001,k=3) | x and not (1 shl (k-1)) |
右數第k位取反 | (101001->101101,k=3) | x xor (1 shl (k-1)) |
取末三位 | (1101101->101) | x and 7 |
取末k位 | (1101101->1101,k=4) | x and 15 |
取右數第k位 | (1101101->1,k=4) | x shr (k-1) and 1 |
把末k位變成1 | (101001->101111,k=4) | x or (1 shl k-1) |
末k位取反 | (101001->100110,k=4) | x xor (1 shl k-1) |
把右邊連續的1變成0 | (100101111->100100000) | x and (x+1) |
把右起第一個0變成1 | (100101111->100111111) | x or (x+1) |
把右邊連續的0變成1 | (11011000->11011111) | x or (x-1) |
取右邊連續的1 | (100101111->1111) | (x xor (x+1)) shr 1 |
去掉右起第一個1的左邊 | (100101000->1000) | x and (x xor (x-1)) 或 x and (-x) |
從右邊開始,把最後一個1改寫成0 | (100101000->100100000) | x and (x-1) |
Pascal和C中的16進制表示
Pascal中需要在16進制數前加$符號表示,C中需要在前面加0x來表示。這個以後我們會經常用到。
位運算位運算交換
#include<cstdio> #include<cstdlib> int main() { int a,b; scanf("%d %d",&a,&b); a=a^b; b=a^b; a=a^b; printf("%d %d\n",a,b); }
位運算儲存
我們前面所説的位運算都沒有涉及負數,都假設這些運算是在unsigned/word類型(只能表示正數的整型)上進行操作。但計算機如何處理有正負符號的整數類型呢?下面兩個程序都是考察16位整數的儲存方式(只是語言不同)。
//pascal代碼 var a,b:integer; begin a:=00; b:=01; write(a,' ',b,' '); a:=$FFFE; b:=$FFFF; write(a,' ',b,' '); a:=$7FFF; b:=$8000; writeln(a,' ',b); end.
//C/C++代碼 #include<stdio.h> int main() { short int a,b; a=0x0000; b=0x0001; printf("%d%d",a,b); a=0xFFFE; b=0xFFFF; printf("%d%d",a,b); a=0x7FFF; b=0x8000; printf("%d%d\n",a,b); return0; }
兩個程序的輸出均為0 1 -2 -1 32767 -32768。其中前兩個數是內存值最小的數,中間兩個數則是內存值最大的數,最後輸出的兩個數是正數與負數的分界處。由此你可以清楚地看到計算機是如何儲存一個整數的:計算機用00到$7FFF依次表示0到32767的數,剩下的$8000到$FFFF依次表示-32768到-1的數。32位有符號整數的儲存方式也是類似的。稍加註意你會發現,二進制的第一位是用來表示正負號的,0表示正,1表示負。這裏有一個問題:0本來既不是正數,也不是負數,但它佔用了00的位置,因此有符號的整數類型範圍中正數個數比負數少一個。對一個有符號的數進行not運算後,最高位的變化將導致正負顛倒,並且數的絕對值會差1。也就是説,not a實際上等於-a-1(或-a=not a+1)。這種整數儲存方式叫做“補碼”(正數的補碼和原碼相同,負數的補碼是該數的絕對值的二進制形式,按位取反後再加1)。
位運算進階介紹
位運算奇偶性
我們可以用下面的代碼來計算一個32位整數的二進制中1的個數的奇偶性,當輸入數據的二進制表示裏有偶數個數字1時程序輸出0,有奇數個則輸出1。例如,1314520的二進制101000000111011011000中有9個1,則x=1314520時程序輸出1。
//pascal代碼 var i,x,c:longint; begin readln(x); c:=0; for i:=1 to 32 do begin c:=c + x and 1; x:=x shr 1; end; writeln( c and 1 ); end.
//C/C++代碼 int even_ones(unsigned int x) { int c = 0; / / 記錄1的個數 for (int i = 0; i < 32; i++) { c += x & 1; //判斷最後一位數字是否為1 x >>= 1; } return c & 1; }
但這樣的效率並不高,位運算的神奇之處還沒有體現出來。
同樣是判斷二進制中1的個數的奇偶性,下面這段代碼就強了。你能看出這個代碼的原理嗎?
//pascal代碼 var x:longint; begin readln(x); x:=x xor (x shr 1); x:=x xor (x shr 2); x:=x xor (x shr 4); x:=x xor (x shr 8); x:=x xor (x shr 16); writeln(x and 1); end.
//c/c++代碼 int even_ones (unsigned int x) { x = x^(x >> 1); x = x^(x >> 2); x = x^(x >> 4); x = x^(x >> 8); x = x^(x >> 16); return x & 1; }
為了説明上面這段代碼的原理,我們還是拿1314520出來説事。1314520的二進制為101000000111011011000,第一次執行x=x^(x>>1)的結果如下:
0000 0000 0001 0100 0000 1110 1101 1000
0000 0000 0000 1010 0000 0111 0110 1100(XOR)
---------------------------------------------------------------------
0000 0000 0001 1110 0000 1001 1011 0100
得到的結果是一個新的二進制數,其中右起第i位上的數表示原數中第i和i+1位上有奇數個1還是偶數個1。比如,最右邊那個0表示原數末兩位有偶數個1,右起第3位上的1就表示原數的這個位置和前一個位置中有奇數個1。對這個數進行第二次x=x^(x>>2)的結果如下:
0000 0000 0001 1110 0000 1001 1011 0100
0000 0000 0000 0111 1000 0010 0110 1101(XOR)
---------------------------------------
0000 0000 0001 1001 1000 1011 1101 1001
結果裏的每個1表示原數的該位置及其前面三個位置中共有奇數個1,每個0就表示原數對應的四個位置上共偶數個1。一直做到第五次異或結束後,得到的二進制數的最末位就表示整個32位數里有多少個1,這就是我們最終想要的答案。
位運算個數計算
同樣假設x是一個32位整數。經過下面五次賦值後,x的值就是原數的二進制表示中數字1的個數。比如,初始時x為1314520(網友抓狂:能不能換一個數啊),那麼最後x就變成了9,它表示1314520的二進制中有9個1。
x := (x and $555555) + ((x shr 1) and $555555);
x := (x and $333333) + ((x shr 2) and $333333);
x := (x and $0f0f0f0f) + ((x shr 4) and $0f0f0f0f);
x := (x and $00ff00ff) + ((x shr 8) and $00ff00ff);
x := (x and $0000ffff) + ((x shr 16) and $0000ffff);
為了便於解説,我們下面僅説明這個程序是如何對一個8位整數進行處理的。我們拿數字211來舉例。211的二進制為11010011。
+---+---+---+---+---+---+---+---+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | <---原數
+---+---+---+---+---+---+---+---+
| 1 0 | 0 1 | 0 0 | 1 0 | <---第一次運算後
+-------+-------+-------+-------+
| 0 0 1 1 | 0 0 1 0 | <---第二次運算後
+---------------+---------------+
| 0 0 0 0 0 1 0 1 | <---第三次運算後,得數為5
+-------------------------------+
整個程序是一個分治的思想。第一次我們把每相鄰的兩位加起來,得到每兩位裏1的個數,比如前兩位10就表示原數的前兩位有2個1。第二次我們繼續兩兩相加,10+01=11,00+10=10,得到的結果是00110010,它表示原數前4位有3個1,末4位有2個1。最後一次我們把0011和0010加起來,得到的就是整個二進制中1的個數。程序中巧妙地使用取位和右移,比如第二行中$333333的二進制為00110011001100....,用它和x做and運算就相當於以2為單位間隔取數。shr的作用就是讓加法運算的相同數位對齊。
位運算查找
這裏用的C語言,我直接Copy的Hacker's Delight上的代碼。這段代碼寫成C要好看些,寫成Pascal的話會出現很多begin和end,搞得代碼很難看。程序思想是二分查找,應該很簡單,我就不細説了。
int nlz(unsigned x) { int n; if(x==0)return(32); n=1; if((x>>16)==0) { n=n+16;x=x<<16; } if((x>>24)==0) { n=n+8;x=x<<8; } if((x>>28)==0) { n=n+4;x=x<<4; } if((x>>30)==0) { n=n+2;x=x<<2; } n=n-(x>>31); returnn; }
只用位運算來取絕對值
答案:假設x為32位整數,則x xor (not (x shr 31) + 1) + x shr 31的結果是x的絕對值 [<-Error: the absolute number of x should be like this: (x xor (x shr 31)) - (x shr 31). Note the operator order rules.
x shr 31是二進制的最高位,它用來表示x的符號。如果它為0(x為正),則not (x shr 31) + 1等於000000,異或任何數結果都不變;如果最高位為1(x為負),則not (x shr 31) + 1等於$FFFFFFFF,x異或它相當於所有數位取反,異或完後再加一。
高低位交換,題目是這樣:
給出一個小於2^32的正整數。這個數可以用一個32位的二進制數表示(不足32位用0補足)。我們稱這個二進制數的前16位為“高位”,後16位為“低位”。將它的高低位交換,我們可以得到一個新的數。試問這個新的數是多少(用十進制表示)。
例如,數1314520用二進制表示為0000 0000 0001 0100 0000 1110 1101 1000(添加了11個前導0補足為32位),其中前16位為高位,即0000 0000 0001 0100;後16位為低位,即0000 1110 1101 1000。將它的高低位進行交換,我們得到了一個新的二進制數0000 1110 1101 1000 0000 0000 0001 0100。它即是十進制的249036820。
當時幾乎沒有人想到用一句位操作來代替冗長的程序。使用位運算的話兩句話就完了。
//pascal代碼 var n:dword; begin readln(n); writeln((n shr 16) or (n shl 16)); end.
而事實上,Pascal有一個系統函數swap直接就可以用。
位運算逆序
下面的程序讀入一個32位整數並輸出它的二進制倒序後所表示的數。
輸入:1314520 (二進制為00000000000101000000111011011000)
輸出:460335104 (二進制為00011011011100000010100000000000)
//pascal代碼 var x:dword; begin readln(x); x := (x and $55555555) shl 1 or (x and $AAAAAAAA) shr 1; x := (x and $33333333) shl 2 or (x and $CCCCCCCC) shr 2; x := (x and $0F0F0F0F) shl 4 or (x and $F0F0F0F0) shr 4; x := (x and $00FF00FF) shl 8 or (x and $FF00FF00) shr 8; x := (x and $0000FFFF) shl 16 or (x and $FFFF0000) shr 16; writeln(x); end.
它的原理和剛才求二進制中1的個數那個例題是大致相同的。程序首先交換每相鄰兩位上的數,以後把互相交換過的數看成一個整體,繼續進行以2位為單位、以4位為單位的左右對換操作。我們再次用8位整數211來演示程序執行過程:
+---+---+---+---+---+---+---+---+
| 1 | 1 | 0 | 1 | 0 | 0 | 1 | 1 | <---原數
+---+---+---+---+---+---+---+---+
| 1 1 | 1 0 | 0 0 | 1 1 | <---第一次運算後
+-------+-------+-------+-------+
| 1 0 1 1 | 1 1 0 0 | <---第二次運算後
+---------------+---------------+
| 1 1 0 0 1 0 1 1 | <---第三次運算後
+-------------------------------+
n皇后問題位運算版
//pascal代碼 procedure test(row,ld,rd:longint); var pos,p:longint; begin if row<>upperlim then begin pos:=upperlim and not (row or ld or rd); while pos<>0 do begin p:=pos and -pos; pos:=pos-p; test(row+p,(ld+p)shl 1,(rd+p)shr 1); end; end else inc(sum); end;
和普通算法一樣,這是一個遞歸過程,程序一行一行地尋找可以放皇后的地方。過程帶三個參數,row、ld和rd,分別表示在縱列和兩個對角線方向的限制條件下這一行的哪些地方不能放。我們以6x6的棋盤為例,看看程序是怎麼工作的。假設已經遞歸到第四層,前三層放的子已經標在左圖上了。紅色、藍色和綠色的線分別表示三個方向上有衝突的位置,位於該行上的衝突位置就用row、ld和rd中的1來表示。把它們三個並起來,得到該行所有的禁位,取反後就得到所有可以放的位置(用pos來表示)。前面説過-a相當於not a + 1,這裏的代碼第6行就相當於pos and (not pos + 1),其結果是取出最右邊的那個1。這樣,p就表示該行的某個可以放子的位置,把它從pos中移除並遞歸調用test過程。注意遞歸調用時三個參數的變化,每個參數都加上了一個禁位,但兩個對角線方向的禁位對下一行的影響需要平移一位。最後,如果遞歸到某個時候發現row=111111了,説明六個皇后全放進去了,此時程序從第1行跳到第11行,找到的解的個數加一。
Gray碼
假如我有4個潛在的GF,我需要決定最終到底和誰在一起。一個簡單的辦法就是,依次和每個MM交往一段時間,最後選擇給我帶來的“滿意度”最大的MM。但看了dd牛的理論後,事情開始變得複雜了:我可以選擇和多個MM在一起。這樣,需要考核的狀態變成了2^4=16種(當然包括0000這一狀態,因為我有可能是玻璃)。問題就是,我應該用什麼順序來遍歷這16種狀態呢?
傳統的做法是,用二進制數的順序來遍歷所有可能的組合。也就是説,我需要以0000->0001->0010->0011->0100->...->1111這樣的順序對每種狀態進行測試。這個順序很不科學,很多時候狀態的轉移都很耗時。比如從0111到1000時我需要暫時甩掉當前所有的3個MM,然後去把第4個MM。同時改變所有MM與我的關係是一件何等巨大的工程啊。因此,我希望知道,是否有一種方法可以使得,從沒有MM這一狀態出發,每次只改變我和一個MM的關係(追或者甩),15次操作後恰好遍歷完所有可能的組合(最終狀態不一定是1111)。大家自己先試一試看行不行。
解決這個問題的方法很巧妙。我們來説明,假如我們已經知道了n=2時的合法遍歷順序,我們如何得到n=3的遍歷順序。顯然,n=2的遍歷順序如下:
00
01
11
10
你可能已經想到了如何把上面的遍歷順序擴展到n=3的情況。n=3時一共有8種狀態,其中前面4個把n=2的遍歷順序照搬下來,然後把它們對稱翻折下去並在最前面加上1作為後面4個狀態:
000
001
011
010 ↑
--------
110 ↓
111
101
100
用這種方法得到的遍歷順序顯然符合要求。首先,上面8個狀態恰好是n=3時的所有8種組合,因為它們是在n=2的全部四種組合的基礎上考慮選不選第3個元素所得到的。然後我們看到,後面一半的狀態應該和前面一半一樣滿足“相鄰狀態間僅一位不同”的限制,而“鏡面”處則是最前面那一位數不同。再次翻折三階遍歷順序,我們就得到了剛才的問題的答案:
0000
0001
0011
0010
0110
0111
0101
0100
1100
1101
1111
1110
1010
1011
1001
1000
這種遍歷順序作為一種編碼方式存在,叫做Gray碼(寫個中文讓蜘蛛來抓:格雷碼)。它的應用範圍很廣。比如,n階的Gray碼相當於在n維立方體上的Hamilton迴路,因為沿着立方體上的邊走一步,n維座標中只會有一個值改變。再比如,Gray碼和Hanoi塔問題等價。Gray碼改變的是第幾個數,Hanoi塔就該移動哪個盤子。比如,3階的Gray碼每次改變的元素所在位置依次為1-2-1-3-1-2-1,這正好是3階Hanoi塔每次移動盤子編號。如果我們可以快速求出Gray碼的第n個數是多少,我們就可以輸出任意步數後Hanoi塔的移動步驟。我告訴你,Gray碼的第n個數(從0算起)是n xor (n shr 1),你能想出來這是為什麼嗎?先自己想想吧。
下面我們把二進制數和Gray碼都寫在下面,可以看到左邊的數異或自身右移的結果就等於右邊的數。
二進制數 Gray碼
000 000
001 001
010 011
011 010
100 110
101 111
110 101
111 100
從二進制數的角度看,“鏡像”位置上的數即是對原數進行not運算後的結果。比如,第3個數010和倒數第3個數101的每一位都正好相反。假設這兩個數分別為x和y,那麼x xor (x shr 1)和y xor (y shr 1)的結果只有一點不同:後者的首位是1,前者的首位是0。而這正好是Gray碼的生成方法。這就説明了,Gray碼的第n個數確實是n xor (n shr 1)。
今年四月份mashuo給我看了這道題,是二維意義上的Gray碼。題目大意是説,把0到2^(n+m)-1的數寫成2^n * 2^m的矩陣,使得位置相鄰兩數的二進制表示只有一位之差。答案其實很簡單,所有數都是由m位的Gray碼和n位Gray碼拼接而成,需要用左移操作和or運算完成。完整的代碼如下:
//pascal代碼 var x,y,m,n,u:longint; begin readln(m,n); for x:=0 to 1 shl m-1 do begin u:=(x xor (x shr 1)) shl n; //輸出數的左邊是一個m位的Gray碼 for y:=0 to 1 shl n-1 do write(u or (y xor (y shr 1)),' '); //並上一個n位Gray碼 writeln; end; end.
位運算實戰
Problem:筷子
有n雙筷子擺在你的面前。已知長度相等的兩根筷子可以配對,輸出無法配對的那一根筷子的長度。
輸入樣例:
5
1 1 2 2 2
輸出樣例:
2
數據範圍:
100%的數據:n<2×10^7,n為奇數,每根筷子的長度小於2^31。數據保證有且只有一根筷子無法配對。
代碼(Pascal):
//pascal代碼 var n,ans,x,i:longint; begin read(n); ans:=0; for i:=1 to n do begin read(x); ans:=ans xor x; end; writeln(ans); end.
Problem : 費解的開關06年NOIp模擬賽一 by Matrix67 第四題 問題描述
你玩過“拉燈”遊戲嗎?25盞燈排成一個5x5的方形。每一個燈都有一個開關,遊戲者可以改變它的狀態。每一步,遊戲者可以改變某一個燈的狀態。遊戲者改變一個燈的狀態會產生連鎖反應:和這個燈上下左右相鄰的燈也要相應地改變其狀態。
我們用數字“1”表示一盞開着的燈,用數字“0”表示關着的燈。下面這種狀態
10111
01101
10111
10000
11011
在改變了最左上角的燈的狀態後將變成:
01111
11101
10111
10000
11011
再改變它正中間的燈後狀態將變成:
01111
11001
11001
10100
11011
給定一些遊戲的初始狀態,編寫程序判斷遊戲者是否可能在6步以內使所有的燈都變亮。
輸入格式
第一行有一個正整數n,代表數據中共有n個待解決的遊戲初始狀態。
以下若干行數據分為n組,每組數據有5行,每行5個字符。每組數據描述了一個遊戲的初始狀態。各組數據間用一個空行分隔。
對於30%的數據,n<=5;
對於100%的數據,n<=500。
輸出數據一共有n行,每行有一個小於等於6的整數,它表示對於輸入數據中對應的遊戲狀態最少需要幾步才能使所有燈變亮。
對於某一個遊戲初始狀態,若6步以內無法使所有燈變亮,請輸出“-1”。
樣例輸入
3
00111
01011
10001
11010
11100
11101
11101
11110
11111
11111
01111
11111
11111
11111
11111
樣例輸出
3
2
-1
程序代碼
//pascal代碼 const BigPrime=3214567; MaxStep=6; type pointer=^rec; rec=record v:longint; step:integer; next:pointer; end; var total:longint; hash:array[0..BigPrime-1]of pointer; q:array[1..400000]of rec; function update(a:longint;p:integer):longint; begin a:=a xor (1 shl p); i f p mod 5<>0 then a:=a xor (1 shl (p-1)); if (p+1) mod 5<>0 then a:=a xor (1 shl (p+1)); if p<20 then a:=a xor (1 shl (p+5)); if p>4 then a:=a xor (1 shl (p-5)); exit(a); end; function find(a:longint;step:integer):boolean; var now:pointer; begin now:=hash[a mod BigPrime]; while now<>nil do begin if now^.v=a then exit(true); now:=now^.next; end; new(now); now^.v:=a; now^.step:=step; now^.next:=hash[a mod BigPrime]; hash[a mod BigPrime]:=now; total:=total+1; exit(false); end; procedure solve; var p:integer; close:longint=0; open:longint=1; begin find(1 shl 25-1,0); q[1].v:=1 shl 25-1; q[1].step:=0; repeat inc(close); for p:=0 to 24 do if not find(update(q[close].v,p),q[close].step+1) and (q[close].step+1<MaxStep) then begin open:=open+1; q[open].v:=update(q[close].v,p); q[open].step:=q[close].step+1; end; until close>=open; end; procedure print(a:longint); var now:pointer; begin now:=hash[a mod BigPrime]; while now<>nil do begin if now^.v=a then begin writeln(now^.step); exit; end; now:=now^.next; end; writeln(-1); end; procedure main; var ch:char; i,j,n:integer; t:longint; begin readln(n); for i:=1 to n do begin t:=0; for j:=1 to 25 do begin read(ch); t:=t*2+ord(ch)-48; if j mod 5=0 then readln; end; print(t); if i<n then readln; end; end; begin solve; main; end.
Problem : garden / 和MM逛花園
問題描述
花園設計強調,簡單就是美。Matrix67常去的花園有着非常簡單的佈局:花園的所有景點的位置都是“對齊”了的,這些景點可以看作是平面座標上的格點。相鄰的景點之間有小路相連,這些小路全部平行於座標軸。景點和小路組成了一個“不完整的網格”。
一個典型的花園佈局如左圖所示。花園佈局在6行4列的網格上,花園的16個景點的位置用紅色標註在了圖中。黑色線條表示景點間的小路,其餘灰色部分實際並不存在。
Matrix67 的生日那天,他要帶着他的MM在花園裏遊玩。Matrix67不會帶MM兩次經過同一個景點,因此每個景點最多被遊覽一次。他和他的MM邊走邊聊,他們是如此的投入以致於他們從不會“主動地拐彎”。也就是説,除非前方已沒有景點或是前方的景點已經訪問過,否則他們會一直往前走下去。當前方景點不存在或已遊覽過時,Matrix67會帶MM另選一個方向繼續前進。由於景點個數有限,訪問過的景點將越來越多,遲早會出現不能再走的情況(即四個方向上的相鄰景點都訪問過了),此時他們將結束花園的遊覽。Matrix67希望知道以這種方式遊覽花園是否有可能遍歷所有的景點。Matrix67可以選擇從任意一個景點開始遊覽,以任意一個景點結束。
在上圖所示的花園佈局中,一種可能的遊覽方式如右圖所示。這種瀏覽方式從(1,2)出發,以(2,4)結束,經過每個景點恰好一次。
輸入格式
第一行輸入兩個用空格隔開的正整數m和n,表示花園被佈局在m行n列的網格上。
以下m行每行n個字符,字符“0”表示該位置沒有景點,字符“1”表示對應位置有景點。這些數字之間沒有空格。
輸出格式
你的程序需要尋找滿足“不主動拐彎”性質且遍歷所有景點的遊覽路線。
如果沒有這樣的遊覽路線,請輸出一行“Impossible”(不帶引號,注意大小寫)。
如果存在遊覽路線,請依次輸出你的方案中訪問的景點的座標,每行輸出一個。座標的表示格式為“(x,y)”,代表第x行第y列。
如果有多種方案,你只需要輸出其中一種即可。評測系統可以判斷你的方案的正確性。
樣例輸入
6 4
1100
1001
1111
1100
1110
1110
樣例輸出
(1,2)
(1,1)
(2,1)
(3,1)
(4,1)
(5,1)
(6,1)
(6,2)
(6,3)
(5,3)
(5,2)
(4,2)
(3,2)
(3,3)
(3,4)
(2,4)
數據規模
對於30%的數據,n,m<=5;
對於100%的數據,n,m<=10。
程序代碼
//pascal代碼 program garden; const dir:array[1..4,1..2]of integer= ((1,0),(0,1),(-1,0),(0,-1)); type arr=array[1..10]of integer; rec=record x,y:integer; end; var map:array[0..11,0..11]of boolean; a ns:array[1..100]of rec; n,m,max:integer; step:integer=1; state:arr; procedure readp; var i,j:integer; ch:char; begin readln(m,n); for i:=1 to n do begin for j:=1 to m do begin read(ch); map[i,j]:=(ch='1'); inc(max,ord( map[i,j] )); end; readln; end; end; procedure writep; var i:integer; begin for i:=1 to step do writeln( '(',ans.x,',',ans.y,')' ); end; procedure solve(x,y:integer); var tx,ty,d:integer; step_cache:integer; state_cache:arr; begin step_cache:=step; state_cache:=state; if step=max then begin writep; exit; end; for d:=1 to 4 do begin tx:=x+dir[d,1]; ty:=y+dir[d,2]; while map[tx,ty] and ( not state[tx] and(1 shl (ty-1) )>0) do begin inc(step); ans[step].x:=tx; ans[step].y:=ty; state[tx]:=state[tx] or ( 1 shl (ty-1) ); tx:=tx+dir[d,1]; ty:=ty+dir[d,2]; end; tx:=tx-dir[d,1]; ty:=ty-dir[d,2]; if (tx<>x) or (ty<>y) then solve(tx,ty); state:=state_cache; step:=step_cache; end; end; {====main====} var i,j:integer; begin assign(input,'garden.txt'); reset(input); assign(output,'garden.out'); rewrite(output); readp; for i:=1 to n do for j:=1 to m do if map[i,j] then begin a ns[1].x:=i; ans[1].y:=j; state:=1 shl (j-1); solve(i,j); state:=0; end; close(input); close(output); end.
對C語言中位運算的一點補充:(位數不同的運算數之間的運算規則)-
C語言中,位運算的對象可以是整型(int)和字符型(char)數據。(整形數據可以直接轉化成二進制數,字符型數據在內存中以它的ASCII碼值存放,也可以站化成二進制數)當兩個運算數類型不同時,位數亦會不同。遇到這種情況,系統將自動進行如下處理:
1將兩個運算數右端對齊。
2 再將位數短的一個運算數往高位擴充,即:無符號數和正整數左側用0補全;負數左側用1補全;然後對位數相等的兩個運算數,按位進行運算。
The Clocks時鐘 usaco1.4.2
考慮將如此安排在一個 3 x3 行列中的九個時鐘:
|-------| |-------| |-------|
| | | | | | |
|---O | |---O | | O |
| | | | | |
|-------| |-------| |-------|
A B C
|-------| |-------| |-------|
| | | | | |
| O | | O | | O |
| | | | | | | | |
|-------| |-------| |-------|
D E F
|-------| |-------| |-------|
| | | | | |
| O | | O---| | O |
| | | | | | | |
|-------| |-------| |-------|
G H I
目標要找一個最小的移動順序次將所有的指針指向12點。
下面原表格列出了9種不同的旋轉指針的方法,每一種方法都叫一次移動。
選擇1到9號移動方法,將會使在表格中對應的時鐘的指針順時針旋轉90度。
移動方法 | 受影響的時鐘 |
1 | ABDE |
2 | ABC |
3 | BCEF |
4 | ADG |
5 | BDEFH |
6 | CFI |
7 | DEGH |
8 | GHI |
9 | EFHI |
Example
9 9 12 9 12 12 9 12 12 12 12 12 12 12 12 6 6 6 5 -> 9 9 9 8-> 9 9 9 4 -> 12 9 9 9-> 12 12 12 6 3 6 6 6 6 9 9 9 12 9 9 12 12 12
[但這可能不是正確的方法,請看下面]
PROGRAM NAME: clocks
INPUT FORMAT
第1-3行: | 三個空格分開的數字,每個數字表示一個時鐘的初始時間,3,6,9,12。數字的含意和上面第一個例子一樣。 |
SAMPLE INPUT (file clocks.txt)
9 9 126 6 66 3 6
OUTPUT FORMAT
單獨的一行包括一個用空格分開的將所有指針指向12:00的最短移動順序的列表。
如果有多種方案,輸出那種使的連接起來數字最小的方案。(舉例來説5 2 4 6 < 9 3 1 1)。
SAMPLE OUTPUT (file clocks.out)
4 5 8 9
要求
1s,16mb
參考方法利用了9重循環,這就需要在循環內部加快運算速度,位運算是不錯的辦法。對於下面的move和k賦值不理解的同學,可以將它用計算器轉化為二進制,然後由最後一位開始三個一個看看。
//pascal代碼 program clocks; const move:array[1..9] of longint=(18911232,19136512,2363904,16810048,2134536,262657,36936,73,4617); var a,b,c,d,e,f,g,h,i,x,y,r,s,t:integer; k,l:longint; ak,bk:array[1..27] of integer; begin assign(input,'clocks.txt'); reset(input); assign(output,'clocks.out'); rewrite(output); l:=0; for x:= 1 to 3 do begin for y:= 1 to 3 do begin read(r); if r=12 then r:=0 else r:=r div 3; l:=l*8+r; end; readln; end; fillchar(ak,27,0); t:=27; d:=1; e:=1; h:=1; i:=1; { for a:= 0 to 3 do for b:= 0 to 3 do for c:= 0 to 3 do for d:= 0 to 3 do for e:= 0 to 3 do for f:= 0 to 3 do for g:= 0 to 3 do for h:= 0 to 3 do for i:= 0 to 3 do begin } k:=l; s:=0; r:=a; while r>0 do begin dec(r); k:=(k+move[1]) and 57521883; inc(s); bk[s]:=1; end; r:=b; while r>0 do begin dec(r); k:=(k+move[2]) and 57521883; inc(s); bk[s]:=2; end; r:=c; while r>0 do begin dec(r); k:=(k+move[3]) and 57521883; inc(s); b k[s]:=3; end; r:=d; while r>0 do begin dec(r); k:=(k+move[4]) and 57521883; inc(s); bk[s]:=4; end; r:=e; while r>0 do begin dec(r); k:=(k+move[5]) and 57521883; inc(s); bk[s]:=5; end; r:=f; while r>0 do begin dec(r); k:=(k+move[6]) and 57521883; inc(s); bk[s]:=6; end; r:=g; while r>0 do begin dec(r); k:=(k+move[7]) and 57521883; inc(s); bk[s]:=7; end; r:=h; while r>0 do begin dec(r); k:=(k+move[8]) and 57521883; inc(s); bk[s]:=8; end; r:=i; while r>0 do begin dec(r); k:=(k+move[9]) and 57521883; inc(s); bk[s]:=9; end; if k=0 then begin if s<t then begin t:=s; for x:= 1 to t do ak[x]:=bk[x]; end else if s=t then begin x:=0; while ak[x]=bk[x] do inc(x); if ak[x]>bk[x] then begin for y:= 1 to t do ak[y]:=bk[y]; end; end; end; for i:= 1 to t do write(ak[i],' '); close(input); close(output); end.