1. Giới thiệu về phép toán bit

1.1 Phép toán bit và hệ nhị phân

Phép toán bit (Bit Operation): Trong máy tính, số được lưu trữ dưới dạng “hệ nhị phân (Binary)“. Phép toán bit là việc thực hiện các phép toán trực tiếp trên biểu diễn nhị phân của số. Sử dụng phép toán bit trong chương trình có thể cải thiện hiệu suất của chương trình đáng kể.

Trước khi tìm hiểu về phép toán bit trên số nhị phân, chúng ta hãy tìm hiểu về khái niệm “số nhị phân”.

Số nhị phân (Binary): Là số được biểu diễn bằng hai chữ số 0 và 1. Mỗi chữ số 0 hoặc 1 trong số nhị phân được gọi là “bit”.

Trong hệ thập phân, chúng ta sử dụng 10 chữ số từ 0 đến 9, và quy tắc cộng là “đầy 10 nhớ 1”. Ví dụ:

  1. : cộng với bằng .
  2. : cộng với vượt quá 10, theo quy tắc “đầy 10 nhớ 1”, kết quả là .

Trong số nhị phân, chúng ta chỉ có 2 chữ số 0 và 1, và quy tắc cộng là “đầy 2 nhớ 1”. Ví dụ:

  1. : cộng với bằng .
  2. : cộng với vượt quá 2, theo quy tắc “đầy 2 nhớ 1”, kết quả là .
  3. .

1.2 Chuyển đổi số nhị phân

1.2.1 Chuyển đổi từ nhị phân sang thập phân

Trong hệ thập phân, số có thể hiểu là , tương đương với , tức là .

Tương tự, trong số nhị phân, có thể hiểu là , tức là .

Chúng ta có thể chuyển đổi một số nhị phân sang thập phân bằng cách này.

1.2.2 Chuyển đổi từ thập phân sang nhị phân

Phương pháp chuyển đổi từ thập phân sang nhị phân là: chia cho hai, lấy phần dư, sắp xếp ngược.

Chúng ta lấy ví dụ với số thập phân .

Chúng ta lấy ngược lại từng phần dư tính toán, ta được .

2. Các phép toán cơ bản trên bit

Dựa trên hệ nhị phân, chúng ta có thể thực hiện các phép toán bit tương ứng trên số nhị phân. Có tổng cộng phép toán bit cơ bản, bao gồm: “Phép toán AND”, “Phép toán OR”, “Phép toán XOR”, “Phép toán NOT”, “Phép toán dịch trái” và “Phép toán dịch phải”.

Ở đây, “Phép toán AND”, “Phép toán OR”, “Phép toán XOR”, “Phép toán dịch trái” và “Phép toán dịch phải” là phép toán hai ngôi.

  • ”Phép toán AND”, “Phép toán OR”, “Phép toán XOR” là phép toán trên từng bit tương ứng của hai số nhị phân, tức là phép toán hai ngôi.
  • ”Phép toán dịch trái” và “Phép toán dịch phải” là phép toán dịch các bit của số nhị phân trái hoặc phải, với số bít dịch được chỉ định bên phải, và các bit mới được thêm vào là 0.

Còn “Phép toán NOT” là phép toán một ngôi, áp dụng lên một số nhị phân để đảo ngược từng bit của số đó.

Chúng ta hãy xem các quy tắc của phép toán bit này trước khi đi vào chi tiết.

Toán tửMô tảQuy tắc
|Phép toán ORChỉ cần có ít nhất một bit tương ứng là 1, kết quả sẽ là 1.
&Phép toán ANDChỉ khi cả hai bit tương ứng đều là 1, kết quả mới là 1.
<<Phép toán dịch tráiDịch trái toàn bộ các bit của số nhị phân. << chỉ định số bit dịch, bit cao bị bỏ đi, bit thấp được thêm vào là 0.
>>Phép toán dịch phảiDịch phải toàn bộ các bit của số nhị phân. >> chỉ định số bit dịch, bit thấp bị bỏ đi, bit cao được thêm vào là 0.
^Phép toán XORKhi hai bit tương ứng khác nhau, kết quả là 1, khi hai bit tương ứng giống nhau, kết quả là 0.
~Phép toán NOTĐảo ngược từng bit của số nhị phân, chuyển 1 thành 0 và 0 thành 1.

2.1 Phép toán AND

Phép toán AND: Toán tử AND là &. Chức năng của nó là thực hiện phép toán AND trên từng bit tương ứng của hai số nhị phân.

  • Quy tắc phép toán AND: Chỉ khi cả hai bit tương ứng đều là 1, kết quả mới là 1.
    • 1 & 1 = 1
    • 1 & 0 = 0
    • 0 & 1 = 0
    • 0 & 0 = 0

