1.引言
前两天是著名的 520 日,尽管在法定上,今天并没有特殊之处,但是对于广大情侣来说,520有着非凡的意义。通常来说,脱单的最直接方式就是表白,一个好的表白可以帮助我们迅速脱单,因此表白墙等社交平台也迅速发展,成为人们表白的不二之选。
据不准确统计,中国90%以上的高校都有官方或民间组织的表白墙平台,下图为上财表白墙的一角,每天可以公示同学们投稿的表白内容。表白墙对我们脱单也有很大的影响。一方面,我们可以在表白墙上高调表白心仪的小哥哥或小姐姐,另一方面,我们也可以时刻关注我们的目标是否被其他人表白,从而推断自己潜在的“情敌”,达到知己知彼的境界。
通常来说,我们往往在表白墙上只会关注一个名字,因此每天保持看表白墙的习惯,将会对脱单有很大的帮助。但是在以下几种情况下,我们可能要在表白墙上同时关注多个名字,可能的情况包括但不限于:
1. 我除了关心心仪的对象是否被表白,我也关心自己是否被表白
2. 我很渣,我同时关心多个心仪的对象是否被表白
3. 我很八卦,我除了关系与自己有关的人,还关注我的哥们/姐妹们是否被表白
4. 我比上面还八卦,我还关心还关注我的哥们/姐妹们,已经哥们的女朋友和姐妹的男朋友是否被表白
在这种情况下,我们的注意力可能是有限的,不能同时关注所有关心的人,因此在今天,雷帅实验室正式发布表白墙分析工具,让你不会错过任何一个在意的人!
2.相关问题描述及技术介绍
因此,我们的问题可以描述为,给定一段文本内容(表白墙内容)和一组关注的对象名字,匹配文本内容中是否包含所给对象名字列表中的一个或多个,或者一个也没有。此外,考虑到许多表白内容比较含蓄,并不包含直接的姓名,而是用拼音缩写影射,我们还需要将拼音影射到姓名上,提供合理的显示。
我们选用的算法是AC自动机算法,这是一种用于多模式匹配的算法,和KMP算法的思想有异曲同工之妙。一般来说,要理解AC自动机,先要理解KMP算法和Trie树,然而根据我的实践,KMP算法忘了的话,也问题不大。
今天时间有限,来不及过多介绍AC自动机的相关原理,代码可以从我的连接中直接获取。值得一提的是,其实我并不懂什么是自动机,虽然我很早就听说了自动机的概念,那是我刚上大学,还是个编程菜鸟的时候,跟一个国内顶尖高校类似专业的中学同学交流,偶然听说的。她说当时正在学自动机,我又不太好意思说没听过,上网百度一番,勉强还能继续这个话题。其实人还是谦虚点好。
3.系统实现
因此我们基于了web开发来实现上述功能。
首先我们收集当天的上财表白墙内容,这件事并不容易,因为微信公众号文章很难爬取。我们选用了另一种策略,即半手工录入,所谓半手工,是指管理员需要填写当天表白墙文章链接,有了这个链接,我们就可以爬取当天的所有表白内容。但是如果长期运营,就需要管理员每天提供表白墙链接,也很麻烦。
第二步,我们在页面上通过js实现AC自动机,来结合上一步爬取到的表白墙内容进行匹配标记,运算可以在前端完成,减轻服务器压力。
第三步,优化用户体验。我们允许用户输入感兴趣的一组人名,并将其保存在COOKIE中,用户重新加载页面时,不需要重新输入。
4.效果展示
为了展示效果,我们不得不化用两个刚好能匹配上的名字来展示。例如我们用“梁振英”和“王六学”两个名字来匹配昨天的表白墙。如图,经过匹配后的姓名或拼音缩写就会被标记为下拉菜单的样式,下拉可以看到所有可能匹配的名字。
为了节目效果,我也发了一条感谢耿哥的表白墙,来检测分析效果。
也没有问题!
5.系统连接
系统连接为
http://sufeguide.fgb2019.top/ac
6.郑重声明
1. 本系统中未经用户允许,我们绝不会擅自收集用户信息,所有信息将保存在用户浏览器的COOKIE上,除非用户同意,我们方可收集信息。
2. 目前为止账号除了接收极少量个人打赏,没有任何官方或民间组织资助。
7.尾声
有空了可以分享一下AC自动机的原理,不过今天就算了。在网上看到大佬用C写的AC自动机代码,美的一塌糊涂,分享一下
#include <queue>
#include <cstdlib>
#include <cmath>
#include <cstdio>
#include <string>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int maxn = 2*1e6+9;
int trie[maxn][26]; //字典树
int cntword[maxn]; //记录该单词出现次数
int fail[maxn]; //失败时的回溯指针
int cnt = 0;
void insertWords(string s){
int root = 0;
for(int i=0;i<s.size();i++){
int next = s[i] - 'a';
if(!trie[root][next])
trie[root][next] = ++cnt;
root = trie[root][next];
}
cntword[root]++; //当前节点单词数+1
}
void getFail(){
queue <int>q;
for(int i=0;i<26;i++){ //将第二层所有出现了的字母扔进队列
if(trie[0][i]){
fail[trie[0][i]] = 0;
q.push(trie[0][i]);
}
}
//fail[now] ->当前节点now的失败指针指向的地方
////tire[now][i] -> 下一个字母为i+'a'的节点的下标为tire[now][i]
while(!q.empty()){
int now = q.front();
q.pop();
for(int i=0;i<26;i++){ //查询26个字母
if(trie[now][i]){
//如果有这个子节点为字母i+'a',则
//让这个节点的失败指针指向(((他父亲节点)的失败指针所指向的那个节点)的下一个节点)
//有点绕,为了方便理解特意加了括号
fail[trie[now][i]] = trie[fail[now]][i];
q.push(trie[now][i]);
}
else//否则就让当前节点的这个子节点
//指向当前节点fail指针的这个子节点
trie[now][i] = trie[fail[now]][i];
}
}
}
int query(string s){
int now = 0,ans = 0;
for(int i=0;i<s.size();i++){ //遍历文本串
now = trie[now][s[i]-'a']; //从s[i]点开始寻找
for(int j=now;j && cntword[j]!=-1;j=fail[j]){
//一直向下寻找,直到匹配失败(失败指针指向根或者当前节点已找过).
ans += cntword[j];
cntword[j] = -1; //将遍历国后的节点标记,防止重复计算
}
}
return ans;
}
int main() {
int n;
string s;
cin >> n;
for(int i=0;i<n;i++){
cin >> s ;
insertWords(s);
}
fail[0] = 0;
getFail();
cin >> s ;
cout << query(s) << endl;
return 0;
}
转自 https://blog.csdn.net/bestsort/article/details/82947639
侵删
祝愿大家520快乐,有情人终成眷属!
欢迎关注我的公众号,我会在那里第一时间分享我制作的废品软件。