根据排名标准选择最佳组

时间:2010-07-14 03:02:40

标签: algorithm language-agnostic optimization set

我得到一个字符串,以及一组规则,这些规则通过一个在这里不重要的过程选择有效的子串。给定所有有效子串的枚举,我必须根据一组排序标准找到最佳子串集,例如:

  1. 子字符串可能不会重叠
  2. 如果可能,所有字符必须是子字符串的一部分
  3. 尽可能少使用不同的子串
  4. 例如,给定字符串abc和子字符串[a, ab, bc],前面规则的最佳子串集合为[a, bc]

    目前我正在通过枚举所有可能的子串集合的标准天真算法来实现这一点,然后迭代它们以找到最佳候选者。问题是随着字符串的长度和子串的数量增加,可能的集合的数量呈指数增长。有50个子串(在这个应用程序的可能性范围内),枚举的集合数是2 ^ 50,这是极其禁止。

    似乎应该有一种方法可以避免产生许多显然会失败的集合,或者在算法上收敛于最优集合而不必盲目地生成每个候选者。有什么选择?

    请注意,对于此应用程序,可以使用提供统计而非绝对保证的算法,例如n%有机会击中非最佳候选者,其中n是合适的小。

4 个答案:

答案 0 :(得分:2)

这是Haskell中的一个有效解决方案。我已经调用了唯一的子串 symbols ,以及一个子串的关联 placement 。我还将标准3(“尽可能少地使用不同的子串”)解释为“尽可能少地使用符号”,而不是“尽可能少地使用放置”。

这是一种动态编程方法;由于记忆而发生实际的修剪。从理论上讲,一个智能的haskell实现可以为你做到,(但是other ways你在其中包装makeFindBest),我建议使用一个位域来表示使用的符号,只是一个整数来表示剩下的字符串优化是可能的,因为:给定字符串S1和S2的最佳解决方案都使用相同的符号集,如果S1和S2连接,则两个解决方案可以以类似的方式连接,新的解决方案将是最佳。因此,对于输入字符串的每个分区,makeFindBest只需要在后缀上为前缀中使用的每个可能符号集计算一次。

我还整合了分支定界修剪,正如丹尼尔的回答所建议的那样;这使得评估功能越多,跳过的字符就越多。成本在处理的字符数量上是单调的,因此如果我们找到一组仅浪费 alpha 字符的展示位置,那么我们再也不会尝试跳过 alpha 人物。

其中 n 是字符串长度,m是符号数,最坏的情况是天真的O( m ^ n ), m 是O(2 ^ n )。请注意,删除约束3会使事情变得更快:只需要通过剩余字符串(即O( n )缓存)对记忆进行参数化,而不是O( n * 2 ^ m )!

使用字符串搜索/匹配算法,例如Aho-Corasick's string matching algorithm,将我在此处使用的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)

答案 1 :(得分:2)

在我看来,需要树形结构。

基本上你的初始分支是在所有子串上,然后是你在第一轮中使用的所有子串,一直到底部。你是对的,因为它分支到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)

*最佳解决方案

我怀疑简单的'浪费的字符'在​​非平凡的例子中是不够的,但它确实在这里修剪了一半的树。

答案 2 :(得分:1)

这闻起来像dynamic programming problem。您可以在其上找到许多好的资源,但要点是您生成一系列子问题,然后通过组合最佳子解决方案来构建“更大”的最佳解决方案。

答案 3 :(得分:0)

这是在C ++中使用 Aho-Corasick字符串匹配算法 Dijkstra算法重写的答案。这应该更接近你的C#目标语言。

Aho-Corasick步骤从模式集构造一个自动机(基于后缀树),然后使用该自动机查找输入字符串中的所有匹配项。 Dijkstra的算法然后将这些匹配视为DAG中的节点,并移向字符串的末尾,寻找成本最低的路径。

这种方法分析起来要容易得多,因为它只是简单地结合了两种易于理解的算法。

构造Aho-Corasick自动机是模式长度的线性时间,然后搜索在输入字符串中是线性的+匹配的累积长度。

Dijkstra算法在O(| E | + | V | log | V |)中运行,假设有效的STL。图形是DAG,其中顶点对应于匹配或跳过的字符的运行。边缘权重是使用额外模式或跳过字符的代价。如果它们相邻且不重叠,则在两个匹配之间存在边缘。如果在 m 之间的最短可能跳过与另一个与某些匹配重叠的匹配 m2 ,则存在从匹配 m 到跳过的边缘< em> m3 从跳过的同一个地方开始(p!)。 Dijkstra算法的结构确保最佳答案是在我们到达输入字符串末尾时找到的第一个答案(它实现了Daniel暗示的修剪)。

#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