从字符串生成子字符串的组合

Posted

技术标签:

【中文标题】从字符串生成子字符串的组合【英文标题】:Generating combinations of substrings from a string 【发布时间】:2011-12-02 05:59:37 【问题描述】:

我正在尝试为给定单词生成所有可能的音节组合。识别什么是音节的过程在这里无关紧要,但它是所有组合的生成给我一个问题。我认为这可能可以在我认为的几行中递归地完成(尽管任何其他方式都可以),但我无法让它工作。有人可以帮忙吗?

    // how to test a syllable, just for the purpose of this example
    bool IsSyllable(string possibleSyllable) 
    
        return Regex.IsMatch(possibleSyllable, "^(mis|und|un|der|er|stand)$");
    

    List<string> BreakIntoSyllables(string word)
    
       // the code here is what I'm trying to write 
       // if 'word' is "misunderstand" , I'd like this to return
       //  => "mis","und","er","stand", "mis","un","der","stand"
       // and for any other combinations to be not included
    

【问题讨论】:

您应该使用 Trie 对您的音节进行编目。或者你可以使用天真的解决方案:-) 【参考方案1】:

通常使用Tries 解决此类问题。我将基于How to create a trie in c# 实现 Trie(但请注意我已经重写了它)。

var trie = new Trie(new[]  "un", "que", "stio", "na", "ble", "qu", "es", "ti", "onable", "o", "nable" );
//var trie = new Trie(new[]  "u", "n", "q", "u", "e", "s", "t", "i", "o", "n", "a", "b", "l", "e", "un", "qu", "es", "ti", "on", "ab", "le", "nq", "ue", "st", "io", "na", "bl", "unq", "ues", "tio", "nab", "nqu", "est", "ion", "abl", "que", "stio", "nab" );

var word = "unquestionable";

var parts = new List<List<string>>();

Split(word, 0, trie, trie.Root, new List<string>(), parts);

//

public static void Split(string word, int index, Trie trie, TrieNode node, List<string> currentParts, List<List<string>> parts)
   
    // Found a syllable. We have to split: one way we take that syllable and continue from it (and it's done in this if).
    // Another way we ignore this possible syllable and we continue searching for a longer word (done after the if)
    if (node.IsTerminal)
    
        // Add the syllable to the current list of syllables
        currentParts.Add(node.Word);

        // "covered" the word with syllables
        if (index == word.Length)
        
            // Here we make a copy of the parts of the word. This because the currentParts list is a "working" list and is modified every time.
            parts.Add(new List<string>(currentParts));
        
        else
        
            // There are remaining letters in the word. We restart the scan for more syllables, restarting from the root.
            Split(word, index, trie, trie.Root, currentParts, parts);
        

        // Remove the syllable from the current list of syllables
        currentParts.RemoveAt(currentParts.Count - 1);
    

    // We have covered all the word with letters. No more work to do in this subiteration
    if (index == word.Length)
    
        return;
    

    // Here we try to find the edge corresponding to the current character

    TrieNode nextNode;

    if (!node.Edges.TryGetValue(word[index], out nextNode))
    
        return;
    

    Split(word, index + 1, trie, nextNode, currentParts, parts);


public class Trie

    public readonly TrieNode Root = new TrieNode();

    public Trie()
    
    

    public Trie(IEnumerable<string> words)
    
        this.AddRange(words);
    

    public void Add(string word)
    
        var currentNode = this.Root;

        foreach (char ch in word)
        
            TrieNode nextNode;

            if (!currentNode.Edges.TryGetValue(ch, out nextNode))
            
                nextNode = new TrieNode();
                currentNode.Edges[ch] = nextNode;
            

            currentNode = nextNode;
        

        currentNode.Word = word;
    

    public void AddRange(IEnumerable<string> words)
    
        foreach (var word in words)
        
            this.Add(word);
        
    


public class TrieNode

    public readonly Dictionary<char, TrieNode> Edges = new Dictionary<char, TrieNode>();
    public string Word  get; set; 

    public bool IsTerminal
    
        get
        
            return this.Word != null;
        
    

word 是您感兴趣的字符串,parts 将包含可能音节的列表(将其设为List&lt;string[]&gt; 可能更正确,但它很容易做到。相反的parts.Add(new List&lt;string&gt;(currentParts)); 写入parts.Add(currentParts.ToArray()); 并将所有List&lt;List&lt;string&gt;&gt; 更改为List&lt;string[]&gt;

我将添加一个比他更快的 Enigmativity 响应变体,因为它会立即丢弃错误的音节,而不是稍后对其进行后过滤。如果你喜欢它,你应该给他+1,因为没有他的想法,这个变种是不可能的。但请注意,它仍然是一个 hack。 “正确”的解决方案是使用 Trie(s) :-)

