题目说明:
这是当时面试金山时,不小心面进了人工智能AI部门,给面到的一道NLP题。 切分子词。现给输入为一个字典(dict_mini.txt),字典的每一行由词、频率组成。要求把词切分成子词组合。输出也为一个文件(word2subword.txt),包含字典中可以切分的词和子词的结果。
切分子词说明
因为面试的时候提出了一个较为奇怪的思路,先解释一下那个思路吧:
- 先预处理所给出的词表,再对所给的词进行解析。
预处理就是将所有可再分解的词进行去重,以免出现有词可以再分例如“球协会”可以再分成“球”,“协会”
但是这样会存在,如果词表中不存在“道来”这个词,所以预处理时又需要去枚举每一段两个字以上的区间来根据频率将其再次丢入词表中。
- 再对所给的词进行一步步的匹配,从前往后用hash值在词表中搜索,保证切割出来的字是最小单元。
当时给卡住了思想,因为“道来”和“球协会”是一样的概念,如果刻意加上频率,可能会导致“球协会”无法再被切分。
其实后来想想,只需要将第一个步骤拆成两部,然后先执行拆词,再进行去重。
但是往这个方面想的时候,遇到了两个最主要的问题。
- 去重时,应该如何匹配呢?我想过将其按照长度排序从长的开始去重,如果内被完整匹配到即为需要被去掉的。
但是这种方式貌似是不可行的,因为按照我对字符串匹配的了解,我需要对每一个需要去重的都做一次新的AC自动机,而在时间复杂度上非常不可行。
- 切割的时候难道不会造成,永远不存在长度超过2的词吗?比如“对不起”这个词,就是无法切割的,那我应该怎么处理呢?
所以在这两个问题中挣扎了好一会之后,感觉自己方向走错了。
因为之前没有看过机器学习,自然语言等算法,自己又对字符匹配是相对比较陌生的,曾今想过对其建边,但是由于考虑到词的整体性,发现并建不起来。。
在各大搜索引擎上搜索的时候,偶然翻到了一篇博客: blog.csdn.net/thealgorith…
讲述的就是这个解决方法。个人去大概浏览了一下,它给予了我思维扩展上很大的帮助
因为处理的情况相对于正常的词语分析并没有那么难。
所以只需要对所需要解析的词,进行词首对尾部的建边,权值给予为负的频率,对于原词,两两汉字之间建立权值为1的边,然后跑一次dijkstra即可。
但是在我自己编写代码的过程中遇到了如下的问题:
- 中文字符的编码在读入时是最头疼的事情,因为他是以两位char类型作为一个中文汉字,而我单独把他们拿出来当string,又会导致乱码。
所以最终我用hashcode,直接对读入进来的string进行编码,然后存储到map中,查找的时候也是通过获取到该字词的hash进行查找。
- 跑最短路的时候,我试图直接在跑图的过程中进行输出答案,但是这样是不可行的,就比如“乒乓球”和“乒乓”这个在建边的时候,“乓球”之间的权值为1,但是在跑最短路的时候,他会先跑“乒乓”再跑“乒-球”,而当“球”字已经从“乒”字跑到时,并不是我们想要输出的答案。因为实际上“乒乓” - “乓球”这条路径才是最优的。
所以我的解决方法是记录每一个节点上一次的来源,然后跑完最短路后反向输出,即找从0出发到最后一个汉字的最短路径。 - 如何处理单个汉字?其实单个汉字都有个特点,他们都是从权值为1的边跑到的,因为并没有其他的点能与他们组成词(即没有走快捷通道),这样就能完美解决单个汉字的问题了,而对于“道来”,其实他们是单个“道”和“来”的汉字,我在输出时特殊处理了,如果连续是单个字,我就不将他们切割了,因为说不定他们还是一个词呢?所以对于这种词,如果要特殊处理,只能人为去给他们加频率,或者是在词表中进行其他方式的处理。
- 因为当时面试时,强调要切成最小单元,但是每个字的频率也在上面出现了,如果直接建边切可能就成单个字了。所以我在代码中没有去给单个字建边,以免出问题,因为那就不叫词啦!
- 词表中所给的“乒乓”出现的频率竟然比“乒乓球”出现的频率要低,所以如果按照原数据,我是无法将“乒乓球”切割成“乒乓”,“球”的,自己测试的时候将数据改了。
其实如果需要解决方法的话,也是有的,对于存入时,他所给的频率按照字符串长度乘上一定量的权值,使得两个词的权值远大于三个词的权值....这样之类的操作,就可以避免频率高时但是又要切割成最小单元了。
/*
@author: QuanQqqqq
@algorithm: dijkstra
@date: 2018-04-25
@resource: wps
*/
#include <bits/stdc++.h>
#define pii pair<int, int>
#define MAXN 100005
#define INF 0x3f3f3f3f
#define mp(a,b) make_pair(a,b)
#define ll long long
#define MOD 1000000007
#define HASH 1001
using namespace std;
map<ll, int> mapp;
int dis[MAXN];
vector<pair<ll, int> > g[MAXN];
string chinese;
bool vis[MAXN];
pair<bool, int> fg[MAXN];
void print(int st, int en) {
for (int i = st; i < en; i++) {
printf("%c", chinese[i]);
}
}
// dijkstra反向记录边
void dijkstra(int st) {
// pair 距离,汇点,源点,是否是单个字
priority_queue<pair<ll, pair<pii, int> >, vector<pair<ll, pair<pii, int> > >, greater<pair<ll, pair<pii, int> > > > q;
q.push(mp(-INF, mp(mp(st, st), 0)));
memset(dis, INF, sizeof(dis));
dis[st] = 0;
while (!q.empty()) {
int u = q.top().second.first.first;
int from = q.top().second.first.second;
ll wt = q.top().first;
bool flag = q.top().second.second;
q.pop();
// 反向记录边
if (fg[u].second == 0) {
fg[u] = mp(flag, from);
}
for (int i = 0; i < (int) g[u].size(); i++) {
int v = g[u][i].second;
ll w = g[u][i].first;
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.push(mp(dis[v], mp(mp(v, u), w == 1)));
}
}
}
}
void addEdge(int u, int v, ll w) {
g[u].push_back(mp(w, v));
}
// hash负数取正
ll update(ll a) {
while (a < 0) {
a += MOD;
}
return a;
}
ll get(string s, int st, int en) {
ll ans = 0;
// 这里构建一个hash码
for (int i = st; i < en; i++) {
ans = ans * HASH + update(s[i]);
ans %= MOD;
}
return ans;
}
// 读入中文对应的频率
void init() {
cout << "请输入中文与其对应的频率,输入#结束" << endl;
string name;
int val;
while (cin >> name) {
if (name == "#") {
break;
}
int len = name.size();
cin >> val;
mapp[get(name, 0, len)] = val;
}
}
pair<bool, pii> ans[MAXN];
// 输出
void output(int en) {
int len = 0;
while (en >= 0) {
if (fg[en].first) {
ans[len++] = mp(true, mp(en, en + 2));
en = fg[en].second;
} else {
ans[len++] = mp(false, mp(fg[en].second, en + 2));
en = fg[en].second - 2;
}
}
for (int i = len - 1; i >= 0; i--) {
print(ans[i].second.first, ans[i].second.second);
if (i == 0) {
cout << ";";
} else if (i - 1 < 0 || !ans[i - 1].first || !ans[i].first) {
cout << ",";
}
}
cout << endl;
}
void initEdge() {
for (int i = 0; i < MAXN; i++) {
g[i].clear();
}
}
int main(int argc, char *agrv[]) {
init();
cout << "开始匹配" << endl;
while (cin >> chinese) {
initEdge();
int len = chinese.size();
// 加边
for (int i = 0; i < len; i += 2) {
for (int j = i + 2; j < len; j += 2) {
if (mapp[get(chinese, i, j + 2)]) {
addEdge(i, j, -mapp[get(chinese, i, j + 2)]);
}
}
}
// 加单个字的边
for (int i = 0; i < len - 2; i += 2) {
addEdge(i, i + 2, 1);
}
memset(fg, 0, sizeof(fg));
dijkstra(0);
output(len - 2);
}
}
最终本人用了接近3个小时,学习了该算法并完成了代码实现,通过了wps的二面,今天hr小姐姐对我进行了最后一面,不知道结果如何呢=-=