Ví dụ, thực hiện phép toán AND trên số nhị phân , kết quả là .

2.2 Phép toán OR

Phép toán OR: Toán tử OR là |. Chức năng của nó là thực hiện phép toán OR trên từng bit tương ứng của hai số nhị phân.

  • Quy tắc phép toán OR: Chỉ cần có ít nhất một bit tương ứng là 1, kết quả sẽ là 1.
    • 1 | 1 = 1
    • 1 | 0 = 1
    • 0 | 1 = 1
    • 0 | 0 = 0

Ví dụ, thực hiện phép toán OR trên số nhị phân , kết quả là .

2.3 Phép toán XOR

Phép toán XOR: Toán tử XOR là ^. Chức năng của nó là thực hiện phép toán XOR trên từng bit tương ứng của hai số nhị phân.

  • Quy tắc phép toán XOR: Khi hai bit tương ứng khác nhau, kết quả là 1, khi hai bit tương ứng giống nhau, kết quả là 0.

  • 0 ^ 0 = 0

  • 1 ^ 0 = 1

  • 0 ^ 1 = 1

  • 1 ^ 1 = 0

Ví dụ, thực hiện phép toán XOR trên số nhị phân , kết quả là .

2.4 Phép toán NOT

Phép toán NOT: Toán tử NOT là ~. Chức năng của nó là đảo ngược từng bit của một số nhị phân.

  • Quy tắc phép toán NOT: Đảo ngược từng bit của số nhị phân, chuyển 1 thành 0 và 0 thành 1.
    • ~0 = 1
    • ~1 = 0

Ví dụ, thực hiện phép toán NOT trên số nhị phân , kết quả như hình dưới đây:

2.5 Phép toán dịch trái và dịch phải

Phép toán dịch trái (SHL): Toán tử dịch trái là <<. Chức năng của nó là dịch trái toàn bộ các bit của một số nhị phân (bit cao bị bỏ đi, bit thấp được thêm vào là 0).

Ví dụ, thực hiện phép toán dịch trái bit trên số nhị phân , kết quả là .

Phép toán dịch phải (SHR): Toán tử dịch phải là >>. Chức năng của nó là dịch phải toàn bộ các bit của một số nhị phân (bit thấp bị bỏ đi, bit cao được thêm vào là 0).

Ví dụ, thực hiện phép toán dịch phải bit trên số nhị phân , kết quả là .

3. Ứng dụng của phép toán bit

3.1 Các phép toán bit thông dụng

3.1.1 Kiểm tra số chẵn lẻ

Một số nguyên là chẵn nếu và chỉ nếu bit cuối cùng của số đó là . Tương tự, một số nguyên là lẻ nếu và chỉ nếu bit cuối cùng của số đó là . Do đó, chúng ta có thể sử dụng phép toán AND với để kiểm tra xem một số có phải là chẵn hay lẻ.

  1. (x & 1) == 0 cho số chẵn.
  2. (x & 1) == 1 cho số lẻ.

3.1.2 Lấy các bit cụ thể từ số nhị phân

Nếu chúng ta muốn lấy ra một số bit cụ thể từ một số nhị phân , sao cho các bit lấy ra giữ nguyên giá trị ban đầu và các bit còn lại bằng , chúng ta có thể sử dụng một số nhị phân khác , trong đó các bit tương ứng với vị trí lấy ra bằng và các bit còn lại bằng . Sau đó, chúng ta thực hiện phép toán “AND bit” giữa hai số này (X & Y), từ đó ta có được số mong muốn.

Ví dụ, nếu chúng ta muốn lấy ra bit cuối cùng của số nhị phân , chúng ta chỉ cần thực hiện phép toán “AND bit” giữa (4 bit cuối cùng bằng , các bit khác bằng ), tức là 01101010 & 00001111 == 00001010. Kết quả 00001010 chính là số mong muốn (tức là bit cuối cùng của số nhị phân ).

3.1.3 Đặt các bit cụ thể thành 1

Nếu chúng ta muốn đặt một số bit cụ thể trong một số nhị phân thành , giữ nguyên giá trị ban đầu của các bit khác, chúng ta có thể sử dụng một số nhị phân khác , trong đó các bit tương ứng với vị trí được chọn bằng và các bit còn lại bằng . Sau đó, chúng ta thực hiện phép toán “hoặc bit” giữa hai số này (X | Y), từ đó ta có được số mong muốn.

Ví dụ, nếu chúng ta muốn đặt bit cuối cùng của số nhị phân thành , giữ nguyên giá trị ban đầu của các bit khác, chúng ta chỉ cần thực hiện phép toán “OR bit” giữa (4 bit cuối cùng bằng , các bit khác bằng ), tức là 01101010 | 00001111 = 01101111. Kết quả 01101111 chính là số mong muốn (tức là bit cuối cùng của số nhị phân được đặt thành , giữ nguyên giá trị ban đầu của các bit khác).