Func<string, bool> isSyllable = t => Regex.IsMatch(t, "^(un|que|stio|na|ble|qu|es|ti|onable|o|nable)$");

Func<string, IEnumerable<string[]>> splitter = null;
splitter =
    t =>
        (
        from n in Enumerable.Range(1, t.Length - 1)
        let s = t.Substring(0, n)
        where isSyllable(s)
        let e = t.Substring(n)
        let f = splitter(e)
        from g in f
        select (new[]  s ).Concat(g).ToArray()
        )
        .Concat(isSyllable(t) ? new[]  new string[]  t   : new string[0][]);

var parts = splitter(word).ToList();

解释:

        from n in Enumerable.Range(1, t.Length - 1)
        let s = t.Substring(0, n)
        where isSyllable(s)

我们计算一个单词的所有可能音节,从长度 1 到单词的长度 - 1 并检查它是否是一个音节。我们直接剔除非音节。后面会检查作为音节的完整单词。

        let e = t.Substring(n)
        let f = splitter(e)

我们搜索字符串剩余部分的音节

        from g in f
        select (new[]  s ).Concat(g).ToArray()

我们将找到的音节与“当前”音节链接起来。请注意,我们正在创建许多无用的数组。如果我们接受 IEnumerable&lt;IEnumerable&lt;string&gt;&gt; 作为结果,我们可以删除这个 ToArray

(我们可以一起重写很多行,删除很多let,比如

        from g in splitter(t.Substring(n))
        select (new[]  s ).Concat(g).ToArray()

但为了清楚起见,我们不会这样做)

我们将“当前”音节与找到的音节连接起来。

        .Concat(isSyllable(t) ? new[]  new string[]  t   : new string[0][]);

在这里,我们可以稍微重建查询,以便不使用这个 Concat 并创建空数组,但这会有点复杂(我们可以将整个 lambda 函数重写为 isSyllable(t) ? new[] new string[] t .Concat(oldLambdaFunction) : oldLambdaFunction

最后,如果整个单词是一个音节,我们将整个单词添加为一个音节。否则我们Concat一个空数组(所以没有Concat

【讨论】:

【参考方案2】:

尝试从这个开始:

var word = "misunderstand";

Func<string, bool> isSyllable =
    t => Regex.IsMatch(t, "^(mis|und|un|der|er|stand)$");

var query =
    from i in Enumerable.Range(0, word.Length)
    from l in Enumerable.Range(1, word.Length - i)
    let part = word.Substring(i, l)
    where isSyllable(part)
    select part;

这会返回:

这至少有助于开始吗?


编辑:我对这个问题进行了更深入的思考,并提出了以下几个问题:

Func<string, IEnumerable<string[]>> splitter = null;
splitter =
    t =>
        from n in Enumerable.Range(1, t.Length - 1)
        let s = t.Substring(0, n)
        let e = t.Substring(n)
        from g in (new []  new []  e  ).Concat(splitter(e))
        select (new []  s ).Concat(g).ToArray();

var query =
    from split in (new []  new []  word  ).Concat(splitter(word))
    where split.All(part => isSyllable(part))
    select split;

现在query 回复这个:

如果现在确定了,请告诉我。

【讨论】:

谢谢,这看起来很有希望,但目前它给了我一个编译错误 - “'System.Collections.Generic.IEnumerable' 不包含 'StartWith' 的定义并且没有扩展名方法‘StartWith’”。 @mikel - 啊,对不起。我使用了 Reactive Framework 扩展方法。当我回到我的电脑上时,我会修复它。 @mikel - 我已将查询更改为使用标准运算符。【参考方案3】:

老实说,您可能在缩放此问题时遇到问题,我不确定您的数据集有多大,但解决方案基于简单的“这是一个音节吗?”您需要为每个单词调用大约 0(n*n) 的“音节检测”例程,其中 n = 单词中的字符数(如果这没有意义,则意味着它对于大型数据集可能会很慢!) .这没有考虑检测算法的可扩展性,当您添加更多音节时,检测算法也可能会减慢。 . 我知道您已经说过您识别什么是音节或不是音节的过程不相关,但可以说您可以更改它以使其更像自动补全,即在音节的开头传递和让它告诉你从这一点开始所有可能的音节将更具可扩展性。如果性能失控,请查看将其替换为 trie。

【讨论】:

以上是关于从字符串生成子字符串的组合的主要内容,如果未能解决你的问题,请参考以下文章

Python - 从大量组合中构建满足某些标准的子列表

从列表创建组合,如果子字符串到分隔符字符位于列表项的 1 个以上的子元素中,则从列表中删除

Hive - 从字符串中删除子字符串

从右侧获取子字符串

查找加起来为给定字符串的所有子字符串组合

从 powershell 中的 JSON 文件中提取子字符串