Алгоритм поиска наименьшего фрагмента из поиска документа?

Я проходил через Skiena отлично "Руководство по разработке алгоритмов" и получил зависание на одном из упражнений.

Вопрос: "Учитывая строку поиска из трех слов, найдите самый маленький фрагмент документа, который содержит все три слова поиска, т.е. Фрагмент с наименьшим количеством слов в нем. Вам предоставляются позиции индекса, в которых эти слова встречаются в строке поиска, например word1: (1, 4, 5), word2: (4, 9, 10) и word3: (5, 6, 15). Каждый из списков находится в отсортированном порядке, как указано выше."

Все, что я придумал, это O (n ^ 2)... Этот вопрос находится в главе "Сортировка и поиск", поэтому я предполагаю, что есть простой и умный способ сделать это. Я сейчас что-то делаю с графиками, но это похоже на перебор.

Идеи? Благодаря

Ответы

Ответ 1

Я уже опубликовал довольно простой алгоритм, который решает именно эту проблему в этом ответе

Результаты поиска Google: как найти минимальное окно, содержащее все ключевые слова для поиска?

Однако в этом вопросе мы предположили, что ввод представлен текстовым потоком, а слова хранятся в легкодоступном для поиска наборе.

В вашем случае ввод представлен немного по-другому: как кучка векторов с отсортированными позициями для каждого слова. Это представление легко трансформируется в то, что необходимо для вышеуказанного алгоритма, просто сведя все эти векторы в один вектор пар (position, word), упорядоченных по положению. Это можно сделать буквально, или это можно сделать "фактически", поставив исходные векторы в очередь приоритетов (упорядоченных в соответствии с их первыми элементами). Выделение элемента из очереди в этом случае означает появление первого элемента из первого вектора в очереди и, возможно, погружение первого вектора в очередь в соответствии с его новым первым элементом.

Конечно, так как ваш оператор проблемы явно фиксирует количество слов как три, вы можете просто проверить первые элементы всех трех массивов и вывести наименьшее на каждой итерации. Это дает вам алгоритм O(N), где N - общая длина всех массивов.

Кроме того, ваше утверждение проблемы, похоже, предполагает, что целевые слова могут перекрываться в тексте, что довольно странно (учитывая, что вы используете термин "слово" ). Это намеренно? В любом случае, это не представляет проблемы для вышеупомянутого связанного алгоритма.

Ответ 2

Если я что-то не заметил, вот простой алгоритм O (n):

  • Мы представляем фрагмент с помощью (x, y), где x и y, где фрагмент начинается и заканчивается соответственно.
  • Фрагмент доступен, если он содержит все 3 слова поиска.
  • Начнем с недостижимого фрагмента (0,0).
  • Повторяйте следующее, пока y не достигнет конца строки:
    1. Если текущий фрагмент (x, y) возможен, перейдите к фрагменту (x + 1, y)
      Else (текущий фрагмент недостижим) перейдите к фрагменту (x, y + 1)
Выберите самый короткий фрагмент среди всех возможных фрагментов, которые мы прошли.

Время выполнения - на каждой итерации либо x, либо y увеличивается на 1, очевидно, x не может превышать y и y не может превышать длину строки, поэтому общее число итераций равно O (n), Кроме того, осуществимость может быть проверена в O (1) в этом случае, так как мы можем отслеживать, сколько вхождений каждого слова в текущий фрагмент. Мы можем поддерживать этот счет при O (1) при каждом увеличении x или y на 1.

