我该如何优化这种递归方法

时间:2011-02-23 00:03:04

标签: c# optimization recursion

我正在尝试制作单词益智游戏,为此我使用递归方法查找给定字母中的所有可能单词。 这些字母是4x4板。

像这样:

ABCD
EFGH
HIJK
LMNO

在此循环中调用递归方法:

for (int y = 0; y < width; y++)
        {
            for (int x = 0; x < height; x++)
            {
                myScabble.Search(letters, y, x, width, height, "", covered, t);
            }
        }

字母是字符的二维数组。

y&amp; x是显示董事会中的位置的整数

宽度&amp; height也是int,它告诉董事会的维度

“”是我们尝试制作的字符串(单词)

覆盖了一系列bool,以检查我们是否已经使用过这个方块。

t是一个列表(包含要检查的所有单词)。

需要优化的递归方法:

public void Search(char[,] letters, int y, int x, int width, int height, string build, bool[,] covered, List<aWord> tt)
    {
        // Dont get outside the bounds
        if (y >= width || y < 0 || x >= height || x < 0)
        {
            return;
        }

        // Dont deal with allrady covered squares
        if (covered[x, y])
        {
            return;
        }

        // Get Letter
        char letter = letters[x, y];

        // Append
        string pass = build + letter;

        // check if its a possibel word
        //List<aWord> t = myWords.aWord.Where(w => w.word.StartsWith(pass)).ToList();
        List<aWord> t = tt.Where(w => w.word.StartsWith(pass)).ToList();
        // check if the list is emphty
        if (t.Count < 10 && t.Count != 0)
        {
            //stop point
        }
        if (t.Count == 0)
        {
            return;
        }
        // Check if its a complete word.
        if (t[0].word == pass)
        {
            //check if its allrdy present in the _found dictinary

            if (!_found.ContainsKey(pass))
            {
                //if not add the word to the dictionary
                _found.Add(pass, true);
            }

        }
        // Check to see if there is more than 1 more that matches string pass 
        // ie. are there more words to find.
        if (t.Count > 1)
        {
            // make a copy of the covered array
            bool[,] cov = new bool[height, width];
            for (int i = 0; i < width; i++)
            {
                for (int a = 0; a < height; a++)
                {
                    cov[a, i] = covered[a, i];
                }
            }
            // Set the current square as covered.
            cov[x, y] = true;

            // Continue in all 8 directions.
            Search(letters, y + 1, x, width, height, pass, cov, t);
            Search(letters, y, x + 1, width, height, pass, cov, t);
            Search(letters, y + 1, x + 1, width, height, pass, cov, t);
            Search(letters, y - 1, x, width, height, pass, cov, t);
            Search(letters, y, x - 1, width, height, pass, cov, t);
            Search(letters, y - 1, x - 1, width, height, pass, cov, t);
            Search(letters, y - 1, x + 1, width, height, pass, cov, t);
            Search(letters, y + 1, x - 1, width, height, pass, cov, t);

        }


    }

代码的工作方式与我预期的相同,但速度非常慢..查找单词大约需要2分钟。

  编辑:我澄清了这些信件   数组是2D

我想展示一下如何让算法快速运行。 我使用@Enigmativity执行Trie,使用@EricLippert描述的搜索模式

public void SearchWord(char[,] letters, Trie parentTrie, char[] build, int x, int y, bool[,] covered )
    {
        char[] pass = new char[build.Length + 1];
        build.CopyTo(pass, 0);

        // iterate through all squares in the board.
        for (var r = 0; r < letters.GetLength(0); r++ )
        {
            for (var c = 0; c < letters.GetLength(1); c++)
            {
                //check if this square is naighbor to the last square
                if ((IsNeighbor(x, y, r, c)|| x == -1) && !(covered[r, c]))
                {
                    // check if the current Trie contains the letter
                    if (parentTrie.ContainsKey(letters[r,c]))
                    {
                        pass[build.Length] = letters[r, c];
                        covered[r, c] = true;
                        SearchWord(letters, parentTrie[letters[r, c]], pass, r, c, covered);
                        covered[r, c] = false;
                    }
                    if (parentTrie.ContainsKey('$') && (!myStrings.Contains(new string(build).ToLower())))
                        myStrings.Add(new string(build).ToLower());
                }
            }
        }
    }

