位图法就是用一个位来存放某个数据的存在状态。
比如:用一个20位长的内存空间可以表示一个所有元素都小于20的简单的非负整数集合。对于集合{1,2,3,5,8,13}可用如下方式表示
0 1 1 1 0 1 0 0 1 0 0 0 0 1 0 0 0 0 0 0
这样,我们就可以用20位来表示上面集合所有元素的存在状态。
当然利用位图我们可以节省很多空间,比如素数筛选中,有一个bool数组prime,我们知道,在C语言中所有数据类型中的最小的至少也是一个字节,而一般来说,bool在C语言中是占一个字节的,也就是8位,可以看出,如果我们用一个位来表示prime数组中的数字i是否是素数,那么空间只需要原来的八分之一。
当然假设我们定义了一个int数组,而想用这个int数组的每一位来表示一个数的存在状态,那么就有两个重要的操作:设置位置为x的数据的存在状态,读取位置为x的数据存在状态。我分别来讲解这两种操作:
(一)设置位置为x的数据的存在状态
对于这个操作,我们应该先找到x所在的位置,包括数组下标的位置和下标对应的位的位置。对于int数组来说,因为一个int占4字节,那么有32位,设SHIFT = 5,RADIX = (1 << SHIFT) - 1,用flag[]数组来表示,那么此操作就可以用如下代码来表示:
void SetBit(int x) { flag[x>>SHIFT] |= (1<<(x&RADIX)); }
(二)读取位置为x的数据存在状态
对于读操作,跟写操作差不多,也是应该先找到x所在的位,然后&一下就取出对应的位的状态了,如下:
bool GetBit(int x) { return flag[x>>SHIFT] & (1<<(x&RADIX)); }
这样通过位图这种数据结构,我们可以用较少的空间来实现。
下面是用位图实现的素数筛选:
#include <iostream> #include <string.h> #include <algorithm> #include <stdio.h> #include <math.h> using namespace std; const int N = 100000005; const int M = 6000005; const int SHIFT = 5; const int RADIX = (1 << SHIFT) - 1; int flag[(N>>SHIFT)+1]; int p[M]; int k; inline void SetBit(int x) { flag[x>>SHIFT] |= (1<<(x&RADIX)); } inline bool GetBit(int x) { return flag[x>>SHIFT] & (1<<(x&RADIX)); } void isprime() { k = 0; for(int i=2; i<N; i++) { if(!GetBit(i)) { p[k++] = i; for(int j=i+i; j<N; j+=i) SetBit(j); } } } int main() { isprime(); return 0; }
以上是位图的实现原理及操作。实际上在C++的STL中,有一个bitset位图容器,它所定义的数据就是占一个位,这样就方便了许多,所以利用它实现起来比较高效。用法如下:
#include <iostream> #include <string.h> #include <algorithm> #include <stdio.h> #include <math.h> #include <bitset> using namespace std; const int N = 100000005; const int M = 6000005; bitset<N> prime; int p[M]; int k; void isprime() { k = 0; prime.set(); for(int i=2; i<N; i++) { if(prime[i]) { p[k++] = i; for(int j=i+i; j<N; j+=i) prime[j] = false; } } } int main() { isprime(); return 0; }