之前看到一个非常有意思的理论——六度分离理论,说的是你和任何一个陌生人之间所间隔的人不会超过五个,也就是说,最多通过五个人你就能够认识任何一个陌生人。
那么有意思的就来了,我们把每个词语都想象成单独的一个人,词与词之间如果能接龙就相当于他们认识,那这样是不是说词语与词语之间也可以通过最多五个词就可以接龙得到呢?于是想用Dijkstra算法来实现一下,看看词语是否满足六度分离理论。
既然要从词语到词语找最短路,那必然要先建图,建图就需要数据。这次数据是从汉辞网获取的。
数据获取的程序如下:
import requests, re, time, random headers = { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) ''Chrome/51.0.2704.6' '3 Safari/537.36' } def getPage(url): html = requests.get(url) html.encoding = 'gbk' text = html.text pattern = re.compile('>(.*?)</a>】:') res = pattern.findall(text) return res def solve(): start = 6250 end = 9644 url_format = 'http://www.hydcd.com/cd/fenlei/f%04d-%03d.htm' pages = list(range(start, end + 1)) random.shuffle(pages) size = end - start + 1 words = [] cnt = 0 for i in range(size): page = 1 while (True): url = url_format % (i + start, page) page = page + 1 res = getPage(url) if len(res) == 0: break for word in res: words.append(word) print(i, res) cnt = cnt + 1 if cnt > 30 or i == size - 1: with open('%d.txt' % (i + start), 'w') as f: for word in words: f.write(word + '\n') cnt = 0 words = [] if __name__=='__main__': solve()
其中header大家可以自行修改,在solve函数中有变量分别是start和end,是说从第start页爬到第end页。这样做是因为,为了防止被反爬我给程序加了sleep,这也导致爬取的速度变慢,如果一下爬非常多页那么一旦中间出现任何问题就前功尽弃了。
但是也不要太担心,这里我设置了最多每爬取30页就保存一次,做了双重保险,但是还是建议一次不要爬取太多,可能会被检测出来。
爬取的结果大概是这样的:
吖吖 吖啶 吖噗鸡 叫吖吖 闹吖吖 叫天吖地 阿Q 阿Q正传 阿阿 阿八 阿巴拉契亚山脉 阿爸 阿傍 阿谤 阿保 阿保之功 阿保之劳 阿本郎 阿鼻 阿鼻地狱
之后将存储数据的文件名存储在一个文本中方便处理。使用以下程序将数据进行整合:
#include <iostream> #include <cstdio> #include <vector> #include <string> #include <algorithm> #include <fstream> std::string str; std::vector<std::string> fileList; int main() { std::ifstream in("list.txt"); std::ofstream out("wordList.txt"); while (in >> str) { fileList.push_back(str); } int tot = 0; std::sort(fileList.begin(), fileList.end()); for (int i = 0; i < fileList.size(); i++) { std::string fileName = fileList[i]; std::ifstream inn(fileName.c_str()); while (inn >> str) { out << str << std::endl; tot++; } } std::cout << tot << std::endl; return 0; }
整合之后所有的词语都被整合到一个叫做wordList.txt的文件中了。
为了加速以及简化流程,代码对汉字字符串进行了哈希。
图的存储方式采用临接表,用map进行存储。
代码最上面有一个filePath的变量,只需要把这个变量改成wordList.txt文件的路径就可以了。
程序运行后它需要一段时间读取数据、哈希然后建图,这个过程可能需要十秒钟左右。
#include <iostream> #include <algorithm> #include <queue> #include <string> #include <vector> #include <fstream> #include <map> #include <set> typedef unsigned long long ull; const ull END = 0x3f3f3f3f; const std::string filePath = ""; struct Node { ull hash; std::string str; Node(){} Node(ull hash, std::string str):hash(hash), str(str){} bool operator < (const Node &x) const { return hash < x.hash; } bool operator == (const Node &x) const { return hash == x.hash; } }; ull getHash(std::string str) { ull hash = 0, seed = 13331; int size = str.size(); for (int i = 0; i < size; i++) { hash = hash * seed + str[i]; } return hash; } bool strIsValid(std::string &str) { int size = str.size(); if (size % 3 != 0) return false; // if (size > 9) return false; for (int i = 0; i < size; i++) { if (str[i] >= 0) return false; } return true; } std::vector<ull> getChineseStrHashList(std::string &str) { std::vector<ull> res; if (!strIsValid(str)) return res; int size = str.size(); for (int i = 0; i < size; i += 3) { std::string tmp(str.begin() + i, str.begin() + i + 3); res.push_back(getHash(tmp)); } return res; } std::vector<std::string> wordList; std::string str; std::map<ull, std::vector<ull> > edges; std::map<ull, std::string> hashToStr; std::set<ull> vis; std::map<ull, ull> father; std::vector<ull> charHash; std::map<ull, ull> charToStr; void solve() { std::ifstream in(filePath); while (in >> str) { wordList.push_back(str); } int tot = 0; for (int i = 0; i < wordList.size(); i++) { std::string &s = wordList[i]; if (!strIsValid(s)) continue; ull hash = getHash(s); charHash = getChineseStrHashList(s); hashToStr[hash] = s; edges[charHash.front()].push_back(hash); tot++; } std::cout << "total: " << tot << ", invalid total: " << wordList.size() - tot << std::endl; std::string start, end; std::cin >> start >> end; ull startHash = getHash(start); ull endHash = getHash(end); hashToStr[startHash] = start; hashToStr[endHash] = end; std::vector<ull> endCharHash = getChineseStrHashList(end); std::vector<ull> startCharHash = getChineseStrHashList(start); father[startCharHash.back()] = END; charToStr[startCharHash.back()] = startHash; std::queue<ull> q; q.push(startCharHash.back()); vis.insert(startCharHash.back()); for (; !q.empty(); q.pop()) { ull u = q.front(); if (u == endCharHash.front()) { break; } std::vector<ull> &e = edges[u]; for (int i = 0; i < e.size(); i++) { ull tarHash = e[i]; std::string &ss = hashToStr[tarHash]; std::vector<ull> strHash = getChineseStrHashList(ss); if (!vis.count(strHash.back())) { vis.insert(strHash.back()); q.push(strHash.back()); father[strHash.back()] = u; charToStr[strHash.back()] = tarHash; } } } ull cur = endCharHash.front(); std::vector<ull> path; while (cur != END) { path.push_back(charToStr[cur]); cur = father[cur]; } std::reverse(path.begin(), path.end()); for (int i = 0; i < path.size(); i++) { std::cout << hashToStr[path[i]] << " "; } std::cout << end << std::endl; std::cout << std::endl; } int main() { solve(); return 0; }
我们可以任意给他两个合法的词语,比如“高等数学”和“线性代数”:
total: 934808, invalid total: 143 高等数学 线性代数 高等数学 学业成绩报告单 单行线 线性代数
再比如“火星”和“吃饭”:
total: 934808, invalid total: 143 火星 吃饭 火星 星次 次要 要嘴吃 吃饭
可以看到可以接龙的找到,并且尝试多次确实没有用超过五个词才能接龙得到词对。因此基本认为词语之间也是满足六度分离理论的。
大家也许会发现,接龙的途中有些词语非常异类,这主要是词库的问题而不是程序的问题,大家如果愿意可以将词库中的词语人工筛选一遍,也不多只有934951个,只保留那些我们常见的,这样得到的结果就会非常正常了。