最初由此调用:

SearchWord(letters, trie, new char[0], -1, -1, new bool[letters.GetLength(0), letters.GetLength(1)]);

我意识到我可以添加字母作为属性,但由于它是一个引用类型,它不是非常昂贵的时间

8 个答案:

答案 0 :(得分:17)

其他答案是正确的:你应该完全放弃这个算法并重新开始。

在文字游戏中处理这些问题的方法是将字典转换为适合您想要进行的搜索的形式。在你的情况下,你想要构建的数据结构称为 trie ,这是一个双关语,因为它是一个快速“重新执行”的“树”,哈哈哈,那些计算机科学家很诙谐!

trie的工作方式是你有一棵树,每个节点最多有27个孩子。假设你有字典{AB,ACE,ACT,CAT}。特里看起来像这样:

         root
      /        \
     A          C
    / \         |
   B   C        A
   |  / \       |
   $ E   T      T
     |   |      |
     $   $      $

其中$表示“单词已结束”。从根到$的每条路径都是合法的词。

现在找到所有单词,首先从字典中构建trie。然后对于网格中的每个起始点,尝试trie中的每个可能的单词。如果你从一个包含A的网格方块开始,那么你当然不会遍历trie的C分支。然后对于特里的A中的每个孩子,看看网格是否有一个相邻的方格,可以让你更深入到特里。

这是一个有趣的递归算法,你会发现它非常快,因为你可以非常聪明地放弃从给定方格开始不可能存在的大量单词。

这一切都有意义吗?

答案 1 :(得分:4)

在相反方向攻击它可能更有效率。迭代你可能的单词列表,找到网格中单词的第一个字母,如果你发现它在相邻的正方形中寻找第二个字母,如果你发现它继续沿着该行继续匹配。

通过这种方式,您可以快速完全消除单词,而不是反复检查每个可能位置的每个单词。

只是看看你正在使用的代码将重复数千次LINQ可能是一个禁忌。您正在使用字符串操作,因此您在内存中生成数千个字符串,并且可能导致垃圾收集器在递归期间运行,您应该切换到字符串生成器或某种字符数组。

此时您可以进行大量优化,例如快速首先在列表中检查每个单词的每个字母是否在网格中的某个位置(只需将网格中的所有字母都放入字符串中即可这很容易),然后你可以快速消除那些字母不在网格中的字样 - 甚至在担心它们是否在正确的顺序之前。

您可以将网格转换为一系列字符串,每个字符串代表网格的某个方向,然后只使用纯字符串搜索。如果你找到匹配,你只需要确保匹配不是跨越网格的边界,这可以通过快速检查匹配的起点和终点来完成。

答案 2 :(得分:4)

创建不变的参数(字母,宽度,高度和tt)字段。不要使用Linq,它在很多迭代中都很慢。此外,将您的单词列表更改为更有效的词典或某种类型的排序列表。

答案 3 :(得分:3)

我想我可能会根据Eric Lippert的回答提出代码。 Eric钉了它,但硬编码总是更好。 ; - )

首先,这是一个简单的trie实现:

public class Trie : Dictionary<char, Trie>
{
    public int Frequency { get; set; }

    public void Add(IEnumerable<char> chars)
    {
        if (chars == null) throw new System.ArgumentNullException("chars");
        if (chars.Any())
        {
            var head = chars.First();
            if (!this.ContainsKey(head))
            {
                this.Add(head, new Trie());
            }
            var tail = this.GetSafeTail(head, chars.Skip(1));
            if (tail.Any())
            {
                this[head].Add(tail);
            }
        }
    }

    public bool Contains(IEnumerable<char> chars)
    {
        if (chars == null) throw new System.ArgumentNullException("chars");
        var @return = false;
        if (chars.Any())
        {
            var head = chars.First();
            if (this.ContainsKey(head))
            {
                var tail = this.GetSafeTail(head, chars.Skip(1));
                @return = tail.Any() ? this[head].Contains(tail) : true;
            }
        }
        return @return;
    }

    private IEnumerable<char> GetSafeTail(char head, IEnumerable<char> tail)
    {
        return ((!tail.Any()) && (head != '$')) ? new [] { '$', } : tail;
    }
}

