1. 程式人生 > 其它 >20191325第七八章學習筆記

20191325第七八章學習筆記

bitset

從暑假開始就一直聽到 \(bitset\) 優化, 而且好像還挺厲害, 雖然只是常數優化, 但是卻非常的好用.

bitset 是啥

\(bitset\) 其實就是一個二進位制數, 包含在 \(bitset\) 庫裡(萬能頭也有), 宣告如下:

bitset <N> B;

表示聲明瞭一個位長為 \(N\)\(bitset\) , 所佔用的記憶體也僅僅是 \(N\) 位, 也就是說, \(bitset\) 的每一位佔用僅僅是 \(1\) 位, 與同樣是表示 \(01\)\(bool\) 相比, 小了 \(8\) 倍 ( \(bool\) 最小時是 \(1\ bit\)

, \(bitset\) 一位只要 \(1\ Byte\)).

所以當我們只需要 \(01\) 時, \(bitset\) 是不二之選, 空間複雜度大大減小.

\(bitset\) 不止能優化空間, 還能優化時間, 我們 \(n\)\(bool\) 做位運算, 時間複雜度是 \(O(n)\) , 而一個 \(n\) 位的 \(bitset\) 做位運算, 時間複雜度僅為 \(n / w\) . 這個 \(w\) 是一個常數, 取決於你的計算機位數, 如果你是 \(32\) 位系統, 那就是 \(32\) , \(64\) 位系統就是 \(64\) .

儘管只是常數優化, 但是這個優化已經非常大了, 可以讓我們原本複雜度 \(1e9 \sim 1e10\)

左右的程式碼卡到 \(1e8\) 左右.

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\) 時, 複雜度可以在常數上有非常大的優化.

看不見我看不見我看不見我