# 集合論與位運算 集合的概念在高中數學就接觸過,大學則學到了位元的四則運算(AND、OR、NOT、XOR等等)。 集合論與位元運算,兩者看似毫不相關的東西,卻有著奇妙的數學關係聯繫著。 ## 集合轉換成位元表示 集合由元素組成,例如 S = {1, 2, 3}。在程式領域遇到有關集合的題目,通常使用 hash table 紀錄一個集合裡面出現的元素。 在集合論中,我們學到了聯集(∪)、交集(∩)以及其他等概念。若我們要找到兩個集合的交集,就要走遍每個 hash table 的元素。 換個思維思考,若我們使用位元 0 代表數字沒有在集合中,位元 1 代表數字有在集合裡,那就可以使用位元表達一個集合。二進位由右至左第 i 個 bit 代表 i 是否在集合中。例如 S = {1, 2, 3} 就可用 1110 表示,反之亦然。 另一個很有意思的點是,將上面集合 S 中的每個數字 i 視為 2 的 i 次方,則合為 $2^1 + 2^2 + 2^3$ = 13, 正好就是二進 1110 轉換為十進位的數字。 ## 集合運算 在一些特定的情況下,若輸入的變數範圍較少,我們可以使用位元表達一個元素是否存在於集合中,將一個集合表達成一個二進位數字,就可使用位元運算取代集合運算。由於位元運算就是 CPU 內部最基本的四則運算,在特定場合下可以提升程式執行效率。 我們可以將其應用場景分為以下四類: * 集合與集合 * 集合與元素 * 走訪整個集合 * 列出所有集合 依據每個類別進行介紹 ## 集合-集合 下表為集合與集合之間的位元運算關係,其中 ^ 代表 XOR、& 代表 AND、| 為 OR 而 ~ 為 NOT 運算。 | 術語 | 集合 | 位元運算 | 集合範例 | 位元運算範例 | |----------|-----------------|-----------|---------------------------------------------------|----------------------------| | 交集 | $A ∩ B$ | $a\ \&\ \ b$ | {0, 2, 3} ∩ {0, 1, 2} = {0, 2} | 1101 & 0111 = 0101 | | 聯集 | $A ∪ B$ | $a\ \| \ b$ | {0, 2, 3} ∪ {0, 1, 2} = {0, 1, 2, 3} | 1101 \| 0111 = 1111 | | 對稱差 | $A △ B$ | $a \oplus b$ | {0, 2, 3} △ {0, 1, 2} = {1, 3} | 1101 ^ 0111 = 1010 | | 差集 | $A \setminus B$ | $a\ \& \ (∼b)$ | {0, 2, 3} \ {1, 2} = {0, 3} | 1101 & 1001 = 1001 | | | 包含於 | $A ⊆ B$ | $a\ \&\ b = a, a\ \|\ b = b$ | {0, 2} ⊆ {0, 2, 3} | 0101 & 1101 = 0101, 0101 \| 1101 = 1101 | 針對以上運算進行推理如下, A = {0, 2, 3},二進表示法為 1101 B = {0, 2},二進位表示法為 0101 ### 交集 A & B = 0101,根據第 i 個 bit 的值,判斷數字 i 是否有出現在集合中,得 0、2 為集合 A ∩ B。 轉換為集合表示法為 A ∩ B = {0, 2} ### 聯集 A | B = 1101,聯集代表有出現在集合 A 或出現在集合 B 中。 採用 OR 位元運算,只要有一個 bit 為 1,OR 運算後該 bit 的結果絕對為 1。 得 A ∪ B = {0, 2, 3}。 ### 對稱差 數學上,兩個集合的對稱差是只屬於其中一個集合,而不屬於另一個集合的元素組成的集合。以繁體中文公式表達為: 對稱差 = 聯集 − 交集 以位元運算思考,上述中文意思 在 a 與 b 的二進位中,同一個位置的 bit 不能有兩個 1,因此使用 XOR 運算就可處理,得: $a \oplus b$ ### 差集 A ∖ B 表示所有屬於集合 A 但不屬於集合 B 的元素組成的集合,也就是 A 與 非 B 集合的交集。用公式可表達 A ∖ B = A - B, 以位元運算來說,若 a 為 1101,b 為 0101,預期結果應為 1000。為了達到這個結果,需要將 b 中 bit 為 1 的地方轉為 0(移除屬於 B),而 0 轉為 1(加入不屬於 B),再將兩者取交集。轉換為位元運算,先對 b 做 NOT 運算(取反),再進行 AND 運算(取交集), 得 A \ B = a & (~b) ### 包含於 A⊆B 代表 A 是 B 的一個子集合,任何出現在 A 的元素必出現在 B。A 與 B 的交集為 A 集合本身,A 與 B 的聯集為 B 集合。以二進位來看,第 i 個 bit 在 a 與 b 中皆為 1。由上述推論可得: a & b = a 且 a | b = b ## 集合-元素 類別屬於一個集合中對單一元素的操作,常使用 << (左移) 與 >> (右移) 運算子 | 術語 | 集合 | 位元運算 | 集合範例 | 位元運算範例 | |--------------|---------------------------------|---------------------------|-----------------------------------|----------------------------------| | 空集合 | $∅$ | 0 | {} | 0 | | 單元素集合 | ${i}$ | $1 << i$ | {2} | $1 << 2$ | | 全集 | $$ U = \{ 0, 1, 2, \dots, n-1 \} $$ | $(1 << n) - 1$ |$\{ 0, 1, 2, 3 \}$ | $(1 << 4) - 1$ | 補集 | $$ A^C = U \setminus A $$ | $((1 << n) - 1) ⊕ s$ | $\( U = \{0, 1, 2, 3\} \), \( $A^C$\{1, 2\} = \{0, 3\} \)$ | $1111 ⊕ 0110 = 1001$ | | 屬於 | $$ i \in S $$ | (s >> i) & 1 = 1 | 2 ∈ {0, 2, 3} | (1101 >> 2) & 1 = 1 | | 不屬於 | $$ i \notin S $$ | (s >> i) & 1 = 0 | 1 ∉ {0, 2, 3} | (1101 >> 1) & 1 = 0 | | 新增元素 | $$ S \cup \{i\} $$ | $s \ \|\ (1 << i)$ | $\{0, 3\} ∪ \{2\} = \{0, 2, 3\}$ | 1001 | (1 << 2) = 1101 | | 刪除元素 | $$ S \setminus \{ i \} $$ | \( s \& \sim (1 << i) \) | \( \{ 0, 2, 3 \} \setminus \{ 2 \} \) | \( 1101 \& \sim (1 << 2) \) | | 刪除元素 (一定在集合中) | $$ S \setminus \{ i \}, \ i \in S $$ | \( s \oplus (1 << i) \) | \( \{ 0, 2, 3 \} \setminus \{ 2 \} \) | \( 1101 \oplus (1 << 2) \) | | 刪除最小元素 | | $$ s \ \&\ (s - 1) $$ | | | ### 單元素集合 集合只有單一元素,方法類似 set bit。 1 << i ### 全集合 給定的範圍內所有數字皆位於集合內,所以 bits 皆為 1,得 (1 << (n + 1)) - 1 ### 補集 ### 是否屬於集合 若數字 i 數於集合,則第 i 個 bit 為 1,反之為 0,得: (1 << i) & 1 = 1(屬於) (1 << i) & 1 = 0(不屬於) ### 新增元素 新增數字 i 到集合中,等價於將二進位的第 i 個 bit 設為 1,也就是 set bits。 s | (1 << i) ### 移除元素 將數字 i 從集合移除,等價於將二進位的第 i 個 bit 設為 0,也就是 clear bits。 s & ~(1 << i) ### 移除最小元素 最小元素,代表二進位由低到高位元數來第一個非 0 bit,將其設為 0,得 s = 101100 s - 1 = 101011 s & (s - 1) = 101000 ## 走訪整個集合 元素範圍從 0 到 n - 1,判斷 i 是否在集合 s 中。 ```cpp! for(int i = 0; i < n;i ++){ if((1 << i) & 1){ // i 在 s 中 } } ``` 也可以 ## 列出所有集合 元素範圍從 0 到 n - 1,列出所有集合,從空集合到全集 U ```cpp! for(int s = 0; s | (1 << n); s++){ // 處理 } ``` ### ## Ref 本文資訊來自以下文章的理解 [從集合論到位運算,常見位元運算技巧分類總結!](https://leetcode.cn/circle/discuss/CaOJ45/)