从搜索文档中查找最小片段的算法?

时间:2010-06-02 02:27:59

标签: algorithm

我一直在阅读Skiena出色的“算法设计手册”,并在其中一个练习中被挂了。

问题是: “给定三个单词的搜索字符串,找到包含所有三个搜索单词的文档的最小片段 - 即其中包含最少单词数的片段。您将获得索引位置,这些单词出现在搜索字符串中,例如word1:(1,4,5),word2:(4,9,10)和word3:(5,6,15)。每个列表都按排序顺序排列,如上所述。“

我想出的任何东西都是O(n ^ 2)......这个问题出现在“排序和搜索”一章中,所以我假设有一种简单而聪明的方法。我现在正在尝试使用图表,但这看起来有点矫枉过正。

想法? 感谢

7 个答案:

答案 0 :(得分:9)

除非我忽略了某些内容,否则这是一个简单的O(n)算法:

  1. 我们将通过(x,y)表示代码段,其中x和y分别是代码段的开始和结束位置。
  2. 如果代码段包含所有3个搜索字词,则该代码段是可行的。
  3. 我们将从不可行的片段(0,0)开始。
  4. 重复以下操作,直到y到达字符串结尾:
    1. 如果当前片段(x,y)可行,请转到片段(x + 1,y)
      否则(当前片段不可行)进入片段(x,y + 1)
  5. 选择我们经历过的所有可行摘要中最短的摘录。
  6. 运行时间 - 在每次迭代中,x或y增加1,显然x不能超过y,y不能超过字符串长度,因此迭代总数为O(n) 。此外,在这种情况下可以在O(1)处检查可行性,因为我们可以跟踪每个单词在当前片段中出现的次数。我们可以将此计数保持在O(1),每次x或y增加1。

    正确性 - 对于每个x,我们计算最小可行片段(x,?)。因此,我们必须重温最小的片段。此外,如果y是最小的y,使得(x,y)是可行的,那么如果(x + 1,y')是可行的片段y'> = = y(这个比特是为什么这个算法是线性的而其他的是“T)。

答案 1 :(得分:7)

我已经发布了一个相当简单的算法,可以在这个答案中解决这个问题

Google search results: How to find the minimum window that contains all the search keywords?

但是,在那个问题中,我们假设输入由文本流表示,并且单词存储在易于搜索的集合中。

在您的情况下,输入的表示略有不同:作为一组向量,每个单词的排序位置。通过简单地将所有这些矢量合并到按位置排序的(position, word)对的单个矢量中,该表示可以容易地变换为上述算法所需的内容。它可以按字面意思完成,或者可以通过将原始向量放入优先级队列(按照其第一个元素排序)来“虚拟地”完成。在这种情况下从队列中弹出一个元素意味着从队列中的第一个向量中弹出第一个元素,并可能根据其新的第一个元素将第一个向量下沉到队列中。

当然,由于你的问题陈述明确地将单词数量固定为 3 ,你可以简单地检查所有三个数组的第一个元素,并在每次迭代时弹出最小的一个。这为您提供了O(N)算法,其中N是所有数组的总长度。

此外,您对问题的陈述似乎表明目标词可能在文本中重叠,这很奇怪(假设您使用术语“词”)。这是故意的吗?在任何情况下,它都不会对上述链接算法产生任何问题。

答案 2 :(得分:5)

从这个问题来看,您似乎已经获得了每个 n “搜索词”的索引位置(word1,word2,word3,...,word n < / em>)在文档中。使用排序算法,与搜索词相关联的 n 独立数组可以很容易地以数字升序表示为所有索引位置的单个数组,并且与数组中的每个索引相关联的单词标签(索引数组)。

基本算法:

(无论该问题的海报是否意图允许两个不同的搜索词在同一索引号上共存,设计工作。)

首先,我们定义一个简单的函数,用于测量包含索引数组中起点的所有 n 标签的片段长度。 (从数组的定义可以明显看出,数组上的任何起始点都必然是 n 搜索标签之一的索引位置。)该函数只是跟踪所看到的唯一搜索标签因为函数遍历数组中的元素,直到观察到所有 n 标签。片段的长度定义为找到的最后一个唯一标签的索引与索引数组中起始点的索引(找到的第一个唯一标签)之间的差异。如果在数组结束之前未观察到所有 n 标签,则该函数返回空值。

现在,可以为数组中的每个元素运行代码段长度函数,以关联包含从数组中每个元素开始的所有 n 搜索字的代码段大小。片段长度函数在整个索引数组上返回的最小非Null值是您正在查找的文档片段。

必要的优化:

  1. 跟踪当前最短片段长度的值,以便在通过索引数组迭代一次后立即知道该值。
  2. 如果正在检查的当前片段超过之前看到的最短片段长度的长度,则在遍历数组时终止片段长度函数。
  3. 当片段长度函数返回null而未在其余索引数组元素中查找所有 n 搜索词时,将空片段长度与索引数组中的所有连续元素相关联。
  4. 如果片段长度函数应用于单词标签并且紧随其后的标签与起始标签相同,请为起始标签指定空值并转到下一个标签。
  5. 计算复杂度:

    显然,算法的排序部分可以安排在O( n log n )中。

    以下是我如何计算算法第二部分的时间复杂度(非常感谢任何批评和更正)。

    在最佳情况下,算法仅将片段长度函数应用于索引数组中的第一个元素,并发现不存在包含所有搜索词的片段。此方案将仅在 n 计算中计算,其中 n 是索引数组的大小。稍微差一点的是,如果最小的片段等于整个数组的大小。在这种情况下,计算复杂度将略小于2 n (一次通过数组以找到最小的片段长度,第二次证明没有其他片段存在)。平均计算片段长度越短,需要在索引数组上应用片段长度函数的次数越多。我们可以假设我们更糟糕的情况是需要将片段长度函数应用于索引数组中的每个元素。为了开发将函数应用于索引数组中的每个元素的情况,我们需要设计一个索引数组,其中整个索引数组的平均片段长度与整个索引数组的大小相比可以忽略不计。使用这种情况,我们可以将我们的计算复杂度写为O(C n ),其中C是一些常数,它明显小于 n 。给出最终的计算复杂度:

    O( n log n + C n

    其中:

    C&lt;&lt; 名词

    修改

    AndreyT正确地指出,不是在 n log n 时间内对单词标记进行排序,而是可以合并它们(因为子数组已经排序)在 n log m 时间,其中<​​em> m 是要合并的搜索字阵列的数量。这显然会加速算法,其中 m &lt; 名词

答案 3 :(得分:3)

O(n log k)解,其中n是索引的总数,k是单词的数量。我们的想法是使用堆来识别每次迭代中的最小索引,同时还跟踪堆中的最大索引。我还将每个值的坐标放在堆中,以便能够在恒定时间内检索下一个值。

#include <algorithm>
#include <cassert>
#include <limits>
#include <queue>
#include <vector>

using namespace std;

int snippet(const vector< vector<int> >& index) {
    // (-index[i][j], (i, j))
    priority_queue< pair< int, pair<size_t, size_t> > > queue;
    int nmax = numeric_limits<int>::min();
    for (size_t i = 0; i < index.size(); ++i) {
        if (!index[i].empty()) {
            int cur = index[i][0];
            nmax = max(nmax, cur);
            queue.push(make_pair(-cur, make_pair(i, 0)));
        }
    }
    int result = numeric_limits<int>::max();
    while (queue.size() == index.size()) {
        int nmin = -queue.top().first;
        size_t i = queue.top().second.first;
        size_t j = queue.top().second.second;
        queue.pop();
        result = min(result, nmax - nmin + 1);
        j++;
        if (j < index[i].size()) {
            int next = index[i][j];
            nmax = max(nmax, next);
            queue.push(make_pair(-next, make_pair(i, j)));
        }
    }
    return result;
}

int main() {
    int data[][3] = {{1, 4, 5}, {4, 9, 10}, {5, 6, 15}};
    vector<vector<int> > index;
    for (int i = 0; i < 3; i++) {
        index.push_back(vector<int>(data[i], data[i] + 3));
    }
    assert(snippet(index) == 2);
} 

答案 4 :(得分:2)

java中的示例实现(仅使用示例中的实现进行测试,可能存在错误)。实施基于上述答复。

import java.util.Arrays;


public class SmallestSnippet {
    WordIndex[] words; //merged array of word occurences

    public enum Word {W1, W2, W3};

    public SmallestSnippet(Integer[] word1, Integer[] word2, Integer[] word3) {
        this.words = new WordIndex[word1.length + word2.length + word3.length];
        merge(word1, word2, word3);
        System.out.println(Arrays.toString(words));
    }

    private void merge(Integer[] word1, Integer[] word2, Integer[] word3) {
        int i1 = 0;
        int i2 = 0;
        int i3 = 0;
        int wordIdx = 0;
        while(i1 < word1.length || i2 < word2.length || i3 < word3.length) {
            WordIndex wordIndex = null;
            Word word = getMin(word1, i1, word2, i2, word3, i3);
            if (word == Word.W1) {
                wordIndex = new WordIndex(word, word1[i1++]);
            }
            else if (word == Word.W2) {
                wordIndex = new WordIndex(word, word2[i2++]);
            }
            else {
                wordIndex = new WordIndex(word, word3[i3++]);
            }
            words[wordIdx++] = wordIndex;
        }       
    }

    //determine which word has the smallest index
    private Word getMin(Integer[] word1, int i1, Integer[] word2, int i2, Integer[] word3,
            int i3) {
        Word toReturn = Word.W1;
        if (i1 == word1.length || (i2 < word2.length && word2[i2] < word1[i1])) {
            toReturn  = Word.W2;
        }
        if (toReturn == Word.W1 && i3 < word3.length && word3[i3] < word1[i1])
        {
            toReturn = Word.W3;
        }
        else if (toReturn == Word.W2){
            if (i2 == word2.length || (i3 < word3.length && word3[i3] < word2[i2])) {
                toReturn = Word.W3;
            }
        }
        return toReturn;
    }

    private Snippet calculate() {
        int start = 0;
        int end = 0;
        int max = words.length;
        Snippet minimum = new Snippet(words[0].getIndex(), words[max-1].getIndex());
        while (start < max)
        {
            end = start;
            boolean foundAll = false;
            boolean found[] = new boolean[Word.values().length];
            while (end < max && !foundAll) {
                found[words[end].getWord().ordinal()] = true;
                boolean complete = true;
                for (int i=0 ; i < found.length && complete; i++) {
                    complete = found[i];
                }
                if (complete)
                {
                    foundAll = true;
                }
                else {
                    if (words[end].getIndex()-words[start].getIndex() == minimum.getLength())
                    {
                        // we won't find a minimum no need to search further
                        break;
                    }
                    end++;
                }
            }
            if (foundAll && words[end].getIndex()-words[start].getIndex() < minimum.getLength()) {
                minimum.setEnd(words[end].getIndex());
                minimum.setStart(words[start].getIndex());
            }
            start++;
        }
        return minimum;

    }


    /**
     * @param args
     */
    public static void main(String[] args) {
        Integer[] word1 = {1,4,5};
        Integer[] word2 = {3,9,10};
        Integer[] word3 = {2,6,15};
        SmallestSnippet smallestSnippet = new SmallestSnippet(word1, word2, word3);
        Snippet snippet = smallestSnippet.calculate();
        System.out.println(snippet);

    }
}

助手班:

public class Snippet {

    private int start;

    private int end;

//getters, setters etc

    public int getLength()
    {
        return Math.abs(end - start);
    }
}



public class WordIndex
{
    private SmallestSnippet.Word word;
    private int index;
    public WordIndex(SmallestSnippet.Word word, int index) {

        this.word = word;
        this.index = index;
    }
}

答案 5 :(得分:1)

O(n)的

Pair find(int[][] indices) {
pair.lBound = max int;
pair.rBound = 0;
index = 0;

for i from 0 to indices.lenght{
    if(pair.lBound > indices[i][0]){
        pair.lBound = indices[i][0]
        index = i;
    }
    if(indices[index].lenght > 0)
        pair.rBound = max(pair.rBound, indices[i][0])
}
remove indices[index][0]

return min(pair, find(indices)}

答案 6 :(得分:1)

其他答案都很好,但是像我一样,如果您一开始就很难理解这个问题,那么这些并没有真正的帮助。让我们改一下这个问题:

  

给出三组整数(分别称为A,B和C),找到每组中包含一个元素的最小连续范围。

这三组内容有些混淆。该书的第二版将其表示为{1, 4, 5}{4, 9, 10}{5, 6, 15}。但是,上面的注释中提到的另一个版本是{1, 4, 5}{3, 9, 10}{2, 6, 15}。如果一个单词不是另一个单词的后缀/前缀,则不可能使用版本1,因此让我们来看第二个单词。

由于一幅图片价值一千个单词,因此请绘制点:

enter image description here

简单地通过视觉检查以上内容,我们可以看到此问题有两个答案:[1,3][2,4],大小均为3(每个范围三分)。

现在,该算法。这个想法是从最小的有效范围开始,然后逐渐尝试通过向左移动左边界来缩小范围。我们将使用从零开始的索引。

MIN-RANGE(A, B, C)
  i = j = k = 0
  minSize = +∞

  while i, j, k is a valid index of the respective arrays, do
    ans = (A[i], B[j], C[k])
    size = max(ans) - min(ans) + 1
    minSize = min(size, minSize)
    x = argmin(ans)
    increment x by 1
  done

  return minSize

其中argminans中最小元素的索引。

+---+---+---+---+--------------------+---------+
| n | i | j | k | (A[i], B[j], C[k]) | minSize |
+---+---+---+---+--------------------+---------+
| 1 | 0 | 0 | 0 | (1, 3, 2)          | 3       |
+---+---+---+---+--------------------+---------+
| 2 | 1 | 0 | 0 | (4, 3, 2)          | 3       |
+---+---+---+---+--------------------+---------+
| 3 | 1 | 0 | 1 | (4, 3, 6)          | 4       |
+---+---+---+---+--------------------+---------+
| 4 | 1 | 1 | 1 | (4, 9, 6)          | 6       |
+---+---+---+---+--------------------+---------+
| 5 | 2 | 1 | 1 | (5, 9, 6)          | 5       |
+---+---+---+---+--------------------+---------+
| 6 | 3 | 1 | 1 |                    |         |
+---+---+---+---+--------------------+---------+

n =迭代

在每个步骤中,三个索引之一都会增加,因此可以保证算法最终终止。在最坏的情况下,ijk以此顺序递增,并且算法以O(n^2)(在这种情况下为9)时间运行。对于给定的示例,它在5次迭代后终止。