20191325第七八章學習筆記
bitset
序
從暑假開始就一直聽到 \(bitset\) 優化, 而且好像還挺厲害, 雖然只是常數優化, 但是卻非常的好用.
bitset 是啥
\(bitset\) 其實就是一個二進位制數, 包含在 \(bitset\) 庫裡(萬能頭也有), 宣告如下:
bitset <N> B;
表示聲明瞭一個位長為 \(N\) 的 \(bitset\) , 所佔用的記憶體也僅僅是 \(N\) 位, 也就是說, \(bitset\) 的每一位佔用僅僅是 \(1\) 位, 與同樣是表示 \(01\) 的 \(bool\) 相比, 小了 \(8\) 倍 ( \(bool\) 最小時是 \(1\ bit\)
所以當我們只需要 \(01\) 時, \(bitset\) 是不二之選, 空間複雜度大大減小.
\(bitset\) 不止能優化空間, 還能優化時間, 我們 \(n\) 個 \(bool\) 做位運算, 時間複雜度是 \(O(n)\) , 而一個 \(n\) 位的 \(bitset\) 做位運算, 時間複雜度僅為 \(n / w\) . 這個 \(w\) 是一個常數, 取決於你的計算機位數, 如果你是 \(32\) 位系統, 那就是 \(32\) , \(64\) 位系統就是 \(64\) .
儘管只是常數優化, 但是這個優化已經非常大了, 可以讓我們原本複雜度 \(1e9 \sim 1e10\)
bitset 咋用
\(bitset\) 支援的操作:
B = 10; //賦值 B[0]; // 訪問某一位 /* 相同大小的 bitset 可以進行運算子操作 */ /* == != & &= | |= ^ ^= ~ << <<= >> >>= */ B.count(); // 返回 1 的數量 B.size(); // 返回 bitset 的大小 B.any(); // 若有 1, 返回 1 B.none(); // 若無 1, 返回 1 B.all(); // 若全是 1, 返回 1 B.set(); // 全部設為 1 B.set(pos, k); // 將第 pos 位設為 k B.reset(); // 全部設為 0 B.flip(); // 翻轉每一位 B.flip(0); // 翻轉某一位
\(bitset\) 主要是應用於我們的狀態僅有 \(01\) 兩種時, 就我見過的題來看, 通常是 \(DP\) .
就以今天模擬賽的一道區間 \(DP\) 為例, 詳細的講解一下 \(bitset\) 到底咋用.
題目給出 \(n\) 個人按照 \(1 \sim n\) 的順序站成一排, , 給出 \(n\) 個人之間 \(pk\) 的勝負關係, 求哪些人最終可以贏.
\(\operatorname{subtask}\ 1(10pts): n \leq 10\) .
\(\operatorname{subtask}\ 2(10pts): n \leq 50\) .
\(\operatorname{subtask}\ 3(20pts): n \leq 120\) .
\(\operatorname{subtask}\ 4(20pts): n \leq 300\) .
\(\operatorname{subtask}\ 5(20pts): n \leq 700\) .
\(\operatorname{subtask}\ 6(20pts): n \leq 2000\) .
時間限制: \(2s\)
空間限制: \(512MB\)
我一上來想成了拓撲排序 + 縮點, 後來發現不行, 然後覺得是個 \(DP\) , 於是開始想 \(DP\) .
想了好久, 感覺是個區間 \(DP\) , 但是暫時沒有頭緒, 所以我就乾脆點, 先把狀態設出來, 於是狀態就出來了: \(f(i, j, k)\) 表示 \(k\) 在 \([i, j]\) 中能不能贏.
先把狀態設出來, 管他會不會轉移.
然而狀態出來之後, 轉移也就自然而然的出來了.
設 \(a[i][j]\) 表示 \(i\) 與 \(j\) \(pk\) 的結果.
我們考慮在區間 \([i, j]\) 中, 什麼情況下 \(k\) 才能贏, 於是我們找到了 \(k\) 贏的充要條件: 當且僅當在 \([i, k - 1]\) 中存在能夠被 \(k\) 打敗的人, 並且這個人能夠在 \([i, k - 1]\) 中贏, 也就是 \(a[k][o]\ \And\ f[i][k - 1][o]\) , 右邊也是同理, \(a[k][o]\ \And\ f[k + 1][j][o]\) .
因為需要列舉 \(o\) , 所以複雜度 \(O(n^4)\) . 期望得分 \(60pts\) .
\(code:\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * f;
}
const int N = 705;
int n, a[N][N], f[N][N][N];
char s[N];
int main() {
n = read();
for (int i = 1; i <= n; i++) {
scanf("%s", s + 1);
for (int j = 1; j <= n; j++) {
a[i][j] = s[j] ^ 48;
}
}
for (int i = 1; i <= n; i++) f[i][i][i] = 1;
for (int i = 1; i < n; i++) {
f[i][i + 1][i] = a[i][i + 1];
f[i][i + 1][i + 1] = a[i + 1][i];
}
for (int l = 3; l <= n; l++) {
for (int i = 1, j = l; j <= n; i++, j++) {
for (int k = i; k <= j; k++) {
if (k == i) {
for (int o = i + 1; o <= j; o++) {
if (a[k][o] && f[i + 1][j][o]) {
f[i][j][k] = 1;
break;
}
}
} else if (k == j) {
for (int o = i; o < j; o++) {
if (a[k][o] && f[i][j - 1][o]) {
f[i][j][k] = 1;
break;
}
}
} else {
if (f[i][k][k] && f[k][j][k]) f[i][j][k] = 1;
}
}
}
}
for (int i = 1; i <= n; i++) {
if (f[1][n][i]) printf("%d ", i);
}
return 0;
}
在還有十幾分鐘的時候, 本來我都放棄優化了的, 已經關掉我的 \(Linux\) 去 \(Windows\) 了, 然後突然, 我在給別人講述我的思路的時候, 他重複了一下我的思路, "也就是說, 只要 \(k\) 能在 \([i, k]\) 中贏, 並且能在 \([k, j]\) 中贏就行是吧." , "woc! 你是天才!" 然後我突然就悟了!!!我說我會 \(n^3\) 的了, 然後想了一會, 好像不是 \(n^3\) , 仔細思索片刻, 確實是 \(n^3\) , 但是由於我太菜了, 有人拿我當蛐蛐, 押寶, 賭我能不能寫出來. 由於非常好寫, 我直接開啟 \(vs\ code\) , 不到 \(2min\) 就寫完了, 然後直接提交, 激動片刻, 沒過大樣例, 但是時間快了很多, 我還在想是不是隻是常數優化? 然後開啟一看, \(MLE\) , 我直接開啟 \(vs\ code\) , 把 \(int\) 的狀態改成了 \(bool\) , 然後提交, 漂亮! 過了. 那個人已經走了, 那個瞧我不起的人. 我的心臟怦怦直跳...一路從五樓衝到一樓, 吃飯去了.
\(n^3\) 其實很簡單, 狀態不變, 我們發現我們其實沒必要列舉 \(o\) , 因為我們已經知道了 \(k\) 在 \([i, k]\) 中以及 \([k, j]\) 中能不能贏了, 所以我們就可以 \(O(1)\) 轉移了, 只需要特判一下 \(k = i\) 以及 \(k = j\) 的情況就行了.
複雜度 \(O(n^3)\) , 期望得分 \(80pts\) .
\(code:\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) +(ch ^ 48);
return x * f;
}
const int N = 705;
int n, a[N][N];
bool f[N][N][N];
char s[N];
int main() {
n = read();
for (int i = 1; i <= n; i++) {
scanf("%s", s + 1);
for (int j = 1; j <= n; j++) {
a[i][j] = s[j] ^ 48;
}
}
for (int i = 1; i <= n; i++) f[i][i][i] = 1;
for (int i = 1; i < n; i++) {
f[i][i + 1][i] = a[i][i + 1];
f[i][i + 1][i + 1] = a[i + 1][i];
}
for (int l = 3; l <= n; l++) {
for (int i = 1, j = l; j <= n; i++, j++) {
for (int k = i + 1; k < j; k++) f[i][j][k] = f[i][k][k] & f[k][j][k];
for (int k = i + 1; k <= j; k++) {
f[i][j][i] = a[i][k] & f[i + 1][j][k];
if (f[i][j][i]) break;
}
for (int k = i; k < j; k++) {
f[i][j][j] = a[j][k] & f[i][j - 1][k];
if (f[i][j][j]) break;
}
}
}
for (int i = 1; i <= n; i++) {
if (f[1][n][i]) printf("%d ", i);
}
return 0;
}
接下來考慮正解, 其實正解已經不需要腦子了, 只可惜我當時不會 \(bitset\) , 加之過於激動, 就沒想.
我們想一下, 我們的狀態存的都只是 \(01\) , 所以我們完全可以開一個 \(bitset\) , 這樣首先就解決了我們記憶體的問題, 因為 \(bool\) 是開不下 \(2000^3\) 的.
然後我們看一下我們的轉移: \(f[i][j][k] = f[i][k][k] \And f[k][j][k]\) .
我們發現 \(k\) 是這三個變數的共同值, 我們就可以開兩個 \(bitset\) , \(f1[i][j]\) 表示 \(j\) 在 \([i, j]\) 裡能不能贏, \(f2[i][j]\) 表示 \(j\) 在 \([j][i]\) 裡能不能贏, 這樣我們就可以用 \(f1\) 和 \(f2\) 來分別表示 \(f[i][k][k]\) 和 \(f[k][j][k]\) 了, 由於第三維相同, 所以轉移可以寫成: \(f[i][j] = f1[i] \And f2[j]\) . 然後特判一下 \(k = i\) 和 \(k = j\) 的情況就行了, 順便處理 \(f1\) 和 \(f2\) .
\(code:\)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
inline int read() {
int x = 0, f = 1;
char ch = getchar();
for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) x = (x << 1) + (x << 3) + (ch ^ 48);
return x * f;
}
const int N = 2005;
int n;
bitset <N> f[N][N], f1[N], f2[N], a[N];
char s[N];
int main() {
n = read();
for (int i = 1; i <= n; i++) {
scanf("%s", s + 1);
for (int j = 1; j <= n; j++) {
a[i][j] = s[j] ^ 48;
}
}
for (int i = 1; i <= n; i++) f[i][i][i] = 1;
for (int l = 2; l <= n; l++) {
for (int i = 1, j = l; j <= n; i++, j++) {
f[i][j] = f1[i] & f2[j];
if ((f[i + 1][j] & a[i]).any()) f[i][j][i] = f2[j][i] = 1;
if ((f[i][j - 1] & a[j]).any()) f[i][j][j] = f1[i][j] = 1;
}
}
for (int i = 1; i <= n; i++) {
if (f[1][n][i]) printf("%d ", i);
}
return 0;
}
轉移 \(O(n/w)\) , 複雜度 \(O(n^3 / w)\) , 期望得分 \(100pts\) .
總結
其實 \(bitset\) 真的是個好東西, 當我們的操作只需要兩個陣列進行位運算, 並且狀態只有 \(01\) 時, 複雜度可以在常數上有非常大的優化.
看不見我看不見我看不見我