把自动机用作 Key-Value 存储
以前只有代码,最近简单写了一点文档: google code 上的链接(总是最新)
自动机是什么
这里只讲有穷自动机,自动机的形式化定义,可以参考 wikipedia:
- 自动机
- 有穷状态自动机 (FSA)
- 确定性的有穷自动机 (DFA)
- 这是本文的重点
- 非确定性的有穷自动机 (NFA)
- 很多 DFA 的构造需要 NFA 作为媒介
DFA 的最小化
- DFA 的等同
- 如果两个dfa的状态转移图同构,那么这两个 DFA 等同
- DFA 的等价
- 如果两个 DFA 能接受的语言集合相同,那么这两个 DFA 等价
- 等价的 DFA 不一定等同
- 最小化的 DFA
- 对于任何一个 DFA,存在唯一一个与该 DFA 等价的 MinDFA,该 MinDFA 的状态数是与原 DFA 等价的所有 DFA 中状态数最小的
- 最小化的 DFA 需要的内存更小
- 各种优化的 DFA 最小化算法是本软件的核心竞争力之一
将 DFA 用做字典
字典,可以认为就是一个 map<string, Data>,这是最简单直接的表达,在 C++ 标准库中,map是用 RBTree 实现的,当然,也可以用 hash_map(或称为 unordered_map)。这些字典在标准库中都有,不是特别追求cpu和内存效率的话,可以直接拿来时使用。
但是,要知道,对于一般应用,将字典文件(假定是文本文件)加载到 map/hash_map 之后,内存占用量要比字典文件大两三倍。当数据源很大时,是不可接受的,虽然在现在这年代,几G可能都算不上很大,但是,如果再乘以3,可能就是十几二十G了,姑且不论数据加载产生的载入延迟(可能得几十分钟甚至一两个小时)。
用 DFA 存储字典,在很多专门的领域中是一个标准做法,例如很多分词库都用 DoubleArray Trie DFA 存储词库,语言识别软件一般也用 DFA 来存储语音。
无环DFA (ADFA, Acyclic DFA)
用做字典的 DFA 是无环DFA (ADFA, Acyclic DFA),ADFA 的图是 DAG (有向无环图)。Trie 是一种最简单的 ADFA,同时也是(所有ADFA等价类中)最大的 ADFA。DoubleArray虽然广为人知,但相比 MinADFA,内存消耗太大。
编译
svn checkout http://febird.googlecode.com/svn/trunk/ febird-read-only
cd febird-read-only
make
cd netbeans-cygwin/automata/
make
ll rls/*/*.exe # 有用的是以下几个,后面会讲到详细用法
-rwxrwxr-x 1 user user 13394937 2013-08-08 11:47 rls/dawg/ldzip.exe
-rwxrwxr-x 1 user user 8902422 2013-08-08 16:26 rls/forward_match/aunzip.exe
-rwxrwxr-x 1 user user 22519765 2013-08-08 11:47 rls/forward_match/ldfa_sset.exe
-rwxrwxr-x 1 user user 9050159 2013-08-08 16:26 rls/forward_match/on_key_value.exe
-rwxrwxr-x 1 user user 8994311 2013-08-08 16:26 rls/forward_match/on_suffix_of.exe
内存用量/查询性能
本软件实现了两种 DFA,一种为运行速度优化,另一种为内存用量优化,前者一般比后者快4~6倍,后者一般比前者节省内存30~40%,具体使用哪一种,由使用者做权衡决策。
不同的数据,DFA有不同的压缩率。 对于典型的应用,为内存优化的DFA,压缩率一般在3倍到20之间,相比RBTree/HashMap的膨胀3倍,内存节省就有9倍到60倍!同时仍然可以保持极高的查询速度(keylen=16字节,QPS 在 40万到60万之间),为速度优化的版本,QPS 有 250万。下面是几个性能数据(map1, dwag,仅 set 的话,尺寸会更小):
| size(bytes) | gzip | DFA (small+slow) | DFA (big+fast) | KeyLen | QPS(big+fast) | |
| File1(Query) | 226,433,393 | 96,293,588 | 101,125,415 | 170,139,298 | 16.7 | 24,000,000 |
| File2(URL) | 485,968,345 | 25,094,568 | 13,990,737 | 35,548,376 | 109.2 | 900,000 |
map 与 set
传统上,ADFA 只能用作 set<Key> ,也就是字符串的集合。但是,本软件可以把 ADFA 用作 map<Key, Value>,通过两种方式可以达到这个目标:
- map1: 扩展 ADFA(从而 DFA 的尺寸会大一点),查找 key 时,同时计算出一个整数 index,该 index 取值范围是 [0, n),n 是 map.size()。从而,应用程序可以在外部存储一个大小为 n 的数组,用该 index 去数组直接访问 value。
- 本软件中有一个 utility 类用来简化这个流程
- map2: 将 Value 编码成 string 形式,然后再生成一个新的 string kv = key + '\t' + value,将 kv 加入 ADFA,在这种情况下,同一个 key 可以有多个 value,相当于 std::multimap<string, Value>,这种方法的妙处在于,如果多个key对应的value相同,这些value就被自动机压缩成一份了!
- 更进一步,这种方法可以扩展到允许 key 是一个正则表达式!(目前还不支持)
自动机实用程序
本软件包含几个程序,用来从文本文件生成自动机,生成的自动机可以用C++接口访问,这样,就将自动机的存储与业务逻辑完全分离。
- ldfa_sset.exe options < input_text_file
||
| options | arguments | comments |
| -o | 输出文件:为 尺寸 优化的自动机 | 匹配速度较慢,尺寸较小 |
| -O | 输出文件:为 速度 优化的自动机 | 匹配速度很快,尺寸较大 |
| -l | 状态字节:为 尺寸 优化的自动机,可取值 4,5,6,7 | 自动机的每个状态占几个字节,越大的数字表示自动机能支持的最大状态数也越大,
一般5就可以满足大多数需求了 |
| -b | 状态字节:为 速度 优化的自动机,可取值 4,5,6,7 | |
| -t | 无参数 | 输出文本,仅用于测试 |
| -c | 无参数 | 检查自动机正确性,仅用于测试 |
- ldzip.exe options < input_text_file
生成扩展的DFA,可以计算 key 的 index 号,对应 map 的第一种实现方式,使用方法同
ldfa_sset.exe
- aunzip.exe < dfa_binary_file
解压 dfa_binary_file,按字典序将解压结果的文本写到标准输出 stdout ,可以接受基本dfa (由 ldfa_sset.exe 生成的) 和扩展dfa文件(由 ldzip.exe 生成的)
- on_suffix_of.exe text1 text2 ... < dfa_binary_file
打印匹配所有 text
n 的前缀的行 (ldfa_sset.exe 或 ldzip.exe 输入文本的行) 的后缀
- on_key_value.exe text1 text2 ... < dfa_binary_file
打印匹配所有 text
n 的前缀的 Key (ldfa_sset.exe 或 ldzip.exe 输入文本的行) 的 value, 用于测试 map 实现方法2 (Key Value 之间加分隔符)
自动机的 C++接口
本软件使用了 C++11 中的新特性,所以必须使用支持 C++11 的编译器,目前测试过的编译器有 gcc4.7 和 clang3.1。不过为了兼容,我提供了C++98 的接口,一旦编译出了静态库/动态库,C++11 就不再是必需的了。
#include<febird/automata/dfa_interface.hpp>
febird/automata/dfa_interface.hpp
头文件 febird/automata/dfa_interface.hpp 中主要包含以下 class:
DFA_Interface
这个类是最主要的 DFA 接口,对于应用程序,总是从 DFA_Interface::load_from(file) 加载一个自动机(ldfa_sset.exe 或 ldzip.exe 生成的自动机文件),然后调用各种查找方法/成员函数。
- for_each_suffix(prefix, on_suffix[, tr])
- 该方法接受一个字符串prefix,如果prefix是自动机中某些字符串的前缀,则通过 on_suffix(nth,suffix) 回调,告诉应用程序,前缀是prefix的那些字符串的后缀(去除prefix之后的剩余部分),nth 是后缀集合中字符串的字典序。 tr 是一个可选参数,用来转换字符,例如全部转小写,将 ::tolower 传作 tr 即可
- 例如:对字符串集合 {com,comp,comparable,comparation,compare,compile,compiler,computer}, prefix=com 能匹配所有字符串(其中nth=0的后缀是空串),prefix=comp能匹配除com之外的所有其它字符串,此时nth=0的也是空串,而 compare 的后缀 are 对应的 nth=1
- match_key(delim,str,on_match[, tr])
- 该方法用于实现 map2,delim 是 key,val 之间的分隔符(如 '\t' ),key中不可包含delim,str 是扫描的文本,如果在扫描过程中,发现 str 的长度为 Kn 的前缀 P 匹配某个 key,就将该 key 对应的所有 value 通过 on_match(Kn,idx, value) 回调告诉调用方, idx是同一个key对应的value集合中当前value的字典序。
DAWG_Interface
这个类用来实现 map1,DAWG 的全称是 Directed Acyclic Word Graph,可以在 ADFA 的基础上,在匹配的同时,计算一个字符串在 ADFA 中的字典序号(如果存在的话),同时,也可以从字典序号计算出相应的字符串,时间复杂度都是O(strlen(word))。
- size_t index(string word)
- 计算 word 的字典序,如果不存在,返回 size_t(-1)
- string nth_word(size_t nth)
- 从字典序 nth 计算对应的 word,如果 nth 在 [0, n) 之内,一定能得到相应的 word,如果 nth 不在 [0, n) 之内,会抛出异常
- v_match_words(string, on_match[, tr])
- 依次对 string 的所有前缀计算 index,并通过 on_match(prelen,nth) 回调返回计算结果,prelen是匹配的前缀长度,该函数也有可选的 tr 参数
- longest_prefix(string, size_t*len, size_t*nth[, tr])
- 相当于 v_match_words 的特化版,只返回最长的那个 prefix
超级功能
以拼音输入法为例
为了保证输入效率,我们需要有一个从 词条拼音 到 词条汉字 的映射表,比如,拼音序列 ZiDongJi 对应的词条是 自动机 , 自冻鸡 ;从而,逻辑上讲,这是一个 map<string,list<string> >。
假定我们有一个汉语词表,该词表的词条超过千万,每个词条可能是一句话(比如名言警句),并且,因为汉语中存在多音字,从而,包含多个多音字的词条都可能有很多种发音,这个数目在最坏情况下是指数级的,第一个字有 X1 个读音,第二个字有 X2 个读音,...,n 个字的词条就有 X1*X2*X3*...*Xn 种读音。
如果我们用 HashMap/std::unordered_map 或 TreeMap/std::map 保存这个注音词典,对于普通无多音字的词条,无任何问题。一旦有多音字, X1*X2*X3*...*Xn 可能是一个非常大的数字,几十,几百,几千,几千万都有可能,这完全是不可接受的!一个折衷的办法就是仅选择概率最大的拼音,但很可惜,有些情况下这个拼音可能是错的!
用自动机解决该问题,最简单的方法就是用 string kv = X1*X2*X3*...*Xn + '\t' + 汉字词条 来逐个插入,动态 MinADFA 算法可以保证内存用量不会组合爆炸,但是,除了内存,还有时间,如此逐个展开,时间复杂度也是指数的!
这个问题我想了很久,终于有一天,想出了一个完美的解决办法:
- 在 MinADFA 中加入一个功能:在线性时间内,给一个唯一的前缀,追加一个 ADFA后缀 ,该 ADFA后缀 可以包含 X1*X2*X3*...*Xn 个 word,并且结构还可以任意复杂(最近我的研究发现,该后缀甚至不必是ADFA,可以包含环!)
- 然后,将 string kv = X1*X2*X3*...*Xn + '\t' + 汉字词条 翻转, rev(X1*X2*X3*...*Xn) 构成一个 ADFA, rev(汉字词条) 是一个唯一前缀
- 将所有的词条做这样的处理,就构成一个 DFA({rev(拼音+汉字)}) ,然后再将该 DFA 翻转,得到 NFA(rev(DFA({rev(拼音+汉字)})),再将该 NFA 转化成 DFA
- 查找时,使用 map2 的方法(DFA_Interface::match_key),因为组合爆炸,不能用 map1
这个方法非常完美!虽然 NFA 转化 DFA 最差情况下是 NSpace(比NP还难) 的,但是在这里,可以证明,这个转化是线性的:O(n)。
把自动机用作 Key-Value 存储的更多相关文章
- Tair分布式key/value存储
[http://www.lvtao.net/database/tair.html](特别详细) tair 是淘宝自己开发的一个分布式 key/value 存储引擎. tair 分为持久化和非持久化 ...
- 淘宝分布式 key/value 存储引擎Tair安装部署过程及Javaclient測试一例
文件夹 1. 简单介绍 2. 安装步骤及问题小记 3. 部署配置 4. Javaclient測试 5. 參考资料 声明 1. 以下的安装部署基于Linux系统环境:centos 6(64位),其他Li ...
- Consul之:key/value存储
key/value作用 动态修改配置文件 支持服务协同 建立leader选举 提供服务发现 集成健康检查 除了提供服务发现和综合健康检查,Consul还提供了一个易于使用的键/值存储.这可以用来保存动 ...
- MySQL key/value存储方案(转)
需求 250M entities, entities表共有2.5亿条记录,当然是分库的. 典型解决方案:RDBMS 问题:由于业务需要不定期更改表结构,但是在2.5亿记录的表上增删字段.修改索引需要锁 ...
- Java中Map<Key, Value>存储结构根据值排序(sort by values)
需求:Map<key, value>中可以根据key, value 进行排序,由于 key 都是唯一的,可以很方便的进行比较操作,但是每个key 对应的value不是唯一的,有可能出现多个 ...
- java中key-value数据有重复KEY如何存储
http://www.iteye.com/problems/87219 Map<Key, List<Value>>, 这个好 师兄厉害,给介绍了个神器:guava
- php array key 的存储规则
刚刚写程序遇到php数组取值的问题,发现字符串和数字取出来的是一样的. key 可以是 integer 或者string.value 可以是任意类型. 此外 key 会有如下的强制转换: 包含有合法整 ...
- MBTiles 1.2 规范翻译
MBTiles 1.2 可以参考超图的文档MBTiles扩展 具体实现可以参考浅谈利用SQLite存储离散瓦片的思路和实现方法 mapbox提供了一个简单实现测试代码,github地址在这里https ...
- [转]MBTiles 1.2 规范翻译
MBTiles 1.2 可以参考超图的文档MBTiles扩展具体实现可以参考浅谈利用SQLite存储离散瓦片的思路和实现方法 mapbox提供了一个简单实现测试代码,github地址在这里https: ...
随机推荐
- AC日记——[HNOI2014]世界树 bzoj 3572
3572 思路: 虚树+乱搞: 代码: #include <bits/stdc++.h> using namespace std; #define maxn 300005 #define ...
- UVALive - 7042 The Problem to Make You Happy 博弈
题目大意:给你一个有向图, Bob 和 Alice 在做游戏,每轮他们走一步,当Bob 和 Alice在同一个点或者 Bob无路可走,Bob输,否则Alice输. 思路:因为在Bob赢的时候存在有环的 ...
- css自媒体查询
准备工作1:设置Meta标签 首先我们在使用Media的时候需要先设置下面这段代码,来兼容移动设备的展示效果: <meta name="viewport" content=& ...
- 0821Servlet基础
什么是servlet jsp页面的前身是servlet, 但是servlet和jsp是两个不同概念 servlet是运行在服务器端的一段程序, 是可以直接运行一段java后台代码 ...
- 日期(date)运用座谈会
前言 最近老被小伙伴戳:其实这些都是些很基础的东西,一边完善工具类:一边整理一下关于date的常见运用:以后使用起来就不需要到处去找了:争取做到想要的这儿都有. 正文 private static f ...
- Spring的安全机制
Spring Security:它提供全面的安全性解决方案,同时在Web请求和方法调用处理身份确认和授权,利用依赖注入和aop技术.主要名词: 1,安全拦截器:相当应用的一把锁,能够阻止对应用程序中保 ...
- [Codeforces-div.1 55D] Beautiful numbers
[Codeforces-div.1 55D] Beautiful numbers 试题分析 还是离散化...\(f_{i,j,k}\)表示i位,gcd为j,余数为k. #include<iost ...
- 【线段树】HDU1394 - Minimum Inversion Number
[题目大意] 给出0..n-1组成的一段数,可以移动前几个数到结尾.求出最小的逆序对个数. [思路] 先用线段树求出逆序对,方法和树状数组是一样的.然后对于当前第一个数num[0],在它之后比它小的数 ...
- 【OpenJudge9275】【Usaco2009 Feb】【递推】Bullcow
Bullcow 总时间限制: 12000ms 单个测试点时间限制: 1000ms 内存限制: 131072kB [描述] 一年一度的展会要来临了,农民约翰想要把N(1 <= N <= 10 ...
- [转]MySQL 数据类型(二)
MySQL 的数值数据类型可以大致划分为两个类别,一个是整数,另一个是浮点数或小数.许多不同的子类型对这些类别中的每一个都是可用的,每个子类型支持不同大小的数据,并且 MySQL 允许我们指定数值字段 ...