【问题标题】:Getting the least amount of sub words获得最少的子词
【发布时间】:2016-07-30 23:37:24
【问题描述】:

Dávid Horváth 的解决方案适用于返回最大最小单词:

import java.util.*;

public class SubWordsFinder
{
    private Set<String> words;

    public SubWordsFinder(Set<String> words)
    {
        this.words = words;
    }

    public List<String> findSubWords(String word) throws NoSolutionFoundException
    {
        List<String> bestSolution = new ArrayList<>();
        if (word.isEmpty())
        {
            return bestSolution;
        }
        long length = word.length();
        int[] pointer = new int[]{0, 0};
        LinkedList<int[]> pointerStack = new LinkedList<>();
        LinkedList<String> currentSolution = new LinkedList<>();
        while (true)
        {
            boolean backtrack = false;
            for (int end = pointer[1] + 1; end <= length; end++)
            {
                if (end == length)
                {
                    backtrack = true;
                }
                pointer[1] = end;
                String wordToFind = word.substring(pointer[0], end);
                if (words.contains(wordToFind))
                {
                    currentSolution.add(wordToFind);
                    if (backtrack)
                    {
                        if (bestSolution.isEmpty() || (currentSolution.size() <= bestSolution.size() && getSmallestSubWordLength(currentSolution) > getSmallestSubWordLength(bestSolution)))
                        {
                            bestSolution = new ArrayList<>(currentSolution);
                        }
                        currentSolution.removeLast();
                    } else if (!bestSolution.isEmpty() && currentSolution.size() == bestSolution.size())
                    {
                        currentSolution.removeLast();
                        backtrack = true;
                    } else
                    {
                        int[] nextPointer = new int[]{end, end};
                        pointerStack.add(pointer);
                        pointer = nextPointer;
                    }
                    break;
                }
            }
            if (backtrack)
            {
                if (pointerStack.isEmpty())
                {
                    break;
                } else
                {
                    currentSolution.removeLast();
                    pointer = pointerStack.removeLast();
                }
            }
        }
        if (bestSolution.isEmpty())
        {
            throw new NoSolutionFoundException();
        } else
        {
            return bestSolution;
        }
    }

    private int getSmallestSubWordLength(List<String> words)
    {
        int length = Integer.MAX_VALUE;

        for (String word : words)
        {
            if (word.length() < length)
            {
                length = word.length();
            }
        }

        return length;
    }

    public class NoSolutionFoundException extends Exception
    {
        private static final long serialVersionUID = 1L;
    }
}

我有一个String,其中包含小写的常规英文单词。假设这个String 已经分解为所有可能子词的List

public List<String> getSubWords(String text)
{
    List<String> words = new ArrayList<>();

    for (int startingIndex = 0; startingIndex < text.length() + 1; startingIndex++)
    {
        for (int endIndex = startingIndex + 1; endIndex < text.length() + 1; endIndex++)
        {
            String subString = text.substring(startingIndex, endIndex);

            if (contains(subString))
            {
                words.add(subString);
            }
        }
    }

    Comparator<String> lengthComparator = (firstItem, secondItem) ->
    {
        if (firstItem.length() > secondItem.length())
        {
            return -1;
        }

        if (secondItem.length() > firstItem.length())
        {
            return 1;
        }

        return 0;
    };

    // Sort the list in descending String length order
    Collections.sort(words, lengthComparator);

    return words;
}

如何找到构成原始字符串的最少子词?

例如:

String text = "updatescrollbar";
List<String> leastWords = getLeastSubWords(text);
System.out.println(leastWords);

输出:

[update, scroll, bar]

我不确定如何遍历所有可能性,因为它们会根据所选单词而变化。开始会是这样的:

