# 演算法課程題解 - 位元運算
# TOJ 244
## 題目
https://toj.tfcis.org/oj/pro/244/
輸入第一行有一個正整數 $N$ ,接下來有長度 $N$ 的字串,請將所有小寫轉成大寫,大寫轉成小寫
## 想法 By Koios
過去曾經在條件判斷的課程中寫過這題,不過現在嘗試用位元運算的角度來解
觀察一下 ASCII Table,會發現到 `a` 以及 `A` 之間剛好差了 $32$
並且將其對應到的數字轉換成二進位後會發現到它們只在**第五個 bit**是不同的
也就是說我們要做大小寫轉換,只需要反轉第五個 bit 即可
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int t, x = (1<<5);
string s;
int main(){
cin>>t;
cin>>s;
for(int i=0 ; i<t ; i++){
cout<<(char)((int)s[i]^x);
}
cout<<"\n";
return 0;
}
```
# UVa 10469
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10469
有個人在學如何用位元運算進行 32-bit 正整數的加法,但是他搞砸了
現在他設計出來的程式不能進位,請你嘗試設計出跟他一樣的加法器
## 想法 By Koios
我們可以考慮兩種情況
1. 原本需要進位
2. 原本不需要進位
如果說原本就需要進位,那就表示在二進位底下兩個 bit 都是 $1$,既然不進位,那這個位置最後會留下 $0$,而下一位不會 $+1$
反之,如果原本就不需要進位的話,那就直接相加即可
也就是說,考慮兩個 bit 的情況,整理如下表
| | 1 | 0 |
| --- | --- | --- |
| 1 | 0 | 1 |
| 0 | 1 | 0 |
發現到這跟 **xor** 就是一樣的道理
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, m;
int main(){
while(cin>>n>>m){
cout<<(n^m)<<"\n";
}
return 0;
}
```
# UVa 11195
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?11195
經典 N 皇后問題
給定一個 $N \times N$ 的西洋棋棋盤,棋盤上可能有障礙物,障礙物可以抵擋攻擊
現在要在這個棋盤上放上 $N$ 個皇后,並且彼此都不會攻擊到對方,請問有多少種解法
## 想法 By Koios
想法跟過去 DFS 解 8 皇后問題是一樣的
對於每個可以放皇后的格子嘗試去放,然後看看能不能放滿整個盤面
不同的是,我們不需要每次都往三個方向(左斜/右斜/直)掃過去檢查,而是改用位元運算的想法
:::info
不考慮橫向是因為在 DFS 的過程當中我們就只會取每一列中其中一個下去枚舉,所以橫向必定不會互相攻擊
:::
以左斜向來看,我們可以用一個數字 $m$ 相對應的 bit 去記錄該點是否會被攻擊到
如果可以被攻擊的話,那麼枚舉下一列的時候會被攻擊到的點就會是 $m$ 全部 bit 左移後的位置
:::info
舉例來說,如果當前是 $4 \times 4$ 的棋盤,並且在第一列可以被左斜向攻擊到的有第 $3$ 行
當前 $m$ 以二進位表示為 $0010$
在枚舉到第二列的時候可以攻擊的點會左移,也就是變成 $0100$
:::
類似地,右斜向則是每次都會向右移一個 bit
至於直向也一樣用一個數字相對應的 bit 紀錄可否被攻擊,只是枚舉下一列時並不需要改變
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int n, ans, Case=1;
char arr[20][20];
// depth 表示當前枚舉的列數
void DFS(int depth, int Lslash, int Rslash, int straight){
if(depth == n){
ans++;
return;
}
// 對於每一行去判斷
// 1. 該點是否沒障礙物
// 2. 左斜向攻擊是否會攻擊到
// 3. 右斜向攻擊是否會攻擊到
// 4. 直向攻擊是否會攻擊到
for(int i=0 ; i<n ; i++){
if(arr[depth][i] == '.' && ((Lslash & (1<<i)) == 0) && ((Rslash & (1<<i)) == 0) && ((straight & (1<<i)) == 0)){
// 記得先把自己加進去,然後再左移右移
DFS(depth+1, (Lslash | (1<<i))<<1, (Rslash | (1<<i))>>1, (straight | (1<<i)));
}
}
}
int main(){
while(cin>>n && n){
for(int i=0 ; i<n ; i++){
for(int j=0 ; j<n ; j++){
cin>>arr[i][j];
}
}
ans=0;
DFS(0, 0, 0, 0);
cout<<"Case "<<Case++<<": "<<ans<<"\n";
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(n^2)$
每筆測資 DFS 時間複雜度為 $O(n^n)$
每筆測資總時間複雜度為 $O(n^n)$
# TOJ 4
## 題目
https://toj.tfcis.org/oj/pro/4/
有 $N$ 個插頭以及電器,分別用長度 $L$ 的 $0/1$ 字串來表示,唯有電器以及插頭完全相同才能配對
現在你只有一種操作方式: 把插頭的某個位元反轉,請問最少需要多少操作才可以讓所有電器以及插頭配對
如果不可能則輸出 `IMPOSSIBLE`
## 想法 By Koios
如果說全部的電器都會對應到一個特定的插頭,那麼第一個電器一定會有一個對應的插頭
枚舉所有插座對應到第一個電器的情況,檢查是否所有的電器跟插座是否匹配
至於如何反轉可以這樣想
要讓插座 $m$ 配對上電器 $n$,那麼需要反轉的位元就是兩兩不相同的位元,可以用 xor 檢查出來,得到 $p = m \oplus n$
接下來要去反轉每個插座 $m_i$,則 $m_i^{'} = m_i \oplus p$
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<algorithm>
using namespace std;
int t, n, l;
long long change, ans, res, apps[1005], plugs[1005], tmp[1005];
string s;
int main(){
cin>>t;
for(int Case=1 ; Case<=t ; Case++){
cin>>n>>l;
for(int i=0 ; i<n ; i++){
cin>>s;
// 將 0/1 字串轉成數字
apps[i] = 0;
for(int j=0 ; j<l ; j++){
apps[i] |= ((long long)(s[j]-'0')<<j);
}
}
// 先排序好,之後比較好判斷是否匹配
sort(apps, apps+n);
for(int i=0 ; i<n ; i++){
cin>>s;
// 將 0/1 字串轉成數字
plugs[i] = 0;
for(int j=0 ; j<l ; j++){
plugs[i] |= ((long long)(s[j]-'0')<<j);
}
}
ans = -1;
for(int i=0 ; i<n ; i++){
// 只看 apps[0] 以及 plugs[i]
// 找出需要轉換的 bit
change = apps[0] ^ plugs[i];
// 將每個插座轉換後的結果儲存在新的陣列
for(int j=0 ; j<n ; j++){
tmp[j] = plugs[j] ^ change;
}
// 排序後檢查
sort(tmp, tmp+n);
bool found = true;
for(int j=0 ; j<n ; j++){
if(apps[j] != tmp[j]){
found = false;
break;
}
}
// 匹配成功
if(found){
res = 0;
while(change){
// 操作數等同 change 中 bit 為 1 的數量
// 以 lowbit 來找
res++;
change ^= (change & -change); // lowbit
}
if(ans == -1) ans = res;
else ans = min(ans, res);
}
}
if(ans == -1){
cout<<"Case #"<<Case<<": IMPOSSIBLE\n";
}
else{
cout<<"Case #"<<Case<<": "<<ans<<"\n";
}
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(NL)$
預先排序 app 的時間複雜度為 $O(NlogN)$
匹配過程重複 $N$ 次,每次切成新插座所需時間複雜度為 $O(N)$,排序時間複雜度為 $O(NlogN)$,檢查時間複雜度為 $O(N)$,計算操作數量時間複雜度為 $O(logL)$
匹配總時間複雜度為 $O(N^2 + N^2logN + NlogL)$
總時間複雜度為 $O(t(NL + N^2 + N^2logN + NlogL))$
---
:::info
TOJ 449 可以參考過去 BFS 這篇(https://hackmd.io/@SCIST/BFS#TOJ-449),寫法就是位元運算
:::
---
# UVa 124
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?124
給你幾個不重複的小寫英文字母,告訴你某些字母之間的大小關係,把所有從小到大的排列列出來
如果沒有告知某個元素的大小關係,那可以任意擺放
## 想法 By Koios
之前有用 DFS 以及 拓樸排序 的方式寫過這題,現在用位元運算的角度來看
我們可以用數字的 bit 去紀錄字元 $a$ 是否小於字元 $b$
例如說 $b < a$ ,那麼就在記錄 $b$ 小於的數字 $m$ 中第 $0$ (`a`-`a` = 0) 個 bit 紀錄 $1$
接下來當我們透過 DFS 枚舉的過程時,把當前有用到的字元都記錄在一個數字的相對應 bit 上
要判斷某個字元能否放上去,就只需要判斷出現過的字元是否都符合小於的條件即可
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<sstream>
using namespace std;
string s;
int vars, var_num, Less[30];
char x, y, res[30];
bool out=false;
void DFS(int depth, int used){
if(depth == var_num){
for(int i=0 ; i<var_num ; i++){
cout<<res[i];
}
cout<<"\n";
return;
}
// 枚舉 26 個字母
for(int i=0, now ; i<26 ; i++){
now = (1<<i);
// 判斷
// 1. 是否為變數表中的變數
// 2. 過去是否用過
// 3. 用過的變數是否都小於當前變數
if((vars & now) != 0 && (used & now) == 0 && (Less[i] & used) == 0){
res[depth] = 'a'+i;
// 記得要把自己加進 used
DFS(depth+1, used|now);
}
}
}
int main(){
while(getline(cin, s)){
if(out) cout<<"\n";
else out=true;
vars = var_num = 0;
// 先將每個人的 less 初始化為 0
for(int i=0 ; i<30 ; i++){
Less[i] = 0;
}
stringstream ss;
ss<<s;
while(ss>>x){
// 紀錄該字元有出現
vars |= (1<<(x-'a'));
var_num++;
}
getline(cin, s);
ss.str("");
ss.clear();
ss<<s;
while(ss>>x>>y){
// 紀錄 y < x
Less[x-'a'] |= (1<<(y-'a'));
}
DFS(0, 0);
}
return 0;
}
```
---
:::info
UVa 321 可以參考過去 BFS 這篇 (https://hackmd.io/@SCIST/BFS#UVa-321),寫法就是位元運算
:::
---
# UVa 10393
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?10393
給定每個手指在鍵盤上可以輸入的字元
現在有一個人有幾個手指頭受傷沒辦法打特定的字,給一群字串,問所有字串中他可以打出來,並且長度最長的字串有多少個?分別是哪些?
輸出請以字典序大小由小到大輸出,並且每個字串都只能至多出現一次
## 想法 By Koios
我們可以用 bit 去分別記錄特定手指可以寫的字
輸入會告訴我們哪些手指不能用,那我們可以先把不能用的手指可以打得字以位元記錄下來,再反轉後就變成可以打的
實作上可以跟所有可行字元組成的數字 xor 來處理
到目前我們有了當前這個人可以打哪些字元這項資訊,那麼最後只需要判斷每個字是否可以被打出來,然後找出最長的長度即可
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<algorithm>
using namespace std;
string dict = "qazwsxedcrfvtgb yhnujmik,ol.p;/";
string fig[11] = {"", "qaz", "wsx", "edc", "rfvtgb", " ", " ", "yhnujm", "ik,", "ol.", "p;/"};
string s[1005];
int tbl[128], alltype[32];
int all=0, F, N, cantype, ans, len;
bool res[1005];
int str_to_num(string s){
int ret = 0;
for(int i=0 ; i<s.size() ; i++){
ret |= (1<<tbl[s[i]]);
}
return ret;
}
int main(){
// 預處理建表
for(int i=0 ; i<dict.size() ; i++){
tbl[dict[i]] = i;
}
for(int i=1 ; i<=10 ; i++){
// alltype 表示該手指能寫的字元
for(int j=0 ; j<fig[i].size() ; j++){
alltype[i] |= (1<<tbl[fig[i][j]]);
}
// all 表示全部可以寫的字元
all |= alltype[i];
}
while(cin>>F>>N){
cantype = ans = len = 0;
for(int i=0, f ; i<F ; i++){
cin>>f;
// 先找出不能打的字元
cantype |= alltype[f];
}
// 接下來用 xor 轉換成可以打的字元
cantype ^= all;
for(int i=0 ; i<N ; i++){
cin>>s[i];
}
// 輸出需要依照字母序排序
sort(s, s+N);
// 以 res 紀錄該字串是否可以被打出來
for(int i=0, num, sz ; i<N ; i++){
// 忽略已經出現過的字串
if(i>0 && s[i] == s[i-1]){
res[i] = false;
continue;
}
num = str_to_num(s[i]);
sz = s[i].length();
// 可以被打出來的情況
if((num & cantype) == num){
res[i] = true;
if(sz > len){
len = sz;
ans = 1;
}
else if(sz == len){
ans++;
}
}
else{
res[i] = false;
}
}
cout<<ans<<"\n";
// 最後找出可以打的字串中長度是最長的字串
for(int i=0 ; i<N ; i++){
if(res[i] && s[i].length() == len){
cout<<s[i]<<"\n";
}
}
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(N)$
每筆測資排序時間複雜度為 $O(NlogN)$
每筆測資找可以被打出來的字串所需時間複雜度為 $O(N)$
每筆測資找答案時間複雜度為 $O(N)$
每筆測資總時間複雜度為 $O(N + NlogN)$
# UVa 12571
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?12571
給定 $N$ 個數字 $x_1, x_2, \dots, x_N$,以及 $Q$ 筆詢問
每筆詢問會有一個數字 $a$,求 $a \& x_i$ 的最大值為多少
$$1 \leq N \leq 100000 \\
1 \leq Q \leq 30000 \\
0 \leq x_i \leq 10^9 \\
0 \leq a < 230
$$
## 想法1 By Koios
雖然說暴力地把每個數字都做 & 一次取最大值會太慢(沒錯,我在這題 TLE 了好幾次)
但是注意到 $a$ 的範圍都在 $230$ 以內,也就是說超過 $2^7$ 這個 bit 的部分都可以直接忽略掉
所以我們就只需要注意所有 $255 = 11111111_2$ 以內的部分即可
找出一開始 $N$ 個數字中所有在 $0$~$255$ 以內的部分記錄起來
接下來如果要詢問,我們只需要針對這 $256$ 個數字檢查是否有出現過,然後找出 & 後的最大值即可
如此一來我們就不需要掃過長度 $N$ 的序列,而只需要掃過長度 $256$ 的序列
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int T, N, Q;
bool used[256];
int main(){
cin>>T;
while(T--){
for(int i=0 ; i<256 ; i++){
used[i] = false;
}
cin>>N>>Q;
for(int i=0, n ; i<N ; i++){
cin>>n;
// 只記錄 255 以內的部分
used[(n&255)] = true;
}
for(int i=0, q, ans=0 ; i<Q ; i++, ans=0){
cin>>q;
for(int j=0 ; j<256 ; j++){
if(used[j]){
// 出現過就找最大值
ans = max(ans, (q & j));
}
}
cout<<ans<<"\n";
}
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入時間複雜度為 $O(N + Q)$
每筆測資找出 `used` 的時間複雜度為 $O(N)$
令 $M = 256$,每筆詢問時間複雜度為 $O(R)$
總時間複雜度為 $O(t(N + Q + QR))$
## 想法2 By Koios
如果我們直接把整個序列掃過去時間太慢,可能的因素是重複檢查相同的數字
也就是說,有些數字有出現的 bit 可能被其他數字完全覆蓋,那麼我們就只需要找那些盡量多 bit 的做一次 & 運算,其效果等同跟這群數字都做一次 &
舉範例的例子來說,輸入的 $N$ 個數字為 $1, 2, 3$,轉換成二進位後
$$
1 = 01_2 \\
2 = 10_2 \\
3 = 11_2
$$
很顯然的,與其跟三個數字都做一次 &,倒不如只跟 $3$ 做一次 &,因為 $3$ 包含了 $1, 2$ 的所有 bit
那麼我們只要想辦法留下這種會覆蓋掉別人的數字即可
那麼要怎麼篩選呢?
我們可以枚舉每個尚未被篩除的數字跟其他數字做 `|` 運算,如果 $a | b = a$,那就表示 $b$ 的所有 bit 都被包含在 $a$ 裡面
這裡有個優化的想法
那些會被篩掉的數字必定都是被比她還大的數字篩掉的,畢竟大的數字在更高位會出現 $1$
那麼,一開始就把數字排序好,必定可以保證由大到小篩選不會漏篩,而且每次都可以從最後一個篩除的數字往下繼續篩
如此一來就不需要每次都跟全部數字篩看看了
最後只需要把篩出來的數字跟詢問做 & 找最大值即可
> 實作部分用了後面教的內容,可以先忽略,或是嘗試不使用 vector
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int t, n, m, arr[100010];
vector<int> res;
int main(){
cin>>t;
while(t--){
res.clear();
cin>>n>>m;
for(int i=0 ; i<n ; i++){
cin>>arr[i];
}
// 排序後來篩,可以明確知道下次要從哪個數字繼續篩,稍微加速所需時間
// i 表示不篩除的數字編號; j 表示當前要篩選的數字編號
sort(arr, arr+n);
for(int i=n-1, j ; i>0 ; i=j){
res.push_back(arr[i]);
for(j=i-1 ; (arr[i] | arr[j]) == arr[i] ; j--){}
}
for(int i=0, q, ans ; i<m ; i++){
cin>>q;
ans=0;
for(auto j: res){
ans = max(ans, (q & j));
}
cout<<ans<<"\n";
}
}
return 0;
}
```
:::info
複雜度我不太知道要怎麼估,但是至少在 UVa 上是比第一個想法快了 0.02 秒
:::
# UVa 12444
## 題目
http://domen111.github.io/UVa-Easy-Viewer/?12444
給你兩個正整數 $C, D$,其中 $C = A\&B \quad D = A|B$
求兩個正整數 $A, B$ 符合以上條件,並且 $B-A$ 為最小, $A \leq B$
如果沒有任何數字符合以上條件則輸出 $-1$
## 想法
要找到兩個數字 $A, B$ 同時符合 $A\&B = C$ 和 $A|B = D$,那麼至少 $C$ 在二進為底下有 $1$ 的 bit 在 $D$ 上面也要有,所以如果不符合就保證無解
接下來要找到使得 $B-A$ 為最小,那麼我們可以這樣想
比較高位的 bit 必定會比所有比他低位的還大,例如說 $100_{(2)}$ 必定大於 $011_{(2)}$
所以我們只要把最高位的給 $B$,剩下的都給 $A$,那麼兩個一定會是最接近的,也符合 $A \leq B$ 的條件
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
#include<cmath>
using namespace std;
int T, C, D, A, B, tmp, highest;
int main(){
cin>>T;
while(T--){
cin>>C>>D;
// 先判斷不符合條件的
if((C&D) != C){
cout<<"-1\n";
}
else{
A = B = C;
// 透過 xor 可以找到哪些位置需要看
tmp = C^D;
// 最高位的要給 B,可以用 log 取整得到
highest = log2(C^D);
for(int i=0 ; i<=highest ; i++){
if((tmp & (1<<i)) != 0){
if(i == highest){
B |= (1<<i);
}
else{
A |= (1<<i);
}
}
}
cout<<A<<" "<<B<<"\n";
}
}
return 0;
}
```
### 時間複雜度分析
每筆測資輸入以及輸出時間複雜度均為 $O(1)$
每筆測資找答案的時間複雜度為 $O(log_{2}{(C \oplus D)})$
總時間複雜度為 $O(T(log_{2}{(C \oplus D)}))$
# Kattis bard
## 題目
https://open.kattis.com/problems/bard
在某個小村莊當中有 $N$ 個村民,並且有一位村民是吟遊詩人
在這個村莊,每個晚上都會聚集一些村民來唱歌
如果晚上吟遊詩人有出現,那麼他會帶來一首新歌,並且當天所有參加的村民都只會唱那首歌,同時彼此都學會了那首新歌
如果當天晚上吟遊詩人沒有出現,那麼所有參加的村民們會將他們所會的歌都唱一遍,同時彼此都學會那些歌
給定村民的數量 $N$,編號 $1$ 的村民為吟遊詩人
給你每天晚上參加晚會的村民編號,問最後一天有哪些村民(包含吟遊詩人)會唱全部的歌
## 想法 By Koios
因為最多只會有 $50$ 場的晚會,$2^{50}$ 還在 long long 的範圍內,所以可以用位元運算來想
每個村民都有一個數字,數字的 bit 記錄該村民是否會唱某首歌
那麼當有吟遊詩人出現時,我們就將所有參加的村民在當天的 bit 上記錄 $1$
如果當天吟遊詩人沒有出現,那麼由於每個參加的村民之間會共享所有他們會的歌曲,所以我們可以透過 or 運算先求出所有人會的歌曲的聯集,最後每個村民會的歌都變成那個聯集結果
如此一來就可以模擬出每晚晚會後的結果
在最後可以保證吟遊詩人會所有的歌(畢竟歌是他提供的),所以接下來只需要判斷所有村民會的歌曲是否跟吟遊詩人相同即可
:::warning
在實作時要特別小心位元運算的左右移,因為這裡用到的是 long long ,而位元運算預設都是採用 int,所以我們要將數字強制轉型為 long long
例如我們要得到 $2^{10}$ 以 long long 為其形態的結果,應該這麼做
```cpp=
1LL<<10
```
也就是說,我們希望得到怎樣的型態,則位元運算子左側之運算元須為該型態
:::
### 程式碼
```cpp=
//By Koios1143
#include<iostream>
using namespace std;
int N, E, K, arr[105];
long long songs[105];
int main(){
cin>>N>>E;
for(int i=0 ; i<E ; i++){
cin>>K;
// 檢查是否有吟遊詩人出現
bool bard=false;
for(int j=0 ; j<K ; j++){
cin>>arr[j];
if(arr[j] == 1) bard=true;
}
if(bard){
// 每個人當天的歌都會學會
for(int j=0 ; j<K ; j++){
songs[arr[j]] |= (1LL<<i);
}
}
else{
// 每個人當天都會學會所有參加者會的歌的聯集
// 先找出聯集
long long all_song = 0;
for(int j=0 ; j<K ; j++){
all_song |= songs[arr[j]];
}
// 全部賦予給參加的村民
for(int j=0 ; j<K ; j++){
songs[arr[j]] = all_song;
}
}
}
for(int i=1 ; i<=N ; i++){
if(songs[i] == songs[1]){
cout<<i<<"\n";
}
}
return 0;
}
```
### 時間複雜度分析
輸入時間複雜度為 $O(EN)$
每晚更新每個參加村民所會的歌曲,其時間複雜度為 $O(K)$
找出答案時間複雜度為 $O(N)$
總時間複雜度為 $O(E(N + K)+ N)$
###### tags: `SCIST 演算法 題解`