---
# System prepended metadata

title: 位元運算 Bitwise Operation
tags: [內湖高中程式競賽]

---

# 位元運算 Bitwise Operation
我們一般看到的數字都是十進位的數字，十進位的數字意旨每十個數字進一位，因此每位數字只能是 $0$~$9$。那麼二進位就是每二個數字進一位，數字只能是 $0$ 或者 $1$。這特性剛好與電路通電與不通電的特性合得來，因此電腦的底層儲存的都是二進位的數字

這篇主要會介紹三個東西 : 不同的進位制、資料型態儲存、布林代數。透過這三者，我們才能完整地介紹位元運算，最後會介紹位元運算在競賽上的常見用法

## 不同進位數字的表示法
### 範例說明
基本上就是數字加小括號，右下角加小標代表進位制

- 十進位 : $(1782)_{10}$
- 二進位 : $(1011)_{2}$
- 八進位 : $(3254)_{8}$

### 換種方式來看十進位、二進位與八進位
我們可以把十進位數字 $(1782)_{10}$ 粗略的這樣做拆分

$$
(1782)_{10}=1\cdot 10^{3}+7\cdot 10^{2}+8\cdot 10^{1}+2\cdot 10^0
$$

二進位數字 $(1011)_{2}$ 大概就會長這樣
$$
(1011)_{2}=1\times 2^3+0\times 2^2+1\times 2^1+1\times 2^0=(11)_{10}
$$

八進位數字 $(3254)_{8}$ 大概就會是長這樣
$$
(3254)_{8}=3\cdot8^3+2\cdot8^2+5\cdot8^1+4\cdot8^0=(1708)_{10}
$$

### $k$ 進位數字
藉由上面的規律不難發現，今天若有一個 $k$ 進位的數字，我們知道他共會由 $0$~$k-1$ 這些符號組成。設一個序列長度為 $n$ 的 $k$ 進位數字 $a$，轉成十進位可以寫成這種形式 : 

$$
a_{n-1} \cdots a_2a_1a_0=a_{n-1} \cdot k^{n-1}+\cdots+ a_2\cdot k^2+a_1\cdot k^1+a_0\cdot k^0
$$

### 十六進位
十六進位在資訊領域也是蠻常見的，基本上會把 $10$\~$15$ 寫成 $A$\~$F$。轉換方法跟前面提及的差不多，很 trivial 所以在此不多贅述

### 實作十進位轉八進位
由於十進位與二進位以外的進位法難以用現有的資料型態表達，所以直接使用字串來做為輸出。若要手算的話，短除法就可以達成了，在程式上也是差不多的作法，只是用 `while` 迴圈來實作
```cpp
string convert(int x){
    string ret;
    while(x > 1){                       // 除到變成 1 就完成了
        ret.push_back('0' + x % 8);     // 這邊就取餘數就好，記得要轉成字元
        x /= 8;                         // 短除法，每次除 8
    }
    reverse(ret.begin(), ret.end());    // 因每次都 push_back()，最後要反過來
    return ret;
}
```

要轉成其他進位制也很 trivial，請自己當作練習試試看

## 資料型態的儲存

這邊只是粗淺地講 C/C++ 中各資料型態是怎麼儲存。如果想知道完整的知識，可以去學計算機組織與架構、計算機概論與數位邏輯設計，這些課應該都會提到

### 原理說明
電腦是通過電壓的大小 (high / low) 來看要存哪一種資料，因此只會有兩種狀態 : $0$、$1$。在每一個位元 (bit) 中，我們只會用 $0$、$1$ 表示，bit 是儲存資料的基本單位，想像成格子就好

### 整數 int

電腦中儲存整數有固定的大小，每個 int 用 $4$ bytes 也就是 $32$ bits 儲存，可以用 $32$ 個格子來表示

#### Sign Bit

第一個格子叫做 sign bit，如果是 $0$ 就是正的 $1$ 就是負的

<center>
<img src="https://hackmd.io/_uploads/S1HDY1_teg.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

#### 舉例說明

以一個 $3$ bit 的系統來說，所有能代表的數字大概長這樣

|bit|value|
|---|---|
|011|3|
|010|2|
|001|1|
|000|0|
|111|-1|
|110|-2|
|101|-3|
|100|-4|

這種表示法叫做二補數，如果要把正的變成負的就要把 $0$ 變成 $1$、把 $1$ 變成 $0$，然後 $+ 1$，然後你就會發現最後的 $-4$ 不適用

### 浮點數 float

用 $4$ bytes 也就是 $32$ bits 儲存，大致可以分為三個部分 :

- sign : 正負號($S$)
- exponent : 位移量($E$)
- mantissa : 尾數($M$)

<center>
<img src="https://hackmd.io/_uploads/SyjgpkuKee.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

