Найдите, является ли матрица 2d подмножеством другой 2d-матрицы

Недавно я принимал участие в одном Hackathon, и я узнал о проблеме, которая пытается найти шаблон формы сетки в 2d-матрице. Образец может быть U, H и T и будет представлен 3 * 3 матрицы предположим, что если я хочу представить H и U

+--+--+--+            +--+--+--+
|1 |0 |1 |            |1 |0 |1 |
+--+--+--+            +--+--+--+
|1 |1 |1 |  --> H     |1 |0 |1 |    -> U
+--+--+--+            +--+--+--+
|1 |0 |1 |            |1 |1 |1 |
+--+--+--+            +--+--+--+

Теперь мне нужно искать это в 10*10 matrix containing 0s and 1s.Closest и только решение я могу получить алгоритм грубой силы O (n ^ 4). В таких языках, как MATLAB и R, есть очень тонкие способы сделать это, но не в C, С++. Я много пробовал искать это решение в Google и на SO.But ближайший, который я могу получить, это SO POST, в котором обсуждается реализация строкового поиска Rabin-Karp алгоритм. Но нет псевдокода или сообщения, объясняющего это. Может ли кто-нибудь помочь или предоставить любую ссылку, PDF или какую-то логику, чтобы упростить это?

ИЗМЕНИТЬ

как Юджин Ш. что N - размер большой матрицы (NxN), а k - малый (kxk), алгоритм buteforce должен принимать O ((Nk) ^ 2). Так как k фиксировано, оно сводится к O (N ^ 2). Да, абсолютно верно. Но существует ли какой-либо обобщенный способ, если N и K большие?

Ответ 1

Хорошо, вот тогда 2D Рабин-Карп.

В следующем обсуждении предположим, что мы хотим найти подматрицу (m, m) внутри a ( n, n). (Эта концепция работает и для прямоугольных матриц, но у меня кончились индексы.)

Идея заключается в том, что для каждой возможной подматрицы мы вычисляем хэш. Только если этот хеш совпадает с хешем матрицы, которую мы хотим найти, мы сравним элемент-мудрый.

Чтобы сделать это эффективным, мы должны избегать повторного вычисления всего хэша субматрицы каждый раз. Поскольку сегодня я мало спал, единственная функция хэша, для которой я мог бы легко понять, как это сделать, - это сумма 1 с в соответствующей подматрице. Я оставляю это как упражнение кому-то умнее, чем мне, чтобы понять лучшую функцию хэширования.

Теперь, если мы только что проверили подматрицу от (i, j) до ( i + m - 1, j + m - 1) и знаем, что внутри него находится x 1s, мы можем вычислить число 1s в подматрица одна вправо, т.е. из ( i, j + 1) в ( i + m - 1, j + m) - путем вычитания числа 1s в под-векторе из ( i, j) до ( i + m - 1, j) и добавив число 1s в подвектор из ( i, j + m) до ( i + m - 1, j + m).

Если мы ударим по правому краю большой матрицы, мы сдвинем окно вниз на один, а затем обратно на левое поле, а затем снова вниз на один, а затем снова вправо и т.д.

Обратите внимание, что для этого требуется выполнение операций O (m), а не O ( m 2) для каждого кандидата. Если мы сделаем это для каждой пары индексов, получим O ( m n 2). Таким образом, умновав окно размера потенциальной подматрицы через большую матрицу, мы можем уменьшить объем работы на множитель m. То есть, если мы не получаем слишком много хэш-коллизий.

Вот изображение:

The rolling hash function for the window shifted to the right.

Когда мы сдвигаем текущее окно вправо, мы вычитаем число 1s в красном столбце с левой стороны и добавим число 1s в зеленый вектор столбца с правой стороны, чтобы получить число 1s в новом окне.

