# Pusheen Contest 2023 #6 ###### tags: `CLA K22` ## Matching ### bitmask Cách biểu diễn một chuỗi nhị phân thành một số nguyên thập phân. VD: - $101_2 \to 2^2 + 2^0 = 5_{10}$ - $1100_2 \to 2^3+2^2 = 12_{10}$ #### Phép xử lí bit 1. Phép and: `a&b` $$ \begin{matrix} 5= & 0101\\ 12= & 1100\\ & ---\\ 4= & 0100 \end{matrix} $$ `5&12 = 4` 2. Phép or: `a|b` $$ \begin{matrix} 5= & 0101\\ 12= & 1100\\ & ---\\ 13= & 1101 \end{matrix} $$ `5|12 = 13` 3. Phép xor: `a^b` Khác nhau thì ra $1$, giống nhau thì ra $0$. Cộng không nhớ $$ \begin{matrix} 5= & 0101\\ 12= & 1100\\ & ---\\ 9= & 1001 \end{matrix} $$ `=> 5^12 = 9` 4. Phép cộng trừ: Tương tự cộng trừ trong hệ nhị phân Xét $5+12$ $$ \begin{matrix} 5= & 0101\\ 12= & 1100\\ & ---\\ 17= & 10001 \end{matrix} $$ 5. Phép dịch bit Dịch phải: `S>>i`, loại bỏ $i$ bit thấp nhất. Tương tự với `S / pow(2, i)`. VD: $S = 1101_2$, $S>>2 = 11_2$ Dịch trái `S<<i`, thêm vào $i$ bit 0 vào đầu dãy. Tương tự với `S * pow(2, i)`. VD: $S = 1101_2$, $S<<2 = 110100_2$ #### Biểu diễn tập hợp Với một tập hợp $n$ phần tử $a = \{a_1, a_2, \dots, a_n\}$, khi muốn biểu diễn một tập con của a, ta có thể dùng một dãy nhị phân. Với mỗi vị trí $i$ trên dãy, nếu bit đó là $0$ thì phần tử đó không thuộc tập con, ngược lại nếu bit là $1$ thì phần tử đó thuộc tập con. VD: $A = \{1, 2, 3, 4, 5\}$. Khi muốn biểu diễn tập $\{3, 5\}$, ta có thể biểu diễn bằng một dãy nhị phân $5$ phần tử: $00101$. Khi chuyển dãy này sang hệ thập phân, ta được $00101_2 = 2^2 + 2^0 = 5_{10}$. Số lượng tập con của một tập hợp $n$ phần tử là $2^n$ tập hợp và có thể được biểu diễn bằng các bitmask từ $0$ đến $2^n-1$. Với $0$ biểu diễn tập rỗng, và $2^n-1 = 1111\dots 1$ biểu diễn tập hợp ban đầu. #### Một số thao tác thường dùng. 1. Kiểm tra tính chẵn lẻ Số chẵn khi bit đầu tiên có giá trị là $0$. Sô lẻ khi bit đầu tiên có giá trị là $1$. Cú pháp: `S&1` $$ 101\&1 = 1 $$ 2. Kiểm tra bit thứ $i$ của một bitmask $S$ ```c++ #define BIT(S, i) (((S)>>(i))&1) ``` Nếu bit thứ $i$ của $S$ là 1 thì kết quả trả về 1, ngược lại trả về $0$ 3. Lấy giá trị $2^i$ trong $O(1)$/ Tạo một dãy nhị phân có duy nhất bit ở vị trí $i$ ``` #define MASK(i) ((1ll)<<(i)) ``` 4. Đếm số bit 1 trong mask `__builtin_popcount(S)` hoặc `__builtin_popcountll(S)` ### Lời giải Gọi $dp[i][S]$ là số cách ghép được khi xét đến bạn nam thứ $i$, và $S$ là tập các bạn nữ chưa được ghép. Khi xét đến bạn nam thứ $i$, ta sẽ thử ghép bạn này với các bạn nữ chưa được ghép. ```c++ #include <bits/stdc++.h> using namespace std; #define BIT(S, i) (((S)>>(i))&1) #define MASK(i) ((1ll)<<(i)) const int N = 23; int n; int dp[N][MASK(21)]; const int MOD = 1e9+7; int a[N]; void add(int &a, int b) { a += b; if (a >= MOD) a -= MOD; } int main() { cin >> n; for(int i = 1; i <= n; ++i) for(int j = 1; j <= n; ++j) { int x; cin >> x; if (x) a[i] |= MASK(j-1); } memset(dp, 0, sizeof dp); dp[1][MASK(n)-1] = 1; for(int i = 1; i <= n; ++i) for(int S = 0; S < MASK(n); ++S) if (dp[i][S]) { add(dp[i+1][S], dp[i][S]); // cout << i << ' ' << S << ' ' << dp[i][S] << '\n'; for(int tmp = S&a[i], j; tmp > 0; tmp ^= MASK(j)) { j = __builtin_ctz(tmp); add(dp[i+1][S^MASK(j)], dp[i][S]); } } cout << dp[n+1][0]; return 0; } ``` #### Tối ưu cài đặt Thay vì duyệt từng bạn nữ $j$ rồi kiểm tra có nằm trong mask không và có hòa hợp với bạn nam $i$ không, ta lưu $a[i]$ dưới dạng một bitmask, và dùng toán tử $\&$ với S trong hàm $dp$, khi đó ta được một bitmask mới gồm các bit 1 có trong cả $a[i]$ và $S$ đang xét, từ đó ta chỉ cần duyệt các bit 1 trong bitmask mới này. #### Duyệt các bit 1 trong một mask (bỏ qua các bit 0) ```c++ for(int tmp = mask, i; tmp > 0; tmp ^= MASK(i)) { i = __builtin_ctz(mask); // your code } ``` Tham khảo: https://vnoi.info/wiki/translate/topcoder/fun-with-bits.md ## Idependent Set Gọi $dp[u][c]$ là số cách tô cây con gốc $u$ hợp lệ sao cho đỉnh $u$ được tô màu $c$. ```c++ const int MOD = 1e9+7; void merge(int cur, int child) { dp[cur][1] = 1ll*dp[cur][1] * dp[child][0] % MOD; dp[cur][0] = 1ll*dp[cur][0] * dp[child][1] % MOD; } void dfs(int u, int p = -1) { dp[u][1] = dp[u][0] = 1; for(int v : adj[u]) if (v != p) { dfs(v, u); merge(u, v); } } int main() { memset(dp, 0, sizeof dp); dfs(1); cout << (dp[1][1] + dp[1][0]) % MOD; } ``` ## Flowers Gọi $dp[i]$ là tổng độ đẹp lớn nhất của các bông hoa được chọn để bông hoa thứ $i$ là bông hoa cuối cùng. Công thức gốc $dp[i] = \max(dp[j] + a[i])$ với $1 \le j < i, h[j] < h[i]$ Cách làm kinh điển: số trạng thái O(n) * chi phí chuyển O(n) => O(n^2) Tối ưu chi phí chuyển: tìm $j$ sao cho $dp[j]$ lớn nhất và $h[j] < h[i], j < i$. Dùng một cây Fenwick/BIT để tối ưu độ phức tạp. Với mỗi $i$, ta gọi truy vấn để tìm max các $dp[j]$ có $h[j] < h[i]$. Sau đó, ta cập nhật $dp[i]$ vào vị trí $h[i]$ trên cây. Độ phức tạp cho mỗi thao tác là $O(\log n)$. Bài viết về cây Fenwick: https://cp-algorithms.com/data_structures/fenwick.html