一個浮點數寫起來就會是 $S\times M\times 2^E$

#### 舉例說明
**$7.25$ 化成浮點數儲存**
1. 判斷正負 : 正
2. 將其化為二進制 : $111.01$
3. 將小數點移到最前面 : $1.1101$ 
發現位移量為$2$ 並捨去最前面的$1$
4. 計算 $E$ 加上 $2^8-1$ 其中 $8$ 為格子數量 : $E=(2^8-1)+2=(129)_{10}=(1000 0001)_{2}$
5. 寫答案 : $01000000111010000000000000000000$


**可以驗算一下答案**
$S=+1$
$E=129-127=2$
$M=(0.1101)_{2}+1=1.8125$
$S\times M\times 2^E=7.25$

### 備註
- 因為格子都是有限的，所以當紀錄的數字超出格子時就會溢位，導致數字出現錯誤
- 以上浮點數看不懂沒關係，不是資工系其實沒必要知道

## 布林代數
電腦處理位元運算是基於電路的邏輯閘，邏輯閘的基礎是基於布林代數，所以先來講講布林代數在幹嘛

### 簡介

布林代數是由[喬治·布爾](https://zh.wikipedia.org/zh-tw/%E4%B9%94%E6%B2%BB%C2%B7%E5%B8%83%E5%B0%94)提出。主要是處理邏輯問題，將邏輯從哲學轉化成數學。整個代數只有兩種元素 : $\text{Ture}$ ($1$), $\text{False}$ ($0$)，有時會寫成 T/F。這東西被信息學之父[克勞德·香農](https://zh.wikipedia.org/zh-tw/%E5%85%8B%E5%8A%B3%E5%BE%B7%C2%B7%E9%A6%99%E5%86%9C)用於處理電路問題，現今主要用於數學論證與電路設計上 ||討厭數位邏輯設計的，應該知道要怪誰了吧||

### 運算子

主要有三種運算子
- OR $\lor$ : 或 
- AND $\land$ : 且
- NOT $\neg$ : 否

要注意的是，OR $\lor$ 跟 AND $\land$ 是二元運算子 (左右各放一個數字)，NOT $\neg$ 是一元運算子 (只能放右邊的數字)

### 真值表 Truth Table
我們可以用「真值表」來看所有的運算，有課本寫說真值表就是運算子的定義。左邊是 $P$、$Q$ 分別代入的數字 (T/F)；右邊是運算結果

**這是 OR $\lor$ 跟 AND $\land$ 的真值表**
|$P$|$Q$|$P\lor Q$|$P\land Q$|
|----|----|----|----|
|0|0|0|0|
|0|1|1|0|
|1|0|1|0|
|1|1|1|1|

**這是 NOT$\neg$ 的真值表**
|$P$|$\neg P$|
|----|----|
|0|1|
|1|0|

### 互斥或 XOR
有了上面三個運算子，我們就可以進行運算，也可以創造出新的運算子。接下來的 XOR $\oplus$ 運算子也算是很常用的運算子，真值表如下

|$P$|$Q$|$P\oplus Q$|
|----|----|----|
|0|0|0|
|0|1|1|
|1|0|1|
|1|1|0|

以下真值表也可以當作我們合成新運算子的工具，算是我們從基本的 AND、OR 與 NOT 推導出來的結果

|$P$|$Q$|$P\lor Q$|$P\land Q$|$\neg(P\land Q)$|$(P\lor Q)\land \neg(P\land Q)$|$P\oplus Q$|
|----|----|----|----|----|----|----|
|0|0|0|0|1|0|0|
|0|1|1|0|1|1|1|
|1|0|1|0|1|1|1|
|1|1|1|1|0|0|0|

會發現 $(P\lor Q)\land \neg(P\land Q)$ 與 $P\oplus Q$ 的真值表是相同的，這時我們就可以寫作 : 
$$
P\oplus Q\equiv (P\lor Q)\land \neg(P\land Q)
$$

符號 $\equiv$ 我們稱為邏輯等價

### 用於敘述

設 $P$、$Q$ 敘述分別為「會下雨」、「地面會溼」，那這樣的敘述「若下雨，則地板會溼」就可以寫成 $\neg P\lor Q$，可以用真值表來寫寫看

|情況|$P$|$Q$|$\neg P\lor Q$|結果|
|----|----|----|----|----|
|沒下雨、地面沒溼|0|0|1|成立 (沒有下雨，無論地面有沒有溼都是對的)|
|沒下雨、面板溼|0|1|1|成立 (同上)|
|下雨、地面沒溼|1|0|0|不成立|
|下雨、地面溼|1|1|1|成立|

冷知識 : 「若 $p$ 則 $q$」的邏輯我們會用運算子 $p\rightarrow q$ 來表示

### 備註
這部分非常深，簡化這些邏輯有非常多技巧，在不同領域應用也很多，建議自己找書來看。關鍵字如下 : 
- [mathatical logic](https://www.reddit.com/r/math/comments/1418n9n/whats_a_good_book_to_learn_rigorous_mathematical/)
- [digital logic design](https://www.tenlong.com.tw/products/9789867696274)

## 位元運算
當我們使用布林代數來處理位元問題時，就稱為位元運算。為了與邏輯運算做出區隔，跟別人講的時候記得加上 (bitwise)，比如說 bitwise OR、bitwise AND 等等

### 位元運算子

邏輯運算與位元運算的整體使用起來非常相似，只是邏輯運算只作用於一個 T/F，位元運算則是作用於一整排 int (bit string) 上的 0/1

|運算|邏輯運算子|位元運算子|
|---|---|----|
|OR|$\lor$|\||
|AND|$\land$|&|
|XOR|$\oplus$|^|
|NOT|$\neg$|\~|

比較特別的是 $>>$、$<<$，在十進位的意思就是 $\div 2$、$\times 2$，但是在二進位的意思就是左移與右移

|運算|數學運算子|位元運算子|
|---|---|---|
|左移|$\div 2$ (向下取整)|<<|
|右移|$\times 2$|>>|

舉例來說

$$
5 \times 2 = 5 << 1 = (101)_2 << 1=(1010)_2=10
$$
$$
5 \div 2 = 5 >> 1 = (101)_2 >> 1=(10)_2=2
$$

左移後最右邊的位置要補 $0$

**注意 : C 語言中的位元運算子只適用於整數 int**

### 舉例說明

**在 C 語言寫 `5|6` 就會得到 $7$ :**

- $5=(101)_{2}$、$6=(110)_{2}$

<center>
<img src="https://hackmd.io/_uploads/Hy4nrl_Yxe.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

**在 C 語言寫 `5^6` 就會得到 $3$ :**

- $5=(101)_{2}$、$6=(110)_{2}$

<center>
<img src="https://hackmd.io/_uploads/SJ9NLlOFle.png" style="margin: 0 auto; display: block;  width: 600px">
</center>

### 備註
- 這些位元運算子也可以加個`=`變成賦值運算子
- 由於位元運算子在 C 語言優先度非常低，要使用時記得括號括好
- 有一個好用的函式 `__builtin_popcount()`，可以帶入數字計算二進位中 $1$ 的數量


## 程式範例

### 二進位轉十進位 
可以先用字串儲存二進位
```cpp
// bi[]: 輸入的字串, ans: 儲存答案
void binary_to_decimal(char bi[], int &ans){
    int len = strlen(bi), ret = 0, ep = 1;
    // len: 字串長度, ret: 紀錄運算過程, ep: 2的次方數
    for(int i = len - 1; i >= 0; i--){
        ret += (bi[i] - '0') * ep;
        ep <<= 1;
    }
    ans = ret;
}
```

### 十進位轉二進位
答案以字串儲存
```cpp
void decimal_to_binary(int &n, char ans[]){
    int idx = 0; // 這就是陣列編號
    while(n){ // 這過程相當於短除法
        ans[idx++] = (n & 1) + '0'; // (n & 1) 就是 (n % 2)
        n >>= 1; // 除以 2
    }
    ans[idx] = '\0'; // 字串末尾一定要加上 '\0'
    int len = strlen(ans); // 字串長度
    for(int i = 0; i < len / 2; i++){ // 字串前後reverse過來
        char tmp = ans[i]; // 做交換
        ans[i] = ans[len - 1 - i];
        ans[len - 1 - i] = tmp;
    }
}
```

## 位元運算常見應用
以下這些都是常見手法，你可以自己寫寫真值表看檢驗是不是正確的。如果你是個注意力驚人的同學，應該可以注意到更多「trivial」的做法 (?
### 計算 $2^i$

```cpp
int ans = (1 << i); // 1 向左移 i 格就是算完了，輕輕鬆鬆
```

### 找出整數 $s$ 的第 $i$ 位 bit 是 $0$ 還是 $1$
```cpp
int ans = s & (1 << i);
if (ans) cout << "1";    // 若不為 0 則 ans 會是 2^i
else cout << "0";        // 若為 0 則 ans 會是 0
```

### 整數 $x$ 除以 $2$ / 乘以 $2$
```cpp
int d = x >> 1; // x 除以 2
int m = x << 1; // x 乘以 2
```

注意到 $x\cdot 2^i$ 就可以這樣寫

```cpp
int d = x >> i; // x 除以 2^i
int m = x << i; // x 乘以 2^i
```

小心這裡的除法都是向下取整，因為資料型態 `int` 無法儲存到小數位

### $0$/$1$ 取反
```cpp
x = ~x; // 其實就是 bitwise NOT
```

### 取整數 $a$ 的補數
前面有提到我們用的系統是二補數的整數系統，因此以下兩種寫法是一樣的 (不溢位的話)
```cpp
int ans = -a;
```

```cpp
int ans = (~a) + 1;
```

### 將 $s$ 的第 $i$ 位設為 $1$
如果第 $i$ 位已經是 $1$ 了，就不會改到任何的值
```cpp
s |= (1 << i);
```

### 將 $s$ 的第 $i$ 位 $0$/$1$ 翻轉
望周知，這種寫法不會改變第 $i$ 位以外的值，因為 $0\oplus0=0$ 且 $1\oplus 0=1$
```cpp
s ^= (1 << i);
```

### 將 $s$ 的第 $i$ 位設為 $0$
如果第 $i$ 位已經是 $0$ 了，就不會改到任何的值
```cpp
s &= ~(1 << i);
```

### 求出 $x$ 最低的 $1$ 在第幾位
假設答案是第 $i$ 位，這裡會回傳 $2^i$
```cpp
int ans = x & -x;
```

### 判斷整數 $x$ 是不是 $2$ 的冪次
```cpp
return (x & -x) == x;
```

### 整數 $x, y$ 交換
相當於 `swap(x, y)`
```cpp
x = x ^ y;
y = y ^ x;
x = x ^ y;
```

**這寫法有點玄，但是容我用幾句解釋 :**
- 設 $z=x\oplus y$，則 $z$ 所儲存的是「每一位是否不同」的 bit string
- $x\oplus z$ 就是 $x$ 把每一位與 $y$ 不相同的 bit 翻轉
- $y\oplus z$ 就是 $y$ 把每一位與 $x$ 不相同的 bit 翻轉

**舉個例子 :** 
$x=10$ 與 $y=12$ 交換得到 $x'=12$ 與 $y'=10$
|第 $i$ 位|$x$|$y$|$z$ (是否不同)|$x'=x\oplus z$|$y'=y\oplus z$|
|---|---|---|---|---|---|
|4|1|1|0|1|1|
|3|0|1|1|1|0|
|2|1|0|1|0|1|
|1|0|0|0|0|0|

**所以程式碼就是這個意思**
1. 我們把 $z$ 的角色交給 $x$
```cpp
x = x ^ y;
```

2. 接下來把 $y$ 變成原本的 $x$
```cpp
y = y ^ x;
```

3. 最後把原本 $x$ 中「不相同的位元」翻過來
```cpp
x = x ^ y;
```

**最後，這裡還有個噁心的寫法**
```cpp
x ^= y ^= x ^= y;
```

### 判斷整數 $x$ 是不是奇數
這很簡單
```cpp
if(x & 1) cout << "YES\n";
else cout << "NO\n";
```

## 題目練習

[Zerojudge c461. apcs 邏輯運算子 (Logic Operators)](https://zerojudge.tw/ShowProblem?problemid=c461)
[Zerojudge e797. p4. 數位邏輯運算](https://zerojudge.tw/ShowProblem?problemid=e797)
[Zerojudge a132. 10931 - Parity](https://zerojudge.tw/ShowProblem?problemid=a132)
[Zerojudge b005. 布林矩陣的等價短陣](https://zerojudge.tw/ShowProblem?problemid=b005)
[Zerojudge a414. 位元運算之進位篇](https://zerojudge.tw/ShowProblem?problemid=a414)
[Zerojudge d632. C and S ??](https://zerojudge.tw/ShowProblem?problemid=d632)
[Zerojudge e253. a + b problem](https://zerojudge.tw/ShowProblem?problemid=e253)
[Zerojudge e545. 10019 - Funny Encryption Method](https://zerojudge.tw/ShowProblem?problemid=e545)
[Zerojudge e799. p6. 資工系的浪漫](https://zerojudge.tw/ShowProblem?problemid=e799)
[CSES Range Xor Queries](https://cses.fi/problemset/task/1650/) (bitwise XOR 可以差分喔)
[CSES Prime Multiples](https://cses.fi/problemset/task/2185)

----

## 參考資料
- [海大競程 - Time Complexity and Bitwise](https://hackmd.io/@LeeShoWhaodian/ComplexityandBitwise#/)
- [維基百科 - Bitwise operation](https://en.wikipedia.org/wiki/Bitwise_operation)
- [台師大演算法筆記 - bit](https://web.ntnu.edu.tw/~algo/Bit.html)
- [哈哈屁眼 - 位元運算](https://hackmd.io/@HAHAHAHAHA/bitwise)

----

> [ShanC 程式競賽筆記](https://hackmd.io/@ShanC/B1ouGxqcC)
> 作者: ShanC
> 更新: 2025/8/25