Выбор оптимального набора в соответствии с ранжированными критериями - PullRequest
1 голос
/ 14 июля 2010

Мне дана строка и набор правил, которые выбирают допустимые подстроки с помощью процесса, который здесь не важен.Учитывая перечисление всех допустимых подстрок, я должен найти оптимальный набор подстрок в соответствии с набором ранжированных критериев, таких как:

  1. Подстроки не должны перекрываться
  2. Все символы должныбыть частью подстроки, если это возможно
  3. Использовать как можно меньше различных подстрок
  4. и т. д.

Например, учитывая строку abc и подстроки [a, ab, bc], оптимальный набор подстрок по предыдущим правилам равен [a, bc].

В настоящее время я делаю это с помощью стандартного наивного алгоритма, который перечисляет все возможные наборы подстрок, а затем перебирает их, чтобы найти лучшийкандидат.Проблема заключается в том, что по мере увеличения длины строки и количества подстрок число возможных наборов увеличивается в геометрической прогрессии.С 50 подстроками (что вполне возможно для этого приложения), число перечисляемых наборов составляет 2 ^ 50, что является чрезвычайно запретительным.

Похоже, должен быть способ избежатьгенерирование множества наборов, которые, очевидно, будут проигрышными или алгоритмически сходятся на оптимальном наборе без слепой генерации каждого кандидата.Какие есть варианты?

Обратите внимание, что для этого приложения может быть приемлемым использовать алгоритм, который предлагает статистическую, а не абсолютную гарантию, такую ​​как n% вероятность попадания неоптимального кандидата, где n достаточно маленький.

Ответы [ 4 ]

2 голосов
/ 14 июля 2010

Вот рабочее решение на Хаскеле. Я назвал уникальные подстроки символами , а ассоциацию одного вхождения подстрок размещения . Я также интерпретировал критерий 3 («Используйте как можно меньше различных подстрок») как «используйте как можно меньше символов», а не «используйте как можно меньше мест размещения».

Это динамическое программирование подход; фактическое сокращение происходит из-за памятки. Теоретически, умная реализация haskell может сделать это за вас (но есть другие способы , где вы заключаете makeFindBest), я бы предложил использовать битовое поле для представления используемых символов и просто целое число для представления оставшаяся строка. Оптимизация возможна из-за того, что: при заданных оптимальных решениях для строк S1 и S2, которые оба используют один и тот же набор символов, если S1 и S2 объединены, тогда два решения могут быть объединены аналогичным образом, и новое решение будет оптимальный. Следовательно, для каждого раздела входной строки makeFindBest нужно оценивать только один раз в постфиксе для каждого возможного набора символов, используемого в префиксе.

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

Где n - длина строки, а m - количество символов, наихудший случай - O ( m ^ n ), а m - O (2 ^ * * п тысячу тридцать один ). Обратите внимание, что удаление ограничения 3 сделает вещи намного быстрее: для запоминания потребуется только параметризация оставшейся строкой, которая является кэшем O ( n ), а не O ( n ). * 2 ^ м )!

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

Также обратите внимание, что ваш 4-й "и т. Д." Критерии могут сильно изменить проблему, если она ограничивает проблему таким образом, чтобы сделать возможным более агрессивное сокращение или требует возврата назад!

module Main where

import List
import Maybe
import System.Environment

type Symbol = String
type Placement = String

-- (remaining, placement or Nothing to skip one character)
type Move = (String, Maybe Placement)

-- (score, usedsymbols, placements)
type Solution = (Int, [Symbol], [Placement])

-- invoke like ./a.out STRING SPACE-SEPARATED-SYMBOLS ...
-- e.g. ./a.out "abcdeafghia" "a bc fg"
-- output is a list of placements
main = do
  argv <- System.Environment.getArgs
  let str = head argv
      symbols = concat (map words (tail argv))
  (putStr . show) $ findBest str symbols
  putStr "\n"

getscore :: Solution -> Int
getscore (sc,_,_) = sc