Я реализовал быструю демонстрацию этой идеи, используя большую библиотеку шаблонов Eigen. В этом примере также используются некоторые вещи из Boost, но только для синтаксического анализа и форматирования вывода, поэтому вы можете легко избавиться от него, если у вас нет Boost, но вы хотите попробовать код. Индексация скриптов немного беспорядочна, но я оставлю это без дальнейших объяснений. Вышеупомянутая проза должна покрывать ее достаточно.

#include <cassert>
#include <cstddef>
#include <cstdlib>
#include <iostream>
#include <random>
#include <type_traits>
#include <utility>

#include <boost/format.hpp>
#include <boost/lexical_cast.hpp>

#include <Eigen/Dense>

#define PROGRAM "submatrix"
#define SEED_CSTDLIB_RAND 1

using BitMatrix = Eigen::Matrix<bool, Eigen::Dynamic, Eigen::Dynamic>;
using Index1D = BitMatrix::Index;
using Index2D = std::pair<Index1D, Index1D>;

std::ostream&
operator<<(std::ostream& out, const Index2D& idx)
{
  out << "(" << idx.first << ", " << idx.second << ")";
  return out;
}

BitMatrix
get_random_bit_matrix(const Index1D rows, const Index1D cols)
{
  auto matrix = BitMatrix {rows, cols};
  matrix.setRandom();
  return matrix;
}

Index2D
findSubMatrix(const BitMatrix& haystack,
              const BitMatrix& needle,
              Index1D *const collisions_ptr = nullptr) noexcept
{
  static_assert(std::is_signed<Index1D>::value, "unsigned index type");
  const auto end = Index2D {haystack.rows(), haystack.cols()};
  const auto hr = haystack.rows();
  const auto hc = haystack.cols();
  const auto nr = needle.rows();
  const auto nc = needle.cols();
  if (nr > hr || nr > hc)
    return end;
  const auto target = needle.count();
  auto current = haystack.block(0, 0, nr - 1, nc).count();
  auto j = Index1D {0};
  for (auto i = Index1D {0}; i <= hr - nr; ++i)
    {
      if (j == 0)  // at left margin
        current += haystack.block(i + nr - 1, 0, 1, nc).count();
      else if (j == hc - nc)  // at right margin
        current += haystack.block(i + nr - 1, hc - nc, 1, nc).count();
      else
        assert(!"this should never happen");
      while (true)
        {
          if (i % 2 == 0)  // moving right
            {
              if (j > 0)
                current += haystack.block(i, j + nc - 1, nr, 1).count();
            }
          else  // moving left
            {
              if (j < hc - nc)
                current += haystack.block(i, j, nr, 1).count();
            }
          assert(haystack.block(i, j, nr, nc).count() == current);
          if (current == target)
            {
              // TODO: There must be a better way than using cwiseEqual().
              if (haystack.block(i, j, nr, nc).cwiseEqual(needle).all())
                return Index2D {i, j};
              else if (collisions_ptr)
                *collisions_ptr += 1;
            }
          if (i % 2 == 0)  // moving right
            {
              if (j < hc - nc)
                {
                  current -= haystack.block(i, j, nr, 1).count();
                  ++j;
                }
              else break;
            }
          else  // moving left
            {
              if (j > 0)
                {
                  current -= haystack.block(i, j + nc - 1, nr, 1).count();
                  --j;
                }
              else break;
            }
        }
      if (i % 2 == 0)  // at right margin
        current -= haystack.block(i, hc - nc, 1, nc).count();
      else  // at left margin
        current -= haystack.block(i, 0, 1, nc).count();
    }
  return end;
}