此代码允许您将字符串传递给Add&amp;作为字符串的Contains方法是IEnumerable<char>

可以像这样使用:

var trie = new Trie();
var before = trie.Contains("Hello"); // == false
trie.Add("Hello");
var after = trie.Contains("Hello"); // == true

现在,给定一个字母网格和一个加载了可能单词的特里,我可以运行以下查询来获得匹配:

var matches =
    from w in this.GetPossibleWords(letters)
    where trie.Contains(w)
    select w;

GetPossibleWords方法实现如下:

public IEnumerable<string> GetPossibleWords(char[,] letters)
{
    return
        from ws in this.GetPossibleWordLists(letters)
        from w in ws
        select w;
}

private IEnumerable<IEnumerable<string>> GetPossibleWordLists(char[,] letters)
{
    Func<int, int> inc = x => x + 1;
    Func<int, int> nop = x => x;
    Func<int, int> dec = x => x - 1;
    for (var r = letters.GetLowerBound(0);
        r <= letters.GetUpperBound(0); r++)
    {
        for (var c = letters.GetLowerBound(1);
            c <= letters.GetUpperBound(1); c++)
        {
            yield return new [] { letters[r, c].ToString(), };
            yield return this.GetPossibleWords(letters, r, c, dec, dec);
            yield return this.GetPossibleWords(letters, r, c, inc, dec);
            yield return this.GetPossibleWords(letters, r, c, nop, dec);
            yield return this.GetPossibleWords(letters, r, c, dec, nop);
            yield return this.GetPossibleWords(letters, r, c, inc, nop);
            yield return this.GetPossibleWords(letters, r, c, nop, inc);
            yield return this.GetPossibleWords(letters, r, c, dec, inc);
            yield return this.GetPossibleWords(letters, r, c, inc, inc);
        }
    }
}

private IEnumerable<string> GetPossibleWords(char[,] letters,
    int r, int c,
    Func<int, int> rd, Func<int, int> cd)
{
    var chars = new List<char>();
    while (r >= letters.GetLowerBound(0) && r <= letters.GetUpperBound(0)
        && c >= letters.GetLowerBound(1) && c <= letters.GetUpperBound(1))
    {
        chars.Add(letters[r, c]);
        if (chars.Count > 1)
        {
            yield return new string(chars.ToArray());
        }
        r = rd(r);
        c = cd(c);
    }
}

此解决方案的性能似乎相当不错。

OP有4x4网格大约需要120秒(2分钟)。我的代码可以在20.3秒内匹配40x40网格中208,560个单词的列表。

不错,嗯?

再次感谢Eric提出使用trie的想法。

答案 4 :(得分:2)

您的定向搜索会进行一次优化。您只需向下,向左和向左对角搜索,因为沿对角线向上,向右和向右搜索将获得与简单地反转字符串相同的结果。

Grid search graphic

编辑:

感兴趣的还可能是Project Euler Problem #11以及使用C#和LINQ here编写解决方案。

这是我试图描述的一个完全未经测试的例子。

    static List<string> Solve()
{
    // Declaring a empty list of strings to hold our results up front.
    List<string> words = new List<string>();

    // I'm using set as the term for your grid of letters.
    string set = @"ABCD
                   EFGH
                   HIJK
                   LMNO";

    // i'm explicitly defining the dimensions, you need to figure this out.
    int sizeX = 4;
    int sizeY = 4;

    // i'm also specifying a maximum word length. you might find a word like 
    // supercalifragilisticexpialidocious, but i doubt it so lets not waste time.
    int maximumWordSize = 3;

    // first, our trie/wordlist/etc. assume `GetWordList()` gets a list of all
    // valid words with indicated number of characters.
    List<string> wordList = GetWordList(3);

    // second, we load a character array with the set.
    char[,] data = new char[sizeX, sizeY];
    string[] lines = set.Split('\n');
    for (int i = 0; i <= lines.Count() -1; i++)
    {
        string line = lines[i].Trim();
        for (int j = 0; j <= line.Length - 1; j++)
        {
            char[j,i] = lines[j];
        }
    }

    // third, we iterate over the data
    for(int x = 0; x <= sizeX - maximumWordSize; x++)
    {
        for (int y = 0; y <= sizeY - maximumWordSize; y++)
        {
            // check to see if we even have any words starting with our cursor
            var validWords = wordList.Where(w=>w.Contains(data[x,y]));
            if (validWords.Count() > 0)
            {
                // ok, we have words. continue on our quest!
                // (this is where your initial qualifier changes if you use a trie
                // or other search method)

                char[] characters = char[maximumWordSize];

                // search left
                for (int i = x; i <= x + maximumWordSize - 1; i++)
                    characters[i] = data[i, y];
                words.AddRange(Search(characters, wordList));

                // search down
                for (int i = y + maximumWordSize - 1; i <= y; i--)
                    characters[i] = data[x, y];
                words.AddRange(Search(characters, wordList));

                // search diagonal right
                for (int i = x; i <= x + maximumWordSize - 1; i++)
                    for (int j = y + maximumWordSize - 1; j <= y; j--)
                        characters[i] = data[i, j];
                words.AddRange(Search(characters, wordList));

                // search diagonal left
                for (int i = x; i <= x - MaximumWordSize + 1; i++)
                    for (int j = y + maximumWordSize - 1; j <= y; j--)
                        characters[i] = data[i, j];
                words.AddRange(Search(characters, wordList));
            }
        }
    }

    return words;
}