-- | consume STR SYM consumes SYM from the start of STR.  returns (s, SYM)
-- where s is the rest of STR, after the consumed occurrence, or Nothing if
-- SYM isnt a prefix of STR.
consume :: String -> Symbol -> Maybe Move
consume str sym = if sym `isPrefixOf` str
                  then (Just (drop (length sym) str, (Just sym)))
                  else Nothing

-- | addToSoln SYMBOLS P SOL incrementally updates SOL with the new SCORE and
-- placement P
addToSoln :: [Symbol] -> Maybe Placement -> Solution -> Solution
addToSoln symbols Nothing (sc, used, ps) = (sc - (length symbols) - 1, used, ps)
addToSoln symbols (Just p) (sc, used, ps) = 
  if p `elem` symbols
  then (sc - 1, used `union` [p], p : ps)
  else (sc, used, p : ps)

reduce :: [Symbol] -> Solution -> Solution -> [Move] -> Solution
reduce _ _ cutoff [] = cutoff
reduce symbols parent cutoff ((s,p):moves) =
    let sol = makeFindBest symbols (addToSoln symbols p parent) cutoff s
        best = if (getscore sol) > (getscore cutoff)
               then sol
               else cutoff
    in reduce symbols parent best moves

-- | makeFindBest SYMBOLS PARENT CUTOFF STR searches for the best placements
-- that can be made on STR from SYMBOLS, that are strictly better than CUTOFF,
-- and prepends those placements to PARENTs third element.
makeFindBest :: [Symbol] -> Solution -> Solution -> String -> Solution
makeFindBest _ cutoff _ "" = cutoff
makeFindBest symbols parent cutoff str =
  -- should be memoized by (snd parent) (i.e. the used symbols) and str
  let moves = if (getscore parent) > (getscore cutoff)
              then (mapMaybe (consume str) symbols) ++ [(drop 1 str, Nothing)]
              else (mapMaybe (consume str) symbols)
  in reduce symbols parent cutoff moves

-- a solution that makes no placements
worstScore str symbols = -(length str) * (1 + (length symbols))

findBest str symbols =
  (\(_,_,ps) -> reverse ps)
  (makeFindBest symbols (0, [], []) (worstScore str symbols, [], []) str)
2 голосов
/ 14 июля 2010

Похоже, мне нужна древовидная структура.

По сути, ваше первоначальное ветвление находится на всех подстроках, а затем на всех, кроме той, которую вы использовали в первом раунде и т. Д. Вплоть до самого дна. Вы правы в том, что это ветвится до 2 ^ 50, но если вы используете ab-pruning, чтобы быстро завершить ветки, которые явно уступают, а затем добавляете некоторую памятку, чтобы обрезать ситуации, которые вы видели, прежде чем сможете значительно ускорить.

Вам, вероятно, придется проделать немалое обучение искусственному интеллекту, чтобы получить все это, но страницы Википедии по таблицам сокращения и транспонирования помогут вам начать.

редактирование: Да, вы правы, возможно, недостаточно ясно. Предположим, в вашем примере "ABABABAB BABABABA" с подстроками {"ABAB", "BABA"}. Если вы установите функцию оценки для простой обработки потерянных символов как плохих, дерево будет выглядеть примерно так:

ABAB (eval=0)
  ABAB (eval=0)
    ABAB (eval=2 because we move past/waste a space char and a B)
      [missing expansion]
    BABA (eval=1 because we only waste the space)
      ABAB (eval=2 now have wasted the space above and a B at this level)
      BABA (eval=1 still only wasted the space)*
  BABA (eval=1 prune here because we already have a result that is 1)
BABA (eval=1 prune here for same reason)

* лучшее решение

Я подозреваю, что простых "потраченных впустую символов" недостаточно в нетривиальном примере, но это сокращает половину дерева здесь.

1 голос
/ 14 июля 2010

Это пахнет как проблема динамического программирования . Вы можете найти множество хороших источников, но суть в том, что вы генерируете набор подзадач, а затем строите «более крупные» оптимальные решения, комбинируя оптимальные подзадачи.