int
main(int argc, char * * argv)
{
  if (SEED_CSTDLIB_RAND)
    {
      std::random_device rnddev {};
      srand(rnddev());
    }
  if (argc != 5)
    {
      std::cerr << "usage: " << PROGRAM
                << " ROWS_HAYSTACK COLUMNS_HAYSTACK"
                << " ROWS_NEEDLE COLUMNS_NEEDLE"
                << std::endl;
      return EXIT_FAILURE;
    }
  auto hr = boost::lexical_cast<Index1D>(argv[1]);
  auto hc = boost::lexical_cast<Index1D>(argv[2]);
  auto nr = boost::lexical_cast<Index1D>(argv[3]);
  auto nc = boost::lexical_cast<Index1D>(argv[4]);
  const auto haystack = get_random_bit_matrix(hr, hc);
  const auto needle = get_random_bit_matrix(nr, nc);
  auto collisions = Index1D {};
  const auto idx = findSubMatrix(haystack, needle, &collisions);
  const auto end = Index2D {haystack.rows(), haystack.cols()};
  std::cout << "This is the haystack:\n\n" << haystack << "\n\n";
  std::cout << "This is the needle:\n\n" << needle << "\n\n";
  if (idx != end)
    std::cout << "Found as sub-matrix at " << idx << ".\n";
  else
    std::cout << "Not found as sub-matrix.\n";
  std::cout << boost::format("There were %d (%.2f %%) hash collisions.\n")
    % collisions
    % (100.0 * collisions / ((hr - nr) * (hc - nc)));
  return (idx != end) ? EXIT_SUCCESS : EXIT_FAILURE;
}

Пока он компилируется и запускается, рассмотрите вышеприведенное как псевдокод. Я почти не пытался его оптимизировать. Это было всего лишь доказательством концепции для меня.

Ответ 2

Я собираюсь представить алгоритм, который занимает O(n*n) время в худшем случае, когда k = O(sqrt(n)) и O(n*n + n*k*k) вообще. Это расширение Ахо-Корасика до 2D. Напомним, что Aho-Corasick находит все вхождения набора шаблонов в целевой строке T, и он делает это во времени, линейном по длине шаблона, длине T и количеству вхождений.

Введем некоторую терминологию. Стог сена - это большая матрица, в которой мы ищем, и игла является матрицей шаблонов. Стол сена является матрицей nxn, а игла представляет собой матрицу kxk. Набор узоров, которые мы собираемся использовать в Ахо-Корасике, представляет собой набор рядов иглы. Этот набор содержит не более k строк и будет иметь меньше, если есть повторяющиеся строки.

Мы собираемся построить автомат Aho-Corasick (который является дополнением Trie с ссылками на отказ), а затем запустить алгоритм поиска на каждой строке стога сена. Поэтому мы берем каждый ряд иглы и ищем ее в каждом ряду стога сена. Для этого мы можем использовать алгоритм линейного времени 1D, но это все равно будет неэффективным. Преимущество Aho-Corasick заключается в том, что он ищет все шаблоны сразу.

Во время поиска мы собираемся заполнить матрицу A, которую мы будем использовать позже. Когда мы ищем в первом ряду стога сена, первая строка A заполняется вхождениями строк иглы в первом ряду стога сена. Таким образом, мы получим первую строку A, которая выглядит, например, как 2 - 0 - - 1. Это означает, что номер строки 0 иглы появляется в позиции 2 в первом ряду стога сена; номер строки 1 появляется в позиции 5; номер строки 2 появляется в позиции 0. Записи - - это позиции, которые не совпадали. Продолжайте делать это для каждой строки.

Предположим теперь, что в игле нет повторяющихся строк. Присвойте 0 первому ряду иглы, 1 ко второму и т.д. Теперь мы будем искать шаблон [0 1 2 ... k-1] в каждом столбце матрицы A с использованием линейного алгоритма поиска 1D (например, KMP). Напомним, что каждая строка A хранит позиции, в которых появляются строки иглы. Поэтому, если столбец содержит шаблон [0 1 2 ... k-1], это означает, что номер строки 0 иглы появляется в некотором ряду стога сена, номер строки 1 иглы находится чуть ниже нее и так далее. Это именно то, что мы хотим. Если есть повторяющиеся строки, просто назначьте уникальный номер каждой уникальной строке.

