bitset

bitset

从暑假开始就一直听到 bitset 优化, 而且好像还挺厉害, 虽然只是常数优化, 但是却非常的好用.

bitset 是啥

bitset 其实就是一个二进制数, 包含在 bitset 库里(万能头也有), 声明如下:

bitset <N> B;

表示声明了一个位长为 Nbitset , 所占用的内存也仅仅是 N 位, 也就是说, bitset 的每一位占用仅仅是 1 位, 与同样是表示 01bool 相比, 小了 8 倍 ( bool 最小时是 1 bit, bitset 一位只要 1 Byte).

所以当我们只需要 01 时, bitset 是不二之选, 空间复杂度大大减小.

bitset 不止能优化空间, 还能优化时间, 我们 nbool 做位运算, 时间复杂度是 O(n) , 而一个 n 位的 bitset 做位运算, 时间复杂度仅为 n/w . 这个 w 是一个常数, 取决于你的计算机位数, 如果你是 32 位系统, 那就是 32 , 64 位系统就是 64 .

尽管只是常数优化, 但是这个优化已经非常大了, 可以让我们原本复杂度 1e91e10 左右的代码卡到 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 个人按照 1n 的顺序站成一排, , 给出 n 个人之间 pk 的胜负关系, 求哪些人最终可以赢.

subtask 1(10pts):n10 .

subtask 2(10pts):n50 .

subtask 3(20pts):n120 .

subtask 4(20pts):n300 .

subtask 5(20pts):n700 .

subtask 6(20pts):n2000 .

时间限制: 2s
空间限制: 512MB

我一上来想成了拓扑排序 + 缩点, 后来发现不行, 然后觉得是个 DP , 于是开始想 DP .

想了好久, 感觉是个区间 DP , 但是暂时没有头绪, 所以我就干脆点, 先把状态设出来, 于是状态就出来了: f(i,j,k) 表示 k[i,j] 中能不能赢.

先把状态设出来, 管他会不会转移.

然而状态出来之后, 转移也就自然而然的出来了.

a[i][j] 表示 ij pk 的结果.

我们考虑在区间 [i,j] 中, 什么情况下 k 才能赢, 于是我们找到了 k 赢的充要条件: 当且仅当在 [i,k1] 中存在能够被 k 打败的人, 并且这个人能够在 [i,k1] 中赢, 也就是 a[k][o] & f[i][k1][o] , 右边也是同理, a[k][o] & f[k+1][j][o] .

因为需要枚举 o , 所以复杂度 O(n4) . 期望得分 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;
}

在还有十几分钟的时候, 本来我都放弃优化了的, 已经关掉我的 LinuxWindows 了, 然后突然, 我在给别人讲述我的思路的时候, 他重复了一下我的思路, "也就是说, 只要 k 能在 [i,k] 中赢, 并且能在 [k,j] 中赢就行是吧." , "woc! 你是天才!" 然后我突然就悟了!!!我说我会 n3 的了, 然后想了一会, 好像不是 n3 , 仔细思索片刻, 确实是 n3 , 但是由于我太菜了, 有人拿我当蛐蛐, 押宝, 赌我能不能写出来. 由于非常好写, 我直接打开 vs code , 不到 2min 就写完了, 然后直接提交, 激动片刻, 没过大样例, 但是时间快了很多, 我还在想是不是只是常数优化? 然后打开一看, MLE , 我直接打开 vs code , 把 int 的状态改成了 bool , 然后提交, 漂亮! 过了. 那个人已经走了, 那个瞧我不起的人. 我的心脏怦怦直跳...一路从五楼冲到一楼, 吃饭去了.

n3 其实很简单, 状态不变, 我们发现我们其实没必要枚举 o , 因为我们已经知道了 k[i,k] 中以及 [k,j] 中能不能赢了, 所以我们就可以 O(1) 转移了, 只需要特判一下 k=i 以及 k=j 的情况就行了.

复杂度 O(n3) , 期望得分 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 是开不下 20003 的.

然后我们看一下我们的转移: f[i][j][k]=f[i][k][k]&f[k][j][k] .

我们发现 k 是这三个变量的共同值, 我们就可以开两个 bitset , f1[i][j] 表示 j[i,j] 里能不能赢, f2[i][j] 表示 j[j][i] 里能不能赢, 这样我们就可以用 f1f2 来分别表示 f[i][k][k]f[k][j][k] 了, 由于第三维相同, 所以转移可以写成: f[i][j]=f1[i]&f2[j] . 然后特判一下 k=ik=j 的情况就行了, 顺便处理 f1f2 .

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(n3/w) , 期望得分 100pts .

总结

其实 bitset 真的是个好东西, 当我们的操作只需要两个数组进行位运算, 并且状态只有 01 时, 复杂度可以在常数上有非常大的优化.

posted @   sshadows  阅读(185)  评论(5)    收藏  举报
编辑推荐:
· 记一次SSD性能瓶颈排查之路——寿命与性能之间的取舍
· 理解 .NET 结构体字段的内存布局
· .NET 9中的异常处理性能提升分析:为什么过去慢,未来快
· 字符集、编码的前世今生
· Web性能优化:从 2 秒到200毫秒
阅读排行:
· 【故障公告】博客主站遭遇很奇怪的疯狂攻击
· 我与博客园的20年
· 刚刚,Cursor 1.0炸裂发布!4大亮点实战
· SQL Server 2025 预览版新功能点评
· 理解 .NET 结构体字段的内存布局
点击右上角即可分享
微信分享提示