static List<string> Search(char[] Input, List<string> WordList)
{
    List<string> result = new List<string>();
    string word = "";
    // find forwards
    for (int i = 0; i <= Input.Length - 1; i++)
    {
        word += Input[i];
        if (WordList.Contains(word))
            result.Add(word);
    }

    // find backwards
    Array.Reverse(Input);
    for (int i = 0; i <= Input.Length - 1; i++)
    {
        word += Input[i];
        if (WordList.Contains(word))
            result.Add(word);
    }

    return result;
}

答案 5 :(得分:2)

  1. 使用更好的数据结构。而不是使用List存储所有单词使用一些树状结构。或者至少是排序列表+二进制搜索。这一点应该会大大提高性能。我建议通过recursice方法使用有序集合和传递范围索引。即你将寻找第一个字母,并确定以该字母开头的单词有123-456个索引。下次你添加新信件时,你会使这个范围更窄,你不需要经历整个收集!
  2. 预过滤您的文字集。你只有16个字母,这意味着缺少10个以上的字母。您可以过滤单词集合并删除所有缺少数字的单词。
  3. 懒惰的LINQ 。如果你想使用Linq,那么不要强制迭代整个集合(我的意思是不要调用ToList方法)。您可以使用它的懒惰性质进行迭代,直到您需要为止,因为如果此数字超过2,您不关心有多少单词以此子词开头。
  4. 不要复制cov 。每次只需设置cov[x, y] = true;并在嵌套搜索尝试后放回false值,而不是复制整个数组:cov[x, y] = false;
  5. 使用分析器。 ; - )

答案 6 :(得分:1)

另一个优化是数据结构:

List<aWord> t = tt.Where(w => w.word.StartsWith(pass)).ToList();

这是所有有效单词的O(n),您可以通过使用即trie来提高性能。

答案 7 :(得分:0)

我不太了解C#,但是我在C ++中会这样做(有些东西是伪代码,以便更容易阅读);它应该很容易翻译:

struct word_search {
  const set<string>& words; // This is a balanced search tree
  const vector<vector<char> >& letters;
  const int width, height;
  vector<vector<bool> > marks;
  string word_so_far;

  // Add constructor
  void search(int x, int y) {
    if (x < 0 || x >= width || y < 0 || y >= height || marks[x][y]) return;
    word_so_far += letters[x][y];
    set<string>::const_iterator it = words.lower_bound(word_so_far);
    if (it == words.end() ||
        it->substr(0, word_so_far.size()) != word_so_far) {
      word_so_far.resize(word_so_far.size() - 1);
      return;
    }
    marks[x][y] = true;
    for (int dx = -1; dx <= 1; ++dx)
      for (int dy = -1; dy <= 1; ++dy)
        search(x + dx, y + dy);
    marks[x][y] = false;
    word_so_far.resize(word_so_far.size() - 1);
  }
};

在C#中,set<string>SortedSetvector<vector<...> >类型为二维数组。不过,我不确定相当于lower_bound; SortedSet似乎没有类似的东西。