public List<String> getLeastSubWords(String text)
{
    String textBackup = text;
    List<String> subWords = getSubWords(text);
    System.out.println(subWords);
    List<List<String>> containing = new ArrayList<>();

    List<String> validWords = new ArrayList<>();

    for (String subWord : subWords)
    {
        if (text.startsWith(subWord))
        {
            validWords.add(subWord);
            text = text.substring(subWord.length());
        }
    }

    // Did we find a valid words distribution?
    if (text.length() == 0)
    {
        System.out.println(validWords.size());
    }

    return null;
}

注意:
这与this 问题有关。

【问题讨论】:

  • 第一次提取text中包含的所有单词的列表?第二,您尝试找到构成该字符串的最少单词(确切地说?)?如果没有解决方案怎么办?我认为同时完成这两项任务会容易得多。
  • 最好使用像TreeSet 这样的索引集合而不是ArrayList

标签: java nlp text-segmentation


【解决方案1】:

更新:如果你反转内部的 foreach,下面的算法会更有效。在这种情况下,将首先检查较长的单词。


这是回溯算法的典型情况。

将您的话存储在TreeSet 中,并实现此算法:

  1. 将开始和结束指针设置为0。创建一个堆栈来存储以前版本的指针。

  2. 从开始指针生成子串,同时增加结束指针,寻找已知单词。如果找到一个单词,将其存储在一个数组中,并将单词的长度添加到起始指针,将该指针压入堆栈。如果未找到已知单词或到达最后一个字符,则将开始和结束指针设置为从堆栈中弹出的前一个值(回溯)。重复 2. 步骤。

  3. 要找到最少的子词,您必须存储以前的最佳解决方案,并将其字数与当前解决方案的字数进行比较。

下面是一个示例实现。它包含一些优化实验:没有递归,在坏分支上回溯等。您可以添加更多优化(例如,跟踪使用的起始位置,或查找可能的子词起始位置),但可能没有必要,如果参数是一个不太长的词。

public class SubWordFinder {

    private TreeSet<String> words = new TreeSet<String>();

    public SubWordFinder(Collection<String> words) {
        this.words.addAll(words);
    }

    public List<String> findSubWords(String word) throws NoSolutionFoundException {
        List<String> bestSolution = new ArrayList<String>();
        if (word.isEmpty()) {
            return bestSolution;
        }
        long length = word.length();
        int[] pointer = new int[]{0, 0};
        LinkedList<int[]> pointerStack = new LinkedList<int[]>();
        LinkedList<String> currentSolution = new LinkedList<String>();
        while (true) {
            boolean backtrack = false;
            for (int end = pointer[1] + 1; end <= length; end++) {
                if (end == length) {
                    backtrack = true;
                }
                pointer[1] = end;
                String wordToFind = word.substring(pointer[0], end);
                if (words.contains(wordToFind)) {
                    currentSolution.add(wordToFind);
                    if (backtrack) {
                        if (bestSolution.isEmpty() || currentSolution.size() < bestSolution.size()) {
                            bestSolution = new ArrayList<String>(currentSolution);
                        }
                        currentSolution.removeLast();
                    } else if (!bestSolution.isEmpty() && currentSolution.size() == bestSolution.size()) {
                        currentSolution.removeLast();
                        backtrack = true;
                    } else {
                        int nextStart = end;
                        int[] nextPointer = new int[]{nextStart, nextStart};
                        pointerStack.add(pointer);
                        pointer = nextPointer;
                    }
                    break;
                }
            }
            if (backtrack) {
                if (pointerStack.isEmpty()) {
                    break;
                } else {
                    currentSolution.removeLast();
                    pointer = pointerStack.removeLast();
                }
            }
        }
        if (bestSolution.isEmpty()) {
            throw new NoSolutionFoundException();
        } else {
            return bestSolution;
        }
    }

    public class NoSolutionFoundException extends Exception {

        private static final long serialVersionUID = 1L;

    }

}

测试:

public class SubWordFinderTest {