Поиск в столбце принимает O(n) с использованием линейного алгоритма времени. Поэтому для поиска всех столбцов требуется O(n*n). Мы заполняем матрицу во время поиска, ищем каждую строку стога сена (есть строки n), а поиск в строке занимает O(n+k*k). Итак, O(n(n+k*k)) в целом.

Итак, идея заключалась в том, чтобы найти эту матрицу, а затем свести проблему к сопоставлению 1D-шаблонов. Ахо-Корасик просто эффективен, я не знаю, есть ли еще один эффективный способ найти матрицу.

EDIT: добавлена ​​реализация.

Вот моя реализация на С++. Максимальное значение n установлено равным 100, но вы можете его изменить.

Программа начинается с чтения двух целых чисел n k (размеры матриц). Затем он читает строки n, каждая из которых содержит строку из 0 и 1 длины n. Затем он читает строки k, каждая из которых содержит строку из 0 и 1 длины k. Вывод - верхняя левая координата всех совпадений. Например, для следующего ввода.

12 2
101110111011
111010111011
110110111011
101110111010
101110111010
101110111010
101110111010
111010111011
111010111011
111010111011
111010111011
111010111011
11
10

Программа выведет:

match at (2,0)
match at (1,1)
match at (0,2)
match at (6,2)
match at (2,10)

#include <cstdio>
#include <cstring>
#include <string>
#include <queue>
#include <iostream>

using namespace std;

const int N = 100;
const int M = N;
int n, m;

string haystack[N], needle[M];
int A[N][N]; /* filled by successive calls to match */
int p[N]; /* pattern to search for in columns of A */

struct Node
{   Node *a[2]; /* alphabet is binary */
    Node *suff; /* pointer to node whose prefix = longest proper suffix of this node */
    int flag;

    Node()
    {   a[0] = a[1] = 0;
        suff = 0;
        flag = -1;
    }
};

void insert(Node *x, string s)
{   static int id = 0;
    static int p_size = 0;

    for(int i = 0; i < s.size(); i++)
    {   char c = s[i];
        if(x->a[c - '0'] == 0)
            x->a[c - '0'] = new Node;
        x = x->a[c - '0'];
    }
    if(x->flag == -1)
        x->flag = id++;

    /* update pattern */
    p[p_size++] = x->flag;
}

Node *longest_suffix(Node *x, int c)
{   while(x->a[c] == 0)
        x = x->suff;
    return x->a[c];
}

Node *mk_automaton(void)
{   Node *trie = new Node;
    for(int i = 0; i < m; i++)
    {   insert(trie, needle[i]);
    }

    queue<Node*> q;

    /* level 1 */
    for(int i = 0; i < 2; i++)
    {   if(trie->a[i])
        {   trie->a[i]->suff = trie;
            q.push(trie->a[i]);
        }
        else trie->a[i] = trie;
    }

    /* level > 1 */
    while(q.empty() == false)
    {   Node *x = q.front(); q.pop();
        for(int i = 0; i < 2; i++)
        {   if(x->a[i] == 0) continue;
            x->a[i]->suff = longest_suffix(x->suff, i);
            q.push(x->a[i]);
        }
    }

    return trie;
}

/* search for patterns in haystack[j] */
void match(Node *x, int j)
{   for(int i = 0; i < n; i++)
    {   x = longest_suffix(x, haystack[j][i] - '0');
        if(x->flag != -1)
        {   A[j][i-m+1] = x->flag;
        }
    }
}