0 голосов
/ 18 июля 2010

Это ответ, переписанный с использованием алгоритма Aho-Corasick и алгоритма Дейкстры в C ++.Это должно быть намного ближе к вашему целевому языку C #.

Шаг Aho-Corasick создает автомат (на основе дерева суффиксов) из набора шаблонов, а затем использует этот автомат для поиска всех совпадений ввходная строка.Затем алгоритм Дейкстры рассматривает эти совпадения как узлы в группе обеспечения доступности баз данных и перемещается к концу строки в поисках пути с наименьшей стоимостью.

Этот подход намного проще анализировать, поскольку он просто объединяет два хорошо понятыхалгоритмы.

Построение автомата Ахо-Корасика - это линейное время по длине паттернов, а затем линейный поиск во входной строке + совокупная длина совпадений.

Алгоритм Дейкстрыработает в O (| E | + | V | log | V |), предполагая эффективную STL.График представляет собой DAG, где вершины соответствуют пропускам или сериям пропущенных символов.Вес ребер - это штраф за использование дополнительного шаблона или за пропуск символов.Между двумя совпадениями существует ребро, если они смежные и не перекрываются.Ребро существует от совпадения m до пропуска, если это наименьший возможный пропуск между m и другим совпадением m2 , которое перекрывается некоторым совпадением м3 начиная с того же места, что и скип (фу!).Структура алгоритма Дейкстры гарантирует, что оптимальный ответ будет первым, который будет найден к тому времени, когда мы достигнем конца входной строки (он обеспечивает неявное сокращение, предложенное Дэниелом).

#include <iostream>
#include <queue>
#include <vector>
#include <list>
#include <string>
#include <algorithm>
#include <set>

using namespace std;

static vector<string> patterns;
static string input;
static int skippenalty;

struct acnode {
      acnode() : failure(NULL), gotofn(256) {}
      struct acnode *failure;
      vector<struct acnode *> gotofn;
      list<int> outputs; // index into patterns global
};

void
add_string_to_trie(acnode *root, const string &s, int sid)
{
   for (string::const_iterator p = s.begin(); p != s.end(); ++p) {
      if (!root->gotofn[*p])
     root->gotofn[*p] = new acnode;
      root = root->gotofn[*p];
   }
   root->outputs.push_back(sid);
}

void
init_tree(acnode *root)
{
   queue<acnode *> q;
   unsigned char c = 0;
   do {
      if (acnode *u = root->gotofn[c]) {
         u->failure = root;
         q.push(u);
      } else
         root->gotofn[c] = root;
   } while (++c);
   while (!q.empty()) {
      acnode *r = q.front();
      q.pop();

      do {
         acnode *u, *v;
         if (!(u = r->gotofn[c]))
            continue;
         q.push(u);
         v = r->failure;
         while (!v->gotofn[c])
            v = v->failure;
         u->failure = v->gotofn[c];
         u->outputs.splice(u->outputs.begin(), v->gotofn[c]->outputs);
      } while (++c);
   }
}

struct match { int begin, end, sid; };

void
ahocorasick(const acnode *state, list<match> &out, const string &str)
{
   int i = 1;
   for (string::const_iterator p = str.begin(); p != str.end(); ++p, ++i) {
      while (!state->gotofn[*p])
         state = state->failure;
      state = state->gotofn[*p];
      for (list<int>::const_iterator q = state->outputs.begin();
           q != state->outputs.end(); ++q) {
         struct match m = { i - patterns[*q].size(), i, *q };
         out.push_back(m);
      }
   }
}

////////////////////////////////////////////////////////////////////////
bool operator<(const match& m1, const match& m2)
{
   return m1.begin < m2.begin
      || (m1.begin == m2.end && m1.end < m2.end);
}

struct dnode {
      int usedchars;
      vector<bool> usedpatterns;
      int last;
};