    @Test
    public void generalTest() throws SubWordFinder.NoSolutionFoundException {
        List<String> words = new ArrayList<String>();
        words.add("ab");
        words.add("abc");
        words.add("cd");
        words.add("cde");
        words.add("de");
        words.add("e");
        SubWordFinder finder = new SubWordFinder(words);
        assertEquals(new ArrayList<String>(), finder.findSubWords(""));
        assertEquals(Arrays.asList("ab", "cde"), finder.findSubWords("abcde"));
        assertEquals(Arrays.asList("cd", "cde"), finder.findSubWords("cdcde"));
        assertEquals(Arrays.asList("abc", "cd"), finder.findSubWords("abccd"));
        assertEquals(Arrays.asList("de", "e", "e", "e", "e"), finder.findSubWords("deeeee"));
        try {
            finder.findSubWords("ae");
            fail();
        } catch (SubWordFinder.NoSolutionFoundException e) {
        }
        try {
            finder.findSubWords("abcccd");
            fail();
        } catch (SubWordFinder.NoSolutionFoundException e) {
        }
        try {
            finder.findSubWords("abcdex");
            fail();
        } catch (SubWordFinder.NoSolutionFoundException e) {
        }
    }

    @Test
    public void dictionaryTest() throws IOException, SubWordFinder.NoSolutionFoundException {
        String resourceDir = "/path_to_resources";
        InputStream inputStream = getClass().getResource(resourceDir + "/20k.txt").openStream();
        InputStreamReader inputStreamReader = new InputStreamReader(inputStream);
        BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
        List<String> words = new ArrayList<String>();
        String line;
        while ((line = bufferedReader.readLine()) != null) {
            words.add(line);
        }
        SubWordFinder finder = new SubWordFinder(words);
        assertEquals(Arrays.asList("bromide", "freshet"), finder.findSubWords("bromidefreshet"));
    }

}

【讨论】:

  • 感谢您的努力 :) 我尝试了您的代码,但没有返回正确的结果。使用我的示例输入updatescrollbar,它将返回[u, p, d, a, t, e, s, c, r, o, l, l, bar],并带有字典子单词列表,这显然不是最少的单词解决方案......
  • 如您所见,我有一定数量的通过子测试。请附上你的字典和/或你失败的测试用例。
  • 谢谢。我忘记了,如果有多个具有相同数量子词的解决方案,则应该选择具有最长最短单词的解决方案。所以[update, scroll, bar][update, sc, rollbar] 更受欢迎,因为sc 只有2 个字符,而bar 有3 个字符。这将是该方法的另一个改进。无论如何,我明白了。查看第一篇文章
  • 如果您有更多要求,请使用自定义Comparator 而不是currentSolution.size() &lt; bestSolution.size()。虽然此比较器与原始要求(字数)兼容,但也可以保留错误的分支出口优化。
  • 回想起来,如果您反转内部 foreach,该算法可以更有效。在这种情况下,将首先检查较长的单词。当然,您仍然需要进行额外的比较。
【解决方案2】:

这要求有很多场景可能性。

您的示例 (updatescrollbar) 已经有 up date ate update scroll bar 并且仍然很容易,但是如果您有一个作为子词的单词,这让您在字符串末尾有一个字符的可能性怎么办。

因此,要完成此操作,您必须对子词列表进行多次迭代,跟踪与您的文本匹配的当前最短有效版本,并继续迭代,直到您尝试了所有变体。

您可以减少变体的数量,例如通过使用将剩余要匹配空间考虑在内的算法:

  • 按长度对子词进行排序,并尝试首先使用最长的词来匹配文本:length subword possible=text-/- 匹配文本。 这将使用包含,因此要匹配的文本仍然可以在已经匹配的单词之前和之后:为您的文本使用数组,以便更容易跟踪匹配的文本。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2015-11-12
    • 2020-06-27
    • 1970-01-01
    • 1970-01-01
    • 2022-07-03
    • 2021-06-05
    • 2011-02-08
    • 1970-01-01
    相关资源
    最近更新 更多