我正在将校园里的建筑名称与各种数据库的输入进行比较。人们输入了这些名字,每个人都使用自己的缩写方案。我正在尝试从用户输入到名称的规范形式找到最佳匹配。
我已经实现了一个递归的Levenshtein距离方法,但是我正在尝试解决一些边缘情况。 My implementation is on GitHub
一些建筑名称是一个单词,而其他建筑名称是两个。单个单词上的单个单词会产生相当准确的结果,但我需要记住两件事。
缩写:假设输入是名称的缩短版本,我有时可以在输入和任意名称之间获得相同的Levenshtein距离,以及正确的名称。
例如,如果我的输入为“Ing
”且建筑物名称 1。为["Boylan", "Ingersoll", "Whitman", "Whitehead", "Roosevelt", and "Library"]
,则Boylan
和Ingersoll
的最终结果为6 Ingersoll
。所需的结果是New Ing
。
多字符串:第二种边缘情况是输入和/或输出是两个字,用空格分隔。例如,New Ingersoll
是New
的缩写。在这种情况下,New Ingersoll和Boylan都得到了Levenshtein距离为6.如果我要分割字符串,New
完全匹配{{1}},那么我只需要参考我的解决方案前一个边缘案例。
处理这两种边缘情况的最佳方法是什么?
<子> 1。对于好奇的人来说,这些都是纽约市布鲁克林学院的建筑。
答案 0 :(得分:3)
我认为你应该使用Longest Common Subsequence的长度而不是Levenshtein距离。对您的案例而言,这似乎是一个更好的衡量标准。从本质上讲,正如我在评论中所建议的那样,它优先考虑插入和删除而不是替换。
它显然在&#34; Ing&#34;之间徘徊。 - &GT; &#34;的Ingersoll&#34;和&#34; Ing&#34; - &GT; &#34;伊兰&#34; (分数为3和1)处理空格没有问题(&#34; New Ing&#34; - &gt;&#34; New Ingersoll&#34;得分7&#34; New Ing&#34; - &gt;& #34; Boylan&#34;再次得分1),如果你遇到像&#34; Ingsl&#34;这样的缩写,也会很好地工作。
该算法很简单。如果你的两个字符串的长度为 m 和 n ,则按字符顺序比较字符串的连续前缀(从空前缀开始),将分数保持在大小 m的矩阵中+1,n + 1 。如果特定对匹配,则在前两个前缀的分数中添加一个(矩阵中的一行向上和一列);否则保持这些前缀的两个分数中的最高分(比较上面的条目和紧接着的条目并采取最佳)。当您浏览了两个字符串时,分数矩阵中的最后一个条目是LCS的长度。
&#34; Ingsll&#34;的示例得分矩阵和&#34;英格索尔&#34;:
0 1 2 3 4 5 6 I n g s l l --------------- 0 | 0 0 0 0 0 0 0 1 I | 0 1 1 1 1 1 1 2 n | 0 1 2 2 2 2 2 3 g | 0 1 2 3 3 3 3 4 e | 0 1 2 3 3 3 3 5 r | 0 1 2 3 3 3 3 6 s | 0 1 2 3 4 4 4 7 o | 0 1 2 3 4 4 4 8 l | 0 1 2 3 4 5 5 9 l | 0 1 2 3 4 5 6
这是一个长度的ObjC实现。这里的大部分复杂性仅仅是因为想要处理组合字符序列 - 例如@"o̶"
- 正确。
#import <Foundation/Foundation.h>
@interface NSString (WSSComposedLength)
- (NSUInteger)WSSComposedLength;
@end
@implementation NSString (WSSComposedLength)
- (NSUInteger)WSSComposedLength
{
__block NSUInteger length = 0;
[self enumerateSubstringsInRange:(NSRange){0, [self length]}
options:NSStringEnumerationByComposedCharacterSequences | NSStringEnumerationSubstringNotRequired
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
length++;
}];
return length;
}
@end
@interface NSString (WSSLongestCommonSubsequence)
- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target;
- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target;
@end
@implementation NSString (WSSLongestCommonSubsequence)
- (NSUInteger)WSSLengthOfLongestCommonSubsequenceWithString:(NSString *)target
{
NSUInteger * const * scores;
scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes];
return scores[[target WSSComposedLength]][[self WSSComposedLength]];
}
- (NSString *)WSSLongestCommonSubsequenceWithString:(NSString *)target
{
NSUInteger * const * scores;
scores = [[self scoreMatrixForLongestCommonSubsequenceWithString:target] bytes];
//FIXME: Implement this.
return nil;
}
- (NSData *)scoreMatrixForLongestCommonSubsequenceWithString:(NSString *)target{
NSUInteger selfLength = [self WSSComposedLength];
NSUInteger targetLength = [target WSSComposedLength];
NSMutableData * scoresData = [NSMutableData dataWithLength:(targetLength + 1) * sizeof(NSUInteger *)];
NSUInteger ** scores = [scoresData mutableBytes];
for( NSUInteger i = 0; i <= targetLength; i++ ){
scores[i] = [[NSMutableData dataWithLength:(selfLength + 1) * sizeof(NSUInteger)] mutableBytes];
}
/* Ranges in the enumeration Block are the same measure as
* -[NSString length] -- i.e., 16-bit code units -- as opposed to
* _composed_ length, which counts code points. Thus:
*
* Enumeration will miss the last character if composed length is used
* as the range and there's a substring range with length greater than one.
*/
NSRange selfFullRange = (NSRange){0, [self length]};
NSRange targetFullRange = (NSRange){0, [target length]};
/* Have to keep track of these indexes by hand, rather than using the
* Block's substringRange.location because, e.g., @"o̶", will have
* range {x, 2}, and the next substring will be {x+2, l}.
*/
__block NSUInteger col = 0;
__block NSUInteger row = 0;
[target enumerateSubstringsInRange:targetFullRange
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString * targetSubstring,
NSRange targetSubstringRange,
NSRange _, BOOL * _0)
{
row++;
col = 0;
[self enumerateSubstringsInRange:selfFullRange
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString * selfSubstring,
NSRange selfSubstringRange,
NSRange _, BOOL * _0)
{
col++;
NSUInteger newScore;
if( [selfSubstring isEqualToString:targetSubstring] ){
newScore = 1 + scores[row - 1][col - 1];
}
else {
NSUInteger upperScore = scores[row - 1][col];
NSUInteger leftScore = scores[row][col - 1];
newScore = MAX(upperScore, leftScore);
}
scores[row][col] = newScore;
}];
}];
return scoresData;
}
@end
int main(int argc, const char * argv[])
{
@autoreleasepool {
NSArray * testItems = @[@{@"source" : @"Ingso̶ll",
@"targets": @[
@{@"string" : @"Ingersoll",
@"score" : @6,
@"sequence" : @"Ingsll"},
@{@"string" : @"Boylan",
@"score" : @1,
@"sequence" : @"n"},
@{@"string" : @"New Ingersoll",
@"score" : @6,
@"sequence" : @"Ingsll"}]},
@{@"source" : @"Ing",
@"targets": @[
@{@"string" : @"Ingersoll",
@"score" : @3,
@"sequence" : @"Ing"},
@{@"string" : @"Boylan",
@"score" : @1,
@"sequence" : @"n"},
@{@"string" : @"New Ingersoll",
@"score" : @3,
@"sequence" : @"Ing"}]},
@{@"source" : @"New Ing",
@"targets": @[
@{@"string" : @"Ingersoll",
@"score" : @3,
@"sequence" : @"Ing"},
@{@"string" : @"Boylan",
@"score" : @1,
@"sequence" : @"n"},
@{@"string" : @"New Ingersoll",
@"score" : @7,
@"sequence" : @"New Ing"}]}];
for( NSDictionary * item in testItems ){
NSString * source = item[@"source"];
for( NSDictionary * target in item[@"targets"] ){
NSString * targetString = target[@"string"];
NSCAssert([target[@"score"] integerValue] ==
[source WSSLengthOfLongestCommonSubsequenceWithString:targetString],
@"");
// NSCAssert([target[@"sequence"] isEqualToString:
// [source longestCommonSubsequenceWithString:targetString]],
// @"");
}
}
}
return 0;
}
答案 1 :(得分:2)
我认为Levenshtein距离仅在您处理几乎相似的单词(如偶然拼写错误)时才有用。如果Levenshtein距离比单词本身长,则它与相似值没有任何有价值的含义。 (在你的例子中,&#34; Ing&#34;和#34; Boylan&#34;没有任何共同之处;没有人会混淆这些词。来自&#34; Ing&#34对于&#34; Boylan&#34;,你需要进行六次编辑,是单词有两个字母的两倍。)我甚至不会考虑具有明显不同长度的单词之间的Levenshtein距离,例如&#34; Ing& #34;和&#34;英格索尔&#34;并宣布它们不同。
相反,我会检查缩写模式下比原文短的单词。要检查单词是否是较长单词的缩写,您可以检查缩写的所有字母是否以相同的顺序出现在原始单词中。您还应该强制使用相同的字母开头。但是,该方法并不能解释错误的缩写。
我认为多字符串字符串更好地解析。你需要区分Ingersoll和New Ingersoll吗?在这种情况下,您可以建立一个评分系统,其中单词匹配得分为100,可能减去Levenshtein距离的十倍。不匹配有负分,比如-100。然后你评估每个单词的得分并除以建筑物中的单词数量:
如果您的字符串是&#34; Ingersoll&#34;:
100 / 1 == 100
100 / 2 == 50
如果您的字符串是&#34; New Ingersoll&#34;:
(100 - 100) / 1 == 100
(100 + 100) / 2 == 100
当您的缩写包含来自不同单词的字母时,单词方法会失败,例如: &#34; NI&#34;或&#34; NIng&#34;对于New Ingersoll,如果你不能在单词到单词匹配中找到匹配项,那么你可以在整个建筑物名称上尝试上面的缩写匹配。
(我意识到所有这些都不是一个真正的答案,而是一堆松散的想法。)