Корректность. Для каждого x мы вычисляем минимальный возможный фрагмент (x,?). Таким образом, мы должны перейти к минимальному фрагменту. Кроме того, если y является наименьшим y таким образом, что (x, y) выполнимо, то if (x + 1, y ') является допустимым фрагментом y' >= y (Этот бит - это то, почему этот алгоритм является линейным, а остальные - т).

Ответ 3

Из этого вопроса кажется, что вам даны указатели для каждого из ваших "поисковых слов" (word1, word2, word3,..., word n) в документе. Используя алгоритм сортировки, n независимых массивов, связанных с поисковыми словами, можно легко представить в виде единого массива всех позиций индекса в возрастающем числовом порядке и метку слова, связанную с каждым индексом в массиве (массив индексов).

Основной алгоритм:

(Предназначен для работы независимо от того, планировал ли этот плакат этот вопрос два сосуществования поисковых слов с одним и тем же номером индекса.)

Сначала мы определяем простую функцию для измерения длины фрагмента, содержащего все n меток, заданных начальной точкой в ​​массиве индексов. (Из определения нашего массива видно, что любая начальная точка в массиве обязательно будет индексированным местоположением одной из n поисковых меток.) ​​Функция просто отслеживает уникальные метки поиска, которые видят, как функция выполняет итерации через элементы в массиве до тех пор, пока не будут соблюдены все n меток. Длина фрагмента определяется как разница между индексом найденной последней уникальной метки и индексом начальной точки в массиве индексов (найдена первая уникальная метка). Если все n меток не наблюдаются до конца массива, функция возвращает нулевое значение.

Теперь функция длины фрагмента может быть запущена для каждого элемента в вашем массиве, чтобы связать размер фрагмента, содержащий все n поисковых слов, начиная с каждого элемента массива. Наименьшее значение, отличное от Null, возвращаемое функцией длины фрагмента во всем массиве индексов, является фрагментом вашего документа, который вы ищете.

Необходимые оптимизации:

  • Отслеживать значение текущей кратчайшей длины фрагмента, чтобы значение было известно сразу после однократного повторения массива индексов.
  • Когда итерация через ваш массив завершает функцию длины фрагмента, если текущий проверяемый фрагмент превосходит длину самой короткой длины фрагмента, которую ранее видели.
  • Когда функция длины фрагмента возвращает значение null для того, чтобы не найти все n поисковых слов в остальных элементах массива индекса, сопоставьте длину нулевого фрагмента всем последовательным элементам в массиве индексов.
  • Если функция длины фрагмента применяется к метке слова, а метка, следующая за ней, идентична стартовой метке, присвойте нулевое значение стартовой метке и перейдите к следующей метке.

Вычислительная сложность:

Очевидно, что сортировочная часть алгоритма может быть организована в O (n log n).

Здесь, как бы я определил временную сложность второй части алгоритма (любые критические замечания и исправления были бы весьма полезны).

В лучшем случае алгоритм применяет только функцию длины фрагмента к первому элементу в массиве индексов и обнаруживает, что не существует фрагмента, содержащего все слова поиска. Этот сценарий будет вычисляться только в n вычислениях, где n - размер массива индексов. Чуть хуже, чем если бы самый маленький фрагмент оказался равным размеру всего массива. В этом случае вычислительная сложность будет немного меньше 2 n (один раз через массив, чтобы найти наименьшую длину фрагмента, второй раз, чтобы продемонстрировать, что нет других фрагментов). Чем короче средняя вычисленная длина фрагмента, тем больше функция длины фрагмента должна применяться к массиву индексов. Мы можем предположить, что наш худший случайный сценарий будет иметь место, когда функция длины фрагмента должна применяться к каждому элементу в массиве индексов. Чтобы разработать случай, когда функция будет применяться к каждому элементу в массиве индексов, нам нужно создать индексный массив, где средняя длина фрагмента по всему массиву индекса пренебрежимо мала по сравнению с размером массива индексов в целом. Используя этот случай, мы можем вычислить нашу вычислительную сложность как O (C n), где C - некоторая константа, которая значительно меньше n. Давая окончательную вычислительную сложность:

O (n log n + C n)

Где:

< < п

Edit:

AndreyT правильно указывает, что вместо сортировки слова указывается в n log n времени, можно просто слить их (так как вспомогательные массивы уже отсортированы) в n log m time, где m - количество массивов поисковых слов для объединения. Это, очевидно, ускорит алгоритм, это случаи, когда m < п.

Ответ 4

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);
} 

Ответ 5

Пример реализации в 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;
    }
}

Ответ 6

О (п)

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)}

Ответ 7

Другие ответы в порядке, но, как и я, если у вас возникли проблемы с пониманием вопроса, они не очень полезны. Позвольте перефразировать вопрос:

Учитывая три набора целых чисел (назовите их 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
    if (size < minSize), then
      minSize = size
    fi
    x = argmin(ans)
    increment x by 1
  done

  return minSize

где argmin - это индекс наименьшего элемента в ans.

+---+---+---+---+--------------------+---------+
| 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= итерация

На каждом шаге один из трех индексов увеличивается, поэтому алгоритм гарантированно завершится. В худшем случае i, j и k увеличиваются в этом порядке, и алгоритм выполняется за O(n^2) (в данном случае 9). Для данного примера он заканчивается через 5 итераций.