bool operator<(const dnode& a, const dnode& b) {
   return a.usedchars > b.usedchars
      || (a.usedchars == b.usedchars && a.usedpatterns < b.usedpatterns);
}
bool operator==(const dnode& a, const dnode& b) {
   return a.usedchars == b.usedchars
      && a.usedpatterns == b.usedpatterns;
}

typedef priority_queue<pair<int, dnode>,
               vector<pair<int, dnode> >,
               greater<pair<int, dnode> > > mypq;

void
dijkstra(const vector<match> &matches)
{
   typedef vector<match>::const_iterator mIt;
   vector<bool> used(patterns.size(), false);
   dnode initial = { 0, used, -1 };
   mypq q;

   set<dnode> last;
   dnode d;

   q.push(make_pair(0, initial));
   while (!q.empty()) {
      int cost = q.top().first;
      d = q.top().second;
      q.pop();

      if (last.end() != last.find(d)) // we've been here before
         continue;

      last.insert(d);
      if (d.usedchars >= input.size()) {
         break; // found optimum
      }

      match m = { d.usedchars, 0, 0 };      
      mIt mp = lower_bound(matches.begin(), matches.end(), m);

      if (matches.end() == mp) {
         // no more matches, skip the remaining string
         dnode nextd = d;
         d.usedchars = input.size();
         int skip = nextd.usedchars - d.usedchars;
         nextd.last = -skip;

         q.push(make_pair(cost + skip * skippenalty, nextd));
         continue;
      }

      // keep track of where the shortest match ended; we don't need to
      // skip more than this.
      int skipmax = (mp->begin == d.usedchars) ? mp->end : mp->begin + 1;
      while (mp != matches.end() && mp->begin == d.usedchars) {
         dnode nextd = d;
         nextd.usedchars = mp->end;
         int extra = nextd.usedpatterns[mp->sid] ? 0 : 1; // extra pattern
         int nextcost = cost + extra;
         nextd.usedpatterns[mp->sid] = true;
         nextd.last = mp->sid * 2 + extra; // encode used pattern
         q.push(make_pair(nextcost, nextd));
         ++mp;
      }

      if (mp == matches.end() || skipmax <= mp->begin)
         continue;

      // skip
      dnode nextd = d;
      nextd.usedchars = mp->begin;
      int skip = nextd.usedchars - d.usedchars;
      nextd.last = -skip;
      q.push(make_pair(cost + skip * skippenalty, nextd));
   }

   // unwind
   string answer;
   while (d.usedchars > 0) {
      if (0 > d.last) {
         answer = string(-d.last, '*') + answer;
         d.usedchars += d.last;
      } else {
         answer = "[" + patterns[d.last / 2] + "]" + answer;
         d.usedpatterns[d.last / 2] = !(d.last % 2);
         d.usedchars -= patterns[d.last / 2].length();
      }

      set<dnode>::const_iterator lp = last.find(d);
      if (last.end() == lp) return; // should not happen
      d.last = lp->last;
   }
   cout << answer;
}

int
main()
{
   int n;
   cin >> n; // read n patterns
   patterns.reserve(n);
   acnode root;
   for (int i = 0; i < n; ++i) {
      string s;
      cin >> s;
      patterns.push_back(s);
      add_string_to_trie(&root, s, i);
   }
   init_tree(&root);

   getline(cin, input); // eat the rest of the first line
   getline(cin, input);
   cerr << "got input: " << input << endl;
   list<match> matches;
   ahocorasick(&root, matches, input);

   vector<match> vmatches(matches.begin(), matches.end());
   sort(vmatches.begin(), vmatches.end());
   skippenalty = 1 + patterns.size();

   dijkstra(vmatches);
   return 0;
}

Воттестовый файл с 52 однобуквенными шаблонами (скомпилируйте и запустите тестовый файл на stdin):

52 a b c d e f g h i j k l m n o p q r s t u v w x y z A B C D E F G H I J K L M N O P Q R S T U V W X Y Z 
abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz
...