基于数组模拟
用途:邻接表(存储图和树)
head -> A -> B -> C -> D -> 空集
编号: 0 1 2 3 -1
值: e[0] = 3 e[1] = 5 e[2] = 7 e[3] = 9
next: ne[0] = 1 ne[1] = 2 ne[2] = 3 ne[3] = 4
每个节点存储value值 和 next指针
e[maxn]:表示某个点的值
ne[maxn]:表示某个点的next指针
单链表算法模板:
// head存储链表头,e[]存储节点的值,ne[]存储节点的next指针,idx表示当前用到了哪个节点
// 第一个插入的下标为0,...,第k个插入的下标为k-1
int head, e[maxn], ne[maxn], idx;
// 初始化
void init()
{
head = -1;
idx = 0;
}
// 在链表头插入一个数a
// 先将a的next指针指向head节点, 再将head指向a
void insert_to_head(int a)
{
e[idx] = a, ne[idx] = head, head = idx ++ ;
}
// 将x这个点插入到下标为k的点后面
// 先让x的next指针指向k的next指针指向的位置
// 将k的next指针指向x
void insert(int k, int x)
{
e[idx] = x, ne[idx] = ne[k], ne[k] = idx ++ ;
}
// 将头结点删除,需要保证头结点存在
void remove_head()
{
head = ne[head];
}
// 将下标是k的点后面的点删除
// 将下标为k的next指针指向 k的next指针的下标的 next指针指向的位置
void remove(int k)
{
ne[k] = ne[ne[k]];
}
// 遍历输出链表的值
void print()
{
for(int i = head; i != -1; i = ne[i])
printf("%d ", e[i]);
printf("\n");
}
用途:优化某些问题
双链表每个节点有两个指针,一个指向前,一个指向后
双链表算法模板:
// e[]表示节点的值,l[]表示节点的左指针,r[]表示节点的右指针,idx表示当前用到了哪个节点
// 令下标为0的点为head头节点,下标为1的点为tail尾节点
// 第1个插入的点下标为2,第k个插入的点下标为 k + 1
int e[maxn], l[maxn], r[maxn], idx;
// 初始化
void init()
{
//0是左端点,1是右端点
r[0] = 1, l[1] = 0;
idx = 2;
}
// 在下标是k的点的右边,插入x
void insert_right(int k, int x)
{
e[idx] = x;
l[idx] = k, r[idx] = r[k];
l[r[k]] = idx, r[k] = idx ++;
}
// 在下标是k的点的左边,插入x
void insert_left(int k, int x)
{
insert_right(l[k], x);
}
// 在最左侧插入一个数x,即在下标为0的点右边插入一个数x
void insert_head(int x)
{
insert_right(0, x);
}
// 在最右侧插入一个数x,即在下标为1的点左边插入一个数x
void insert_tail(int x)
{
insert_left(1, x);
}
// 删除下标为k的点
void remove(int k)
{
l[r[k]] = l[k];
r[l[k]] = r[k];
}
// 遍历输出链表的值
void print()
{
for(int i = r[0]; i != 1; i = r[i])
printf("%d ", e[i]);
printf("\n");
}
先进后出
栈算法模板:
// tt表示栈顶
int stk[maxn], tt = 0;
// 向栈顶插入一个数x
void push(int x)
{
stk[ ++ tt] = x;
}
// 从栈顶弹出一个数
void pop()
{
tt -- ;
}
// 查询栈顶元素
int query()
{
return stk[tt];
}
// 判断栈是否为空
bool empty()
{
if (tt > 0) return false;
else return true;
}
将中缀表达式转换为后缀表达式
/*
遇到运算数时,输出
若遇到左括号,入栈
若遇到右括号,栈顶的运算符依次弹出并输出,直至遇到左括号,左括号弹出但不输出
若遇到的是运算符:
a、如果该运算符的优先级大于栈顶运算符的优先级时,将其压栈
b、如果该运算符的优先级小于等于栈顶运算符的优先级时,将栈顶运算符弹出并输出,
最后若堆栈中还有存留的运算符依次弹出并输出即可。
*/
stack<char> s;
string changeSuffix(string infix) {
string ans = "";
for(int i = 0; i < infix.length(); i ++)
{
if ('0' <= infix[i] && infix[i] <= '9')
{
ans += infix[i];
if (infix[i + 1] > '9' || infix[i + 1] < '0') ans += '.';
}
if ('+' == infix[i] || '-' == infix[i])
{
if (s.empty())
{
s.push(infix[i]);
}
else
{
while (!s.empty())
{
if (s.top() == '(')
{
break;
}
ans += s.top();
ans += '.';
s.pop();
}
s.push(infix[i]);
}
}
if (')' == infix[i])
{
while ('(' != s.top())
{
ans += s.top();
ans += '.';
s.pop();
}
s.pop();
}
if ('*' == infix[i] || '/' == infix[i] || '(' == infix[i])
{
s.push(infix[i]);
}
}
while (!s.empty())
{ //把栈中剩余的符号弹出
if (s.top() == '(')
s.pop();
else
{
ans += s.top();
ans += '.';
s.pop();
}
}
return ans;
}
计算后缀表达式
/*
我们采用.的方式分隔数字
6.5.2.3.+.8.*.+.3.+.*.
遍历表达式,遇到的数字首先放入栈中。
接着读到“+”,则弹出3和2,执行3+2,计算结果等于5,并将5压入到栈中。
读到8,将其直接放入栈中。
读到“*”,弹出8和5,执行8*5,并将结果40压入栈中。
而后过程类似,读到“+”,将40和5弹出,将40+5的结果45压入栈...以此类推。最后求的值288。
*/
stack<int> num;
int sufcalc(string str)
{
for(int i = 0; i < str.length(); i ++)
{
if(str[i] == '.') continue;
if(str[i] >= '0' && str[i] <= '9')
{
string ss;
while(str[i] >= '0' && str[i] <= '9')
{
ss += str[i];
i ++;
}
num.push(stoi(ss));
}
if(str[i] == '+' || str[i] == '-' || str[i] == '*' || str[i] == '/')
{
int sec = num.top();
num.pop();
int fir = num.top();
num.pop();
if(str[i] == '+') num.push(fir + sec);
else if(str[i] == '-') num.push(fir - sec);
else if(str[i] == '*') num.push(fir * sec);
else num.push(fir / sec);
}
}
return num.top();
}
先进先出
普通队列算法模板:
// hh 表示队头,tt表示队尾
int q[maxn], hh = 0, tt = -1;
// 向队尾插入一个数
void push(int x)
{
q[ ++ tt] = x;
}
// 从队头弹出一个数
void pop()
{
hh ++ ;
}
// 队头的值
int query_head()
{
return q[hh];
}
// 队尾的值
int query_tail()
{
return q[tt];
}
// 判断队列是否为空
bool empty()
{
if (hh <= tt) return false;
else return true;
}
循环队列算法模板:
// hh 表示队头,tt表示队尾的后一个位置
int q[N], hh = 0, tt = 0;
// 向队尾插入一个数
q[tt ++ ] = x;
if (tt == N) tt = 0;
// 从队头弹出一个数
hh ++ ;
if (hh == N) hh = 0;
// 队头的值
q[hh];
// 判断队列是否为空
if (hh != tt)
{
}
给定一个序列,求序列中的每个数左边 离他最近的并且比它小的数
暴力做法:
//i的左边比i小的第一个数
for(int i = 0; i < n; i ++)
{
for(int j = i - 1; j >= 0; j --)
{
if(a[i] > a[j])
{
cout << a[j] << endl;
break;
}
}
}
对于暴力寻找性质:
暴力:可以用栈存储i左边的所有元素,对于第i个元素,查找栈顶是否比它小,大的话就弹出
优化:栈里面有些元素是永远不会作为答案出现的,假如a3 >= a5,那么对于5往后的询问,a3永远不会出现为答案
如果存在x < y,并且ax>=ay ,那么ax就可以被删掉,那么栈里面 存储的就一定是严格单调上升的序列了
求第i个数左边,离得最近的比它小的数:
x < y < i,如果存在a[x] >= a[y] 的话,假如a[y] < a[i] ,那么就不会考虑a[x]了,所以a[x] 就没有存在的必要了
所以只需要每次判断栈顶是否比当前元素大,如果大的话就弹出,直到栈顶比当前元素小,输出栈顶并push当前元素
复杂度:O(n)
注意点:
单调栈算法模板:
常见模型:找出每个数左边离它最近的比它大/小的数
int tt = 0;
for (int i = 1; i <= n; i ++ )
{
while (tt && check(stk[tt], i)) tt -- ;
stk[ ++ tt] = i;
}
求滑动窗口中的最大值/最小值
窗口可以用队列来维护
当窗口移动的时候,新的元素从队尾进,旧元素从队尾出
优化:
队列里面有些元素是没有用的
(使用双端队列)
求滑动窗口最大值:
滑动窗口的初始状态是指滑动窗口内只有一个元素,在此基础上向后移动,
第0次有1个元素,队头为-2,
第1次有2个元素,队头为-1,
第2次有3个元素,队头为0,
从第k - 1次开始出现全
第i 次有3个元素,队头为 i - 2,即队头下标为 i - k + 1,就可以判断队头有没有出去
我们每次将有可能成为最大值的元素放入队列末尾
对于当前元素,我们判断队列末尾是不是比当前元素小,假如队尾元素比当前元素小,就不可能成为最大的元素了,就从后面弹出,
直到队列内没有比当前元素更小的元素
但是队列头上可能存在比当前元素更大的元素,所以区间的最大值为队列头
但是每次需要判断队头有没有出去,如果出去了,最大值就变了
单调队列算法模板:
//常见模型:找出滑动窗口中的最大值/最小值
//单调队列里面存的是下标
//当前队首元素为 i - k + 1
int hh = 0, tt = -1;
for (int i = 0; i < n; i ++ )
{
while (hh <= tt && check_out(q[hh])) hh ++ ; // 判断队头是否滑出窗口
while (hh <= tt && check(q[tt], i)) tt -- ;
q[ ++ tt] = i;
}
给定字符串S,和模式串P,求P在S中所有出现的位置的起始下标
暴力做法:
//S长度为n,P长度为m
for (int i = 1; i <= n; i ++ )
{
//寻找原串
bool flag = true;
for (int j = 1; j <= m; j ++ )
{
if (s[i + j - 1] != p[j])
{
flag=false;
break;
}
}
}
利用字符串的特殊信息来减少暴力枚举的次数
当我们匹配了一段失败后,会将模式串向后移动以希望能够继续匹配上
那么就存在第二次开始匹配能成功的一段字符串的前缀等于上一次匹配能成功的一段字符串的后缀
对于每一个点预处理出来一段后缀,要最长的一段后缀和前缀相等
字符串的某一段前后缀相等
next[i] = j
p[1,j] = p[i - j + 1, i]
S = "abababc"
P = "abababab"
12345678
//P的next数组
ne[1] = 0; //a
ne[2] = 0; //ab
ne[3] = 1; //aba
ne[4] = 2; //abab
ne[5] = 3; //ababa
ne[6] = 4; //ababab
ne[7] = 5; //abababa
ne[8] = 6; //abababab
//匹配
KMP算法模板:
// s[]是长文本,p[]是模式串,n是s的长度,m是p的长度
//在某个头文件里面有next,所以一般用ne作为数组名
//求模式串的Next数组:
char p[MAXN], s[MAXN];
int ne[MAXN];
scanf("%d%s%d%s", &n, p + 1, &m, s + 1);
for (int i = 2, j = 0; i <= n; i ++ )
{
while (j && p[i] != p[j + 1]) j = ne[j];
if (p[i] == p[j + 1]) j ++ ;
ne[i] = j;
}
// 匹配
for (int i = 1, j = 0; i <= m; i ++ )
{
while (j && s[i] != p[j + 1]) j = ne[j];
if (s[i] == p[j + 1]) j ++ ;
if (j == n)
{
// 匹配成功,输出匹配位置下标
printf("%d ", i - n);
j = ne[j];
}
}
快速存储和查找字符串集合的数据结构
abcdef
abdef
aced
bcdf
bcff
root
a b
b c c
c d e d f
d e 1d 1f 1f
e 1f
1f
//在单词结束的地方打标记
维护一个字符串集合,支持向集合中插入一个字符串以及询问一个字符串在集合中出现了多少次
关于trie树如何用数组去建树,一维是结点总数,而结点和结点之间的关系(谁是谁儿子)存在第二个维度,比如[0][1]=3, [0]表示根节点,[1]表示它有一个儿子‘b’,这个儿子的下标是3;接着如果有一个[3][2]=8 ; 说明根节点的儿子‘b’也有一个儿子‘c’,这个孙子的下标就是8;这样传递下去,就是一个字符串。随便给一个结点[x][y], 并不能看出它在第几层,只能知道,它的儿子是谁。
Trie树算法模板:
int son[N][26], cnt[N], idx;
// 0号点既是根节点,又是空节点
// son[][]存储树中每个节点的子节点,每个字母最多再往外连26条边
// cnt[]存储以每个节点结尾的单词数量
// idx存储当前用到了哪个下标
// 插入一个字符串
void insert(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
//找到下一条边的编号,如果未被创建就创建一下
if (!son[p][u]) son[p][u] = ++ idx;
p = son[p][u];
}
//以p结尾的单词增加
cnt[p] ++ ;
}
// 查询字符串出现的次数
int query(char *str)
{
int p = 0;
for (int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if (!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
o(1)
基本原理:
问题:
优化:
注意:
朴素并查集算法模板:
int p[MAXN]; //存储每个点的祖宗节点
// p[x] == x代表这个点是树根
// 返回x的祖宗节点 + 路径压缩
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ ) p[i] = i;
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
维护size(每个集合的元素个数)的并查集算法模板:
int p[MAXN], siz[MAXN];
// p[]存储每个点的祖宗节点, siz[]只有祖宗节点的有意义,表示祖宗节点所在集合中的点的数量
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
// 初始化,假定节点编号是1~n, siz[]也要初始化为1
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
siz[i] = 1;
}
// 合并a和b所在的两个集合:
// 有的情况下要判断a和b是否在一个集合里面,假如在一个集合里面还要操作的话集合元素就会翻倍
if(find(a) == find(b)) continue;
siz[find(b)] += siz[find(a)];
p[find(a)] = find(b);
维护到祖宗节点距离的并查集算法模板:
int p[N], d[N];
//p[]存储每个点的祖宗节点, d[x]存储x到p[x]的距离
// 返回x的祖宗节点
int find(int x)
{
if (p[x] != x)
{
int u = find(p[x]);
d[x] += d[p[x]];
p[x] = u;
}
return p[x];
}
// 初始化,假定节点编号是1~n
for (int i = 1; i <= n; i ++ )
{
p[i] = i;
d[i] = 0;
}
// 合并a和b所在的两个集合:
p[find(a)] = find(b);
d[find(a)] = distance; // 根据具体问题,初始化find(a)的偏移量
手写一个堆需要能实现哪些操作:
堆的底层是完全二叉树
小根堆:每个点都是小于等于左右儿子的值(根节点就是最小值)
存储方式:用数组存,1号点为根节点,x节点的左儿子节点下标为2x, x节点的右儿子节点下标为2x+1
更新的时候,每个点和左右儿子的较小值进行交换,以实现up和down操作
插入:heap[++ size] = x; up(size) 在堆的最后位置放x,up里面传要进行操作的位置
堆中的最小值:heap[1]
删除最小值: heap[1] = heap[size]; size – ; down(1); 用最后一个元素来覆盖最小的元素
删除任意元素:heap[k] = heap[size]; size --; down(k); up(k);
修改任意元素: heap[k] = x; down(k); up(k);
堆算法模板:
#include
#incldue <algorithm>
// h[N]存储堆中的值, h[1]是堆顶,x的左儿子是2x, 右儿子是2x + 1
// ph[k]存储第k个插入的点在堆中的位置
// hp[k]存储堆中下标是k的点是第几个插入的
int h[MAXN], ph[MAXN], hp[MAXN], siz;
// 交换两个点,及其映射关系
void heap_swap(int a, int b)
{
swap(ph[hp[a]],ph[hp[b]]);
swap(hp[a], hp[b]);
swap(h[a], h[b]);
}
void down(int u)
{
int t = u;
if (u * 2 <= siz && h[u * 2] < h[t]) t = u * 2;
if (u * 2 + 1 <= siz && h[u * 2 + 1] < h[t]) t = u * 2 + 1;
if (u != t)
{
heap_swap(u, t);
// 当对于第几个插入没有执念的时候,直接swap(h[u], h[t])也可以
down(t);
}
}
void up(int u)
{
while (u / 2 && h[u] < h[u / 2])
{
heap_swap(u, u / 2);
u >>= 1;
}
}
// O(n)建堆
for (int i = n / 2; i; i -- ) down(i);
// 向堆中插入元素
h[++ siz] = x;
up(siz);
// 向堆中插入第k个元素x
h[++ siz] = x;
ph[++ m] = siz, hp[siz] = m;
up(siz);
// 堆中的最小值
h[1]
// 删除堆的最小值
h[1] = h[siz];
// 或者heap_swap(1, siz);
siz --;
down(1);
// 删除任意元素
h[k] = h[siz];
siz --;
down(k), up(k);
// 删除第k个插入的元素,注意先把ph[k]先存起来
k = ph[k];
heap_swap(k, siz);
siz --;
down(k), up(k);
// 修改任意元素
h[k] = x;
down(k), up(k);
// 修改第k个插入的元素为x
k = ph[k];
h[k] = x;
down(k), up(k);
将一些比较复杂的数据映射到0-N这些数,比如将0 - 109映射到0 - 105
将输入x (-10^9 ~ + 10^9)通过哈希函数转换为输出y (0 ~ +10^5)
问题:
拉链法:
开一个一维数组来存储所有的哈希值,如果有出现冲突的话就在下面把数拉出来
数组a[] : 1 2 3 4 5 6
|
23
|
13
开放寻址法:
开一个一维数组,开到数据范围的2-3倍
一般哈希算法模板:
(1) 拉链法
#include
#include
// N一般为质数,比如1000007
// h[]是拖的一条链
// e[]存放值,ne[]存放指针,idx表示用到了哪些位置
int h[MAXN], e[MAXN], ne[MAXN], idx;
// 向哈希表中插入一个数
void insert(int x)
{
int k = (x % N + N) % N;
e[idx] = x;
ne[idx] = h[k];
// 让新的点的next指针指向h[k],再让h[k]指向新的点
h[k] = idx ++ ;
}
// 在哈希表中查询某个数是否存在
bool query(int x)
{
int k = (x % N + N) % N;
for (int i = h[k]; i != -1; i = ne[i])
if (e[i] == x)
return true;
return false;
}
// 链表头最开始初始化为-1
memset(h, -1, sizeof(h));
(2) 开放寻址法
const int null = 0x3f3f3f3f;
int h[MAXN];
// 如果x在哈希表中,返回x的下标;如果x不在哈希表中,返回x应该插入的位置
int query(int x)
{
int t = (x % N + N) % N;
while (h[t] != null && h[t] != x)
{
t ++ ;
if (t == N) t = 0;
}
return t;
}
memset(h, 0x3f, sizeof(h));
int k = query(x);
// 插入
h[k] = x;
// 查找
if(h[k] != null) cout << "YES" << endl;
else cout << "NO" << endl;
字符串前缀哈希法
Str = “ABCABCDEYXCAcwing”
预处理所有前缀的哈希
h[0] = 0
h[1] = "A"的哈希值
h[2] = "AB"的哈希值
把字符串看成是p进制的数
ABCD
(1234)p
哈希值 = (1 * p^3 + 2 * p^2 + 3 * p^1 + 4 * p^0 ) mod q
不能映射成0
此处假定不存在冲突
可以利用前缀哈希求出所有子串的哈希值
计算子串 str[l ~ r] 的哈希值:
1 l-1 l r
r-1 l l-1 0
h[1 - r] = h[r]
h[1 - l-1] = h[l-1] * p^(r-l+1)
用于判断两个子串是否相同
字符串哈希算法模板:
// 核心思想:将字符串看成P进制数,P的经验值是131或13331,取这两个值的冲突概率低
// 小技巧:取模的数用2^64,这样直接用unsigned long long存储,溢出的结果就是取模的结果
typedef unsigned long long ULL;
const int P = 13331;
const ULL q = pow(2,64);
ULL h[MAXN], p[MAXN]; // h[k]存储字符串前k个字母的哈希值, p[k]存储 P^k mod 2^64
char str[MAXN];
// 初始化
p[0] = 1;
for (int i = 1; i <= n; i ++ )
{
h[i] = h[i - 1] * P + str[i];
p[i] = p[i - 1] * P;
}
// 计算子串 str[l ~ r] 的哈希值
ULL get(int l, int r)
{
return h[r] - h[l - 1] * p[r - l + 1];
}
C++ STL算法模板:
vector, 变长数组,倍增的思想
// 定义
vector<int> a;
vector<int> a(n);
vector<int> a(10, 3); //定义一个长度为10的vector,并初始化为3
// 函数
a.size() 返回元素个数
a.empty() 返回是否为空
a.clear() 清空
a.front()/a.back()
a.push_back()/a.pop_back()
a.begin()/a.end() // 迭代器
// 遍历
for(auto x : a)
for(vector<int>::iterator i = a.begin(); i != a.end(); i ++)
for(auto i = a.begin(); i != a.end(); i ++)
for(int i = 0; i < a.size(); i ++)
[]
支持比较运算,按字典序
pair<int, int> p;
p.first, 第一个元素
p.second, 第二个元素
支持比较运算,以first为第一关键字,以second为第二关键字(字典序)
pair<int, pair<int,int> >
// 初始化
p = make_pair(10, "ab");
p = {20, "ac"};
string,字符串
string s;
s.size() / s.length() 返回字符串长度
s.empty()
s.clear()
s += str
s.substr(起始下标,(子串长度)) 返回子串
// s.substr(1, 2);
s.substr(1) //返回从1开始的所有子串
s.c_str() 返回字符串所在字符数组的起始地址
printf("%s\n", a.c_str());
queue, 队列
// 没有clear()
queue<int> q;
q.size()
q.empty()
q.push() 向队尾插入一个元素
q.front() 返回队头元素
q.back() 返回队尾元素
q.pop() 弹出队头元素
priority_queue, 优先队列,默认是大根堆
#include
priority_queue<int> q;
q.clear();
size()
empty()
push() 插入一个元素
top() 返回堆顶元素
pop() 弹出堆顶元素
定义成小根堆的方式:priority_queue<int, vector<int>, greater<int>> q;
stack, 栈
size()
empty()
push() 向栈顶插入一个元素
top() 返回栈顶元素
pop() 弹出栈顶元素
deque, 双端队列
size()
empty()
clear()
front()/back()
push_back()/pop_back()
push_front()/pop_front()
begin()/end() //迭代器
[]
set, map, multiset, multimap, 基于平衡二叉树(红黑树),动态维护有序序列
size()
empty()
clear()
begin()/end()
++, -- 返回前驱和后继,时间复杂度 O(logn)
set/multiset
// set里面没有重复元素,multiset里面有重复元素
set<int> s;
multiset<int> MS;
insert() 插入一个数
find() 查找一个数 // 不存在的话返回end()迭代器
count() 返回某一个数的个数
erase()
(1) 输入是一个数x,删除所有x O(k + logn)
(2) 输入一个迭代器,删除这个迭代器
lower_bound() / upper_bound()
lower_bound(x) 返回大于等于x的最小的数的迭代器
upper_bound(x) 返回大于x的最小的数的迭代器
map/multimap
insert() 插入的数是一个pair
erase() 输入的参数是pair或者迭代器
find()
[] 注意multimap不支持此操作。 时间复杂度是 O(logn)
// map a;
// a["yxc"] = 1; 插入
// cout << a["yxc"] << endl; 查询
lower_bound()/upper_bound()
unordered_set, unordered_map, unordered_multiset, unordered_multimap, 哈希表
和上面类似,增删改查的时间复杂度是 O(1)
内部无序, 不支持 lower_bound()/upper_bound(), 迭代器的++,--
bitset, 圧位
bitset<10000> s;
// 是正常的bool数组内存的1/8
~, &, |, ^
>>, <<
==, !=
[]
count() 返回有多少个1
any() 判断是否至少有一个1
none() 判断是否全为0
set() 把所有位置成1
set(k, v) 将第k位变成v
reset() 把所有位变成0
flip() 等价于~
flip(k) 把第k位取反