int match2d(Node *x)
{   int matches = 0;
    static int z[M+N];
    static int z_str[M+N+1];

    /* init */
    memset(A, -1, sizeof(A));

    /* fill the A matrix */
    for(int i = 0; i < n; i++)
    {   match(x, i);
    }

    /* build string for z algorithm */
    z_str[n+m] = -2; /* acts like `\0` for strings */
    for(int i = 0; i < m; i++)
    {   z_str[i] = p[i];
    }

    for(int i = 0; i < n; i++)
    {   /* search for pattern in column i */
        for(int j = 0; j < n; j++)
        {   z_str[j + m] = A[j][i];
        }

        /* run z algorithm */
        int l, r;
        l = r = 0;
        z[0] = n + m;
        for(int j = 1; j < n + m; j++)
        {   if(j > r)
            {   l = r = j;
                while(z_str[r] == z_str[r - l]) r++;
                z[j] = r - l;
                r--;
            }
            else
            {   if(z[j - l] < r - j + 1)
                {   z[j] = z[j - l];
                }
                else
                {   l = j;
                    while(z_str[r] == z_str[r - l]) r++;
                    z[j] = r - l;
                    r--;
                }
            }
        }

        /* locate matches */
        for(int j = m; j < n + m; j++)
        {   if(z[j] >= m)
            {   printf("match at (%d,%d)\n", j - m, i);
                matches++;
            }
        }
    }

    return matches;
}

int main(void)
{   cin >> n >> m;
    for(int i = 0; i < n; i++)
    {   cin >> haystack[i];
    }
    for(int i = 0; i < m; i++)
    {   cin >> needle[i];
    }

    Node *trie = mk_automaton();
    match2d(trie);

    return 0;
}

Ответ 3

Начнем с решения O(N * N * K). Я буду использовать следующие обозначения: A является матрицей шаблонов, B является большой матрицей (той, которую мы будем искать вхождения шаблона в).

  • Мы можем исправить верхнюю строку матрицы B (т.е. будем искать все вхождения, которые начинаются в позиции (эта строка, любой столбец). Позвоните в эту строку a topRow. Теперь мы можем взять срез этой матрицы, содержащий строки [topRow; topRow + K) и все столбцы.

  • Создайте новую матрицу в результате конкатенации A + column + the slice, где a column - столбец с элементами K, которые не присутствуют в A или B (если A и B состоят из 0 и 1, мы можем использовать, например, -1). Теперь мы можем рассматривать столбцы этой новой матрицы как буквы и запускать алгоритм Кнута-Морриса-Пратта. Для сравнения двух букв требуется O(K) время, поэтому временная сложность этого шага O(N * K).

Существует несколько способов исправления верхней строки O(N), поэтому общая временная сложность O(N * N * K). Это уже лучше, чем решение грубой силы, но мы еще не закончили. Теоретическая нижняя граница O(N * N) (я предполагаю, что N >= K), и я хочу ее достичь.

Посмотрим, что можно улучшить здесь. Если бы мы могли сравнить два столбца матрицы в O(1) времени вместо O(K), мы бы достигли желаемой временной сложности. Пусть объединяются все столбцы как A, так и B, вставляя некоторый разделитель после каждого столбца. Теперь у нас есть строка, и нам нужно сравнить ее подстроки (потому что столбцы и их части теперь подстроки). Пусть построено дерево суффикса в линейном времени (с использованием алгоритма Укконена). Теперь сравнение двух подстрок - это поиск высоты наименьшего общего предка (LCA) двух узлов в этом дереве. Существует алгоритм который позволяет нам делать это с использованием линейного времени предварительной обработки и O(1) времени на запрос LCA. Это означает, что мы можем сравнивать две подстроки (или столбцы) в постоянное время! Таким образом, общая временная сложность O(N * N). Существует еще один способ достижения этой временной сложности: мы можем построить массив суффикса в линейном времени и ответить на самые длинные общие запросы префикса в постоянное время (с линейной предварительной обработкой). Тем не менее, обе эти решения O(N * N) выглядят довольно сложно, и они будут иметь большую константу.

P.S Если у нас есть полиномиальная хеш-функция, которую мы можем полностью доверять (или у нас все хорошо с несколькими ложными срабатываниями), мы можем получить гораздо более простое решение O(N * N) с использованием двухмерных полиномиальных хэшей.