位運算詳解及競賽常見用法入門
位運算
程式中的所有數在計算機記憶體中都是以二進位制的形式儲存的。位運算說穿了,就是直接對整數在記憶體中的二進位制位進行操作。比如,and運算本來是一個邏輯運算子,但整數與整數之間也可以進行and運算。舉個例子,6的二進位制是110,11的二進位制是1011,那麼6
and 11的結果就是2,它是二進位制對應位進行邏輯運算的結果(0表示False,1表示True,空位都當0處理)。
各種位運算的使用
=== 1. & 運算 ===
and 運算通常用於二進位制取位操作, 例如一個數 and 1的結果就是取二進位制的最末位。 這可以用來判斷一個
整數的奇偶, 二進位制的最末位為 0 表示該數為偶數, 最末位為 1 表示該數為奇數.
=== 2. |運算===
or 運算通常用於二進位制特定位上的無條件賦值, 例如一個數 or 1 的結果就是把二進位制最末位強行變成 1。
如果需要把二進位制最末位變成 0, 對這個數 or 1 之後再減一就可以了, 其實際意義就是把這個數強行變成最接
近的偶數。
=== 3. ^ 運算===
xor 運算通常用於對二進位制的特定一位進行取反操作, 因為異或可以這樣定義: 0 和 1 異或 0 都不變, 異或 1
則取反。
=== 4. ~ 運算 ===
~ 運算的定義是把記憶體中的0和 1 全部取反。 使用 ~ 運算時要格外小心, 你需要注意整數型別有沒有符
號。
=== 5. << 運算===
a << b就表示把a轉為二進位制後左移b位 ( 在後面添b個0)。
=== 6. >> 運算===
和 >> 相似, a >> b 表示二進位制右移 b 位( 去掉末 b 位), 相當於 a 除以 2 的 b 次方( 取整)。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
分割線(上面全是粘的)
在使用位運算的時候,需要注意的一點是他的優先順序
上面這個圖是在別的部落格裡找到=。= 好難記。。。
其實只要記住這幾點就差不多了:
1.取非比較快,僅次() .
2.其餘需要兩個變數的位運算比算術運算(如+ - * / )要低,比賦值運算(=),邏輯運算要高(||, &&)
3.左移右移比較調皮,跳到關係運算(> < <= == !=)上面
如果還是不好記。。那就括號大法好
位運算這麼麻煩,為什麼還要用他呢,因為他的速度特別快,可以用位運算來實現一些功能
比較常用的幾個功能:
a乘以2除以2:<< >>
判斷奇偶性:a&1,為零為偶,否則奇數
交換兩個數:a^=b^=a^=b;(可以不借助別的變數)
判斷兩個數是否相同:a^b==0則相同,否則不同(可以快速從只有一個數出現一次其餘都出現偶數次的一串數裡找到這個數,
1 1 2 3 3 3 3 4 4 ,從頭異或一遍,就可以得到2)其實異或還有更大作用的用法,下面會提到
去掉二進位制最後一個1:i-=i&-i,也可以寫作i=i&(i-1),在樹狀數組裡會用到
下面舉幾個簡單基礎的可能會用到位運算的常見演算法和有趣的題:
1.首先肯定是快速冪,實用且簡單,位運算則大大節省了時間(其實因為快速冪本身就很快,不選擇位運算一般的題也能ac)
下面是程式碼:
#include<stdio.h>
int main()
{
int n,a;
scanf("%d %d",&n,&a);
int t=n,ans=1;
while(a)
{
if(a&1)
ans*=t;
t*=t;
a>>=1;
}
printf("%d\n",ans);
return 0;
}
像這些比較有名的演算法,只說用法,就不說原理了,
還有快速冪延伸的矩陣快速冪。。。
2.
給定一個數組A, 長度為n,求下面這段程式的值
ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
ans ^= A[i] + A[j];
多組測試資料,第一行一個整數T,T<=20,表示資料的組數,每組資料一行包含n, m, z, l; A[1] = 0; A[i] = (A[i-1] * m + z) mod l; 1 <= n, m, z, l <= 5 * 10^5
每組資料輸出Case #x: ans 其中x代表第幾組測試資料,從1開始,ans代表程式中的ans.
複製2 3 5 5 7 6 8 8 9
Case #1: 14 Case #2: 16
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
水題一秒思考分界線
這道題不是很難,但也挺有意思的,重點考察對異或的理解,直接模擬肯定是過不了的,A[i] + A[j]=A[j] + A[i];所以除了主對角線上的項只異或了一遍外,其餘都異或了兩遍,異或兩遍不就為零了,說以直接把主對角線上的乘二異或起來就好了
#include<stdio.h>
int main()
{
long long int t,ans,sum,a;
int n,m,i,z,l;
scanf("%lld",&t);
for(int j=1;j<=t;j++)
{
sum=0;
ans=0;
scanf("%d %d %d %d",&n,&m,&z,&l);
a=0;
for(i=1;i<n;i++)
{
a=(a*m+z)%l;
ans^=(a*2);
}
printf("Case #%d: %lld\n",j,ans);
}
}
3.
數數字
給你一個整數數列,保證只有一個數出現過奇數次,輸出它。
多組測試資料。 每組測試資料第一行為一個整數n,代表該數列元素個數。(1 <= n <= 500000) 第二行為n個整數ai,以空格隔開。(-1000000 <= ai <= 1000000)
輸出一行表示這個出現奇數次的數。
複製5 2 3 2 3 1 7 6 6 6 2 6 6 6
1 2這個題更水,懂上面那道題了,這個還不秒切?
全部異或起來就出答案了,因為異或符合交換律,而別的數異或0等於本身,出現偶數次的數會全部異或成0,奇數次的數自然也就出來了
#include<stdio.h>
int main()
{
int a,b;
int n;
while(scanf("%d",&n)!=EOF)
{
scanf("%d",&a);
for(int i=1;i<n;i++)
{
scanf("%d",&b);
a^=b;
}
printf("%d\n",a);
}
return 0;
}
4.
數數字 2
輸入一些數字,int範圍內,大部分數字都出現了三次,只有一個數字出現了一次,輸出這個數字。
第一行是數字的個數n,n < 2000000,接下來每行一個數字。
輸出出現了一次的數字
複製4 1 1 1 3
3
這個要比上面的複雜一些,出現三次和出現一次,都是奇數次,異或起來好像沒什麼區別啊=。=看了半天,想了半天絲毫沒有思路,沒辦法,只能搜題解了
因為十進位制的數字在計算機中是以二進位制的形式儲存的,所以陣列中的任何一個數字都可以轉化為類似101001101這樣的形式,int型別佔記憶體4個位元組,也就是32位。那麼,如果一個數字在陣列中出現了三次,比如18,二進位制是10010,所以第一位和第四位上的1,也都各出現了3次。
因此可以用ones代表只出現一次的數位,twos代表出現了兩次的數位,xthrees代表出現了三次的數位。
public int singleNumber(int[] A) {
int ones=0;
int twos=0;
int xthrees=0;
for(int i = 0;i <A.length;i++){
twos ^= (ones&A[i]);
ones ^= A[i];
xthrees = ~(ones&twos);
twos &= xthrees;
ones &=xthrees;
}
return ones;
}
不過說實話,我沒怎麼看懂。。。。不過後來從某飛君學到了一個更簡單的做法,按位記錄每一個數那個位的出現的次數,最後把這些位的次數對三取餘,再轉為十進位制就是要求的數。雖然沒有用到位運算,但這是一種按位去考慮的方法,值得一記。
#include<stdio.h>
#include<string.h>
int main()
{
int dight[40],a,b=0;
int n,i;
memset(dight,0,sizeof(dight));
scanf("%d",&n);
while(n--)
{
scanf("%d",&a);
i=0;
while(a)
{
dight[i++]+=a%2;
a/=2;
}
}
int t=1;
for(i=0;i<32;i++)
{
dight[i]%=3;
b+=dight[i]*t;
t*=2;
}
printf("%d\n",b);
return 0;
}
這個複雜度是nlogn,所以sort也能做,僅供參考
5.nim博弈問題:
通常的Nim遊戲的定義是這樣的:有若干堆石子,每堆石子的數量都是有限的,合法的移動是“選擇一堆石子並拿走若干顆(不能不拿)”,如果輪到某個人時所有的石子堆都已經被拿空了,則判負(因為他此刻沒有任何合法的移動)。
對於一個Nim遊戲的局面(a1,a2,...,an),它是必贏狀態當且僅當a1^a2^...^an=0,其中^表示異或(xor)運算。
Rabbit and Grass
Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 3767 Accepted Submission(s): 2844
Problem Description 大學時光是浪漫的,女生是浪漫的,聖誕更是浪漫的,但是Rabbit和Grass這兩個大學女生在今年的聖誕節卻表現得一點都不浪漫:不去逛商場,不去逛公園,不去和AC男約會,兩個人竟然貓在寢食下棋……
說是下棋,其實只是一個簡單的小遊戲而已,遊戲的規則是這樣的:
1、棋盤包含1*n個方格,方格從左到右分別編號為0,1,2,…,n-1;
2、m個棋子放在棋盤的方格上,方格可以為空,也可以放多於一個的棋子;
3、雙方輪流走棋;
4、每一步可以選擇任意一個棋子向左移動到任意的位置(可以多個棋子位於同一個方格),當然,任何棋子不能超出棋盤邊界;
5、如果所有的棋子都位於最左邊(即編號為0的位置),則遊戲結束,並且規定最後走棋的一方為勝者。
對於本題,你不需要考慮n的大小(我們可以假設在初始狀態,棋子總是位於棋盤的適當位置)。下面的示意圖即為一個1*15的棋盤,共有6個棋子,其中,編號8的位置有兩個棋子。
大家知道,雖然偶爾不夠浪漫,但是Rabbit和Grass都是冰雪聰明的女生,如果每次都是Rabbit先走棋,請輸出最後的結果。
Input 輸入資料包含多組測試用例,每個測試用例佔二行,首先一行包含一個整數m(0<=m<=1000),表示本測試用例的棋子數目,緊跟著的一行包含m個整數Ki(i=1…m; 0<=Ki<=1000),分別表示m個棋子初始的位置,m=0則結束輸入。
Output 如果Rabbit能贏的話,請輸出“Rabbit Win!”,否則請輸出“Grass Win!”,每個例項的輸出佔一行。
Sample Input 2 3 5 3 3 5 6 0
Sample Output Rabbit Win! Grass Win!
#include<stdio.h>
int main()
{
int ans,a,m;
while(~scanf("%d",&m))
{
ans=0;
if(m==0)
break;
while(m--)
{
scanf("%d",&a);
ans^=a;
}
if(ans==0)
printf("Grass Win!\n");
else
printf("Rabbit Win!\n");
}
return 0;
}
只介紹用法,至於為什麼要用異或,可以搜nim博弈的相關部落格
6.樹狀陣列
樹狀陣列是用於求區間和,同時支援單點更新
優點就是程式碼長度短,可以求解一些線段樹的題
模板:
#include<stdio.h>
#define max_n 1000
int bit[max_n+1],n;
int sum(int x)
{
int sum=0;
while(x>0)
{
sum+=bit[x];
x-=x&-x;
}
return sum;
}
void add(int x,int a)
{
while(x<=n)
{
bit[x]+=a;
x+=x&-x;
}
return ;
}
int main()
{
int x,i,a;
scanf("%d",&n);
for(i=1;i<=n;i++)
{
scanf("%d",&a);
add(i,a);
}
scanf("%d",&x);
printf("%d\n",sum(x));//前x個數的和
scanf("%d",&a);
add(x,a);//單點更新
printf("%d\n",sum(x));
return 0;
}
7.判斷一個數x是不是2的某次方
x&(x-1)==0?YES : NO;
總之,位運算既騷氣又高效,如果能夠靈活掌握對自己將會是一個很大的提高。