3.1.4 Đảo ngược các bit cụ thể

Nếu chúng ta muốn đảo ngược một số bit cụ thể trong một số nhị phân , chúng ta có thể sử dụng một số nhị phân khác , trong đó các bit tương ứng với vị trí được chọn bằng và các bit còn lại bằng . Sau đó, chúng ta thực hiện phép toán “XOR bit” giữa hai số này (X ^ Y), từ đó ta có được số mong muốn.

Ví dụ, nếu chúng ta muốn đảo ngược bit cuối cùng của số nhị phân , chúng ta chỉ cần thực hiện phép toán “xor bit” giữa (4 bit cuối cùng bằng , các bit khác bằng ), tức là 01101010 ^ 00001111 = 01100101. Kết quả 01100101 chính là số mong muốn (tức là bit cuối cùng của số nhị phân được đảo ngược).

3.1.5 Hoán đổi hai số

Chúng ta có thể sử dụng phép toán XOR để hoán đổi hai số (chỉ áp dụng cho việc hoán đổi hai số nguyên).

a, b = 10, 20
a ^= b
b ^= a
a ^= b
print(a, b)

3.1.6 Đặt bit cuối cùng có giá trị 1 trong số nhị phân thành 0

Nếu chúng ta muốn đặt bit cuối cùng có giá trị 1 của một số nhị phân thành , chúng ta chỉ cần thực hiện phép toán X & (X - 1).

Ví dụ, cho , , vì vậy X & (X - 1) = 01101100 & 01101011 = 01101000. Kết quả là (tức là bit phải nhất của được chuyển thành ).

3.1.7 Đếm số bit 1 trong số nhị phân

Từ 3.1.6, chúng ta biết rằng bằng cách sử dụng phép toán X & (X - 1), chúng ta có thể chuyển bit phải nhất của số nhị phân thành . Nếu chúng ta tiếp tục thực hiện phép toán X & (X - 1) cho đến khi trở thành , và đếm số lần thực hiện phép toán, chúng ta có thể tính được số bit bằng trong số nhị phân.

Code cụ thể như sau:

class Solution:
    def hammingWeight(self, n: int) -> int:
        cnt = 0
        while n:
            n = n & (n - 1)
            cnt += 1
        return cnt

3.1.8 Kiểm tra xem một số có phải là lũy thừa của 2 không

Bằng cách kiểm tra điều kiện X & (X - 1) == 0, chúng ta có thể xác định xem số có phải là một lũy thừa của hay không.

Điều này là vì:

  1. Mọi lũy thừa của có một bit cao nhất bằng và tất cả các bit khác đều bằng trong biểu diễn nhị phân. Ví dụ: , .
  2. Một số không phải là lũy thừa của nếu nó có nhiều hơn một bit bằng trong biểu diễn nhị phân. Ví dụ: , .

Tiếp theo, chúng ta sử dụng phép toán X & (X - 1) để chuyển bit phải nhất của số ban đầu thành , và thu được giá trị mới:

  1. Nếu số ban đầu là lũy thừa của , sau khi thực hiện phép toán X & (X - 1), giá trị mới sẽ có tất cả các bit bằng , và giá trị mới sẽ là .
  2. Nếu số đó không phải là lũy thừa của , sau khi thực hiện phép toán X & (X - 1), giá trị mới vẫn có các bit khác , và giá trị mới sẽ khác .

Vì vậy, chúng ta có thể xác định xem số đó có phải là lũy thừa của hay không bằng cách kiểm tra xem giá trị mới có bằng hay không.

3.2 Tổng kết các phép toán thường dùng trong bit

Chức năngPhép toán bitVí dụ
Thay đổi bit cuối cùng 1 thành 0x & (x - 1)100101000 -> 100100000
Bỏ đi các bit bên trái của bit 1 đầu tiên từ phảix & (x ^ (x - 1)) hoặc x & (-x)100101000 -> 1000
Bỏ đi bit cuối cùngx >> 1101101 -> 10110
Lấy bit thứ k từ phảix >> (k - 1) & 11101101 -> 1, k = 4
Lấy 3 bit cuối cùngx & 71101101 -> 101
Lấy k bit cuối cùngx & 151101101 -> 1101, k = 4
Chỉ giữ lại các bit 1 liên tiếp từ phải(x ^ (x + 1)) >> 1100101111 -> 1111
Đảo bit thứ k từ phảix ^ (1 << (k - 1))101001 -> 101101, k = 3
Thêm một bit 0 vào cuốix << 1101101 -> 1011010
Thêm một bit 1 vào cuối(x << 1) + 1101101 -> 1011011
Đặt bit thứ k từ phải thành 0x & ~(1 << (k - 1))101101 -> 101001, k = 3
Đặt bit thứ k từ phải thành 1x | (1 << (k - 1))101001 -> 101101, k = 3
Đặt bit 0 đầu tiên từ phải thành 1x | (x + 1)100101111 -> 100111111
Đặt các bit 0 liên tiếp từ phải thành 1x | (x - 1)11011000 -> 11011111
Đặt các bit 1 liên tiếp từ phải thành 0x & (x + 1)100101111 -> 100100000
Đặt bit cuối cùng thành 0x | 1 - 1101101 -> 101100
Đặt bit cuối cùng thành 1x | 1101100 -> 101101
Đặt k bit cuối cùng thành 1x | (1 << k - 1)101001 -> 101111, k = 4
Đảo bit cuối cùngx ^ 1101101 -> 101100
Đảo k bit cuối cùngx ^ (1 << k - 1)101001 -> 100110, k = 4

3.3 Liệt kê tập con bằng cách đếm nhị phân

Ngoài các phép toán thông thường đã đề cập ở trên, chúng ta thường sử dụng trạng thái hoặc của các bit từ đến trong một số nhị phân để biểu diễn một tập hợp gồm các phần tử từ đến . Nghĩa là chúng ta sử dụng phép liệt kê nhị phân để liệt kê các tập con.

3.3.1 Giới thiệu về phép liệt kê nhị phân

Hãy để tôi giới thiệu về khái niệm “tập con”.

  • Tập con: Nếu mọi phần tử của tập đều là phần tử của tập , thì ta gọi tập là tập con của tập . Ta có thể ký hiệu là .

Đôi khi chúng ta gặp phải vấn đề như sau: cho trước một tập , hãy liệt kê tất cả các tập con có thể có của nó.

Có nhiều phương pháp để liệt kê các tập con, ở đây tôi giới thiệu một phương pháp đơn giản và hiệu quả: “Thuật toán liệt kê tập con theo hệ nhị phân”.

Đối với một tập phần tử, mỗi vị trí của mỗi phần tử có hai trạng thái: được chọn và không được chọn. Chúng ta có thể sử dụng số nhị phân có độ dài để biểu diễn tập hoặc tập con của . Mỗi chữ số nhị phân tương ứng với trạng thái chọn hoặc không chọn của một phần tử trong tập. Với phần tử thứ trong tập, chữ số nhị phân tại vị trí tương ứng là đại diện cho việc chọn phần tử đó, và đại diện cho việc không chọn phần tử đó.

Ví dụ, với tập có độ dài , chúng ta có thể sử dụng một số nhị phân có độ dài để biểu diễn tập đó.

Ví dụ, số nhị phân biểu diễn việc chọn phần tử thứ , , , , trong tập, tức là tập , tức là tập chính nó. Bảng dưới đây minh họa:

Vị trí phần tử trong tập S54321
Giá trị nhị phân tương ứng11111
Trạng thái tương ứngChọnChọnChọnChọnChọn

Ví dụ khác, số nhị phân biểu diễn việc chọn phần tử thứ , , trong tập, tức là tập . Bảng dưới đây minh họa:

Vị trí phần tử trong tập S54321
Giá trị nhị phân tương ứng10101
Trạng thái tương ứngChọnKhông chọnChọnKhông chọnChọn

Ví dụ khác, số nhị phân biểu diễn việc chọn phần tử thứ , trong tập, tức là tập . Bảng dưới đây minh họa:

Vị trí phần tử trong tập S54321
Giá trị nhị phân tương ứng01001
Trạng thái tương ứngKhông chọnChọnKhông chọnKhông chọnChọn

Từ những ví dụ trên, ta có thể suy ra: Đối với tập có độ dài , chúng ta chỉ cần liệt kê từ đến (tương ứng với từ đến ở hệ thập phân) một lần duy nhất để có được tất cả các tập con của tập .

3.3.2 Code liệt kê tập con bằng cách đếm nhị phân

class Solution:
    def subsets(self, S):                   # Trả về tất cả các tập con của tập hợp S
        n = len(S)                          # n là số phần tử của tập hợp S
        sub_sets = []                       # sub_sets dùng để lưu trữ tất cả các tập con
        for i in range(1 << n):             # Liệt kê từ 0 đến 2^n - 1
            sub_set = []                    # sub_set dùng để lưu trữ tập con hiện tại
            for j in range(n):              # Liệt kê phần tử thứ i
                if i >> j & 1:              # Nếu bit thứ i được đặt thành 1, tức là chọn phần tử đó
                    sub_set.append(S[j])    # Thêm phần tử đã chọn vào tập con sub_set
            sub_sets.append(sub_set)        # Thêm tập con sub_set vào mảng tất cả các tập con sub_sets
        return sub_sets                     # Trả về tất cả các tập con