搜索算法以生成按优先级排序的组合

时间:2013-10-06 00:01:32

标签: algorithm

我正在尝试找到一种算法来识别结果的有序组合,如下所示:

  • 有N场比赛,每场比赛有3个相互排斥的结果(胜利,失败或平局),共有3N个结果和3 ^ N个组合
  • 3N可能结果中的每一个都被赋予了唯一的等级,最优选的结果具有等级1并且最不优选的结果具有等级3N
  • 查找每个比赛的前M个结果组合,从包含最佳排名结果的组合开始。

例如,假设N = 3,结果排名如下:

Contest 1 Win = 1
Contest 1 Tie = 4
Contest 1 Loss = 7
Contest 2 Win = 2
Contest 2 Tie = 5
Contest 2 Loss = 8
Contest 3 Win = 3
Contest 3 Tie = 6
Contest 3 Loss = 9

鉴于这些排名,组合应按如下顺序排列:

Contest 1 Win  (1), Contest 2 Win  (2), Contest 3 Win  (3)
Contest 1 Win  (1), Contest 2 Win  (2), Contest 3 Tie  (6)
Contest 1 Win  (1), Contest 2 Win  (2), Contest 3 Loss (9)
Contest 1 Win  (1), Contest 2 Tie  (5), Contest 3 Win  (3)
Contest 1 Win  (1), Contest 2 Loss (8), Contest 3 Win  (3)
Contest 1 Win  (1), Contest 2 Tie  (5), Contest 3 Tie  (6)
Contest 1 Win  (1), Contest 2 Tie  (5), Contest 3 Loss (9)
Contest 1 Win  (1), Contest 2 Loss (8), Contest 3 Win  (6)
Contest 1 Win  (1), Contest 2 Loss (8), Contest 3 Loss (9)
Contest 1 Tie  (4), Contest 2 Win  (2), Contest 3 Win  (3)
Contest 1 Loss (7), Contest 2 Win  (2), Contest 3 Win  (3)
Contest 1 Tie  (4), Contest 2 Win  (2), Contest 3 Tie  (6)
Contest 1 Tie  (4), Contest 2 Win  (2), Contest 3 Loss (9)
Contest 1 Loss (7), Contest 2 Win  (2), Contest 3 Tie  (6)
Contest 1 Loss (7), Contest 2 Win  (2), Contest 3 Loss (9)
Contest 1 Tie  (4), Contest 2 Tie  (5), Contest 3 Win  (3)
Contest 1 Tie  (4), Contest 2 Loss (8), Contest 3 Win  (3)
Contest 1 Loss (7), Contest 2 Tie  (5), Contest 3 Win  (3)
Contest 1 Loss (7), Contest 2 Loss (8), Contest 3 Win  (3)
Contest 1 Tie  (4), Contest 2 Tie  (5), Contest 3 Tie  (6)
Contest 1 Tie  (4), Contest 2 Tie  (5), Contest 3 Loss (9)
Contest 1 Tie  (4), Contest 2 Loss (8), Contest 3 Tie  (6)
Contest 1 Tie  (4), Contest 2 Loss (8), Contest 3 Loss (9)
Contest 1 Loss (7), Contest 2 Tie  (5), Contest 3 Tie  (6)
Contest 1 Loss (7), Contest 2 Tie  (5), Contest 3 Loss (9)
Contest 1 Loss (7), Contest 2 Loss (8), Contest 3 Tie  (6)
Contest 1 Loss (7), Contest 2 Loss (8), Contest 3 Loss (9)

我正在寻找一种方法来生成这些组合以便任意大的N值,尽管我不希望得到所有组合。例如,在N = 256和总共3 ^ 256种组合的情况下,我希望找到前500种组合。

4 个答案:

答案 0 :(得分:4)

此算法似乎有效。 Python中的实现如下。

基本上我接受输入,然后按给定结果的值对其进行排序:

Contest 1 Win = 1
Contest 2 Win = 2
Contest 3 Win = 3
Contest 1 Tie = 4
Contest 2 Tie = 5
Contest 3 Tie = 6
Contest 1 Loss = 7
Contest 2 Loss = 8
Contest 3 Loss = 9

我称之为排序。然后我生成一个空的结果列表:

[None, None, None]

递归算法非常简单,如下所示:

  1. 如果插槽已满,则打印结果并返回。
  2. 否则,按升序迭代未使用的排序。如果订购的是未使用的插槽,则填充插槽,将订购标记为已使用,然后重复。否则,继续迭代。
  3. 这是代码。还有一个额外的技巧可以避免重复,如果我们只是填写说排序#6,我们只会使用排序#7,8和9。

    #rankings as tuple of (winval, tieval, lossval) for each
    #contest
    rankings = [(1, 4, 7), (2, 5, 8), (3, 6, 9)]
    
    #first sort the rankings by their values into
    #list of (contestnum, w/t/l, value)
    orderings = []
    for i, (w, t, l) in enumerate(rankings):
        orderings.append((i, 'w', w))
        orderings.append((i, 't', t))
        orderings.append((i, 'l', l))
    orderings.sort(key=lambda (i,res,val): val)
    
    #now, find solution recursively as follows:
    #- if list is full then print result & return
    #- else, iterate thru the rankings & recur for each unused slot
    
    def solve(orderings, slots, used_orderings, first_ordering):
        if all(slot is not None for slot in slots):
            yield slots
            return
    
        i = first_ordering
        while i < len(orderings):
            slot, result, value = orderings[i]
    
            if used_orderings[i]:
                i += 1
                continue
            if slots[slot] is not None:
                i += 1
                continue
    
            slots[slot] = (result, value)
            used_orderings[i] = True
            for solution in solve(orderings, slots, used_orderings, i):
                yield solution
            #backtrack
            slots[slot] = None
            used_orderings[i] = False
    
            i += 1
    
    #print the first 40 solutions
    num_solutions = 0
    for solution in solve(orderings, [None]*len(rankings), [False]*len(orderings), 0):
        print "Solution #%d: %s" % (num_solutions+1, solution)
        num_solutions += 1
        if num_solutions >= 40:
            break
    

    以下是针对给定输入打印的结果,其匹配问题:

    Solution #1: [('w', 1), ('w', 2), ('w', 3)]
    Solution #2: [('w', 1), ('w', 2), ('t', 6)]
    Solution #3: [('w', 1), ('w', 2), ('l', 9)]
    Solution #4: [('w', 1), ('t', 5), ('w', 3)]
    Solution #5: [('w', 1), ('l', 8), ('w', 3)]
    Solution #6: [('w', 1), ('t', 5), ('t', 6)]
    Solution #7: [('w', 1), ('t', 5), ('l', 9)]
    Solution #8: [('w', 1), ('l', 8), ('t', 6)]
    Solution #9: [('w', 1), ('l', 8), ('l', 9)]
    Solution #10: [('t', 4), ('w', 2), ('w', 3)]
    Solution #11: [('l', 7), ('w', 2), ('w', 3)]
    Solution #12: [('t', 4), ('w', 2), ('t', 6)]
    Solution #13: [('t', 4), ('w', 2), ('l', 9)]
    Solution #14: [('l', 7), ('w', 2), ('t', 6)]
    Solution #15: [('l', 7), ('w', 2), ('l', 9)]
    Solution #16: [('t', 4), ('t', 5), ('w', 3)]
    Solution #17: [('t', 4), ('l', 8), ('w', 3)]
    Solution #18: [('l', 7), ('t', 5), ('w', 3)]
    Solution #19: [('l', 7), ('l', 8), ('w', 3)]
    Solution #20: [('t', 4), ('t', 5), ('t', 6)]
    Solution #21: [('t', 4), ('t', 5), ('l', 9)]
    Solution #22: [('t', 4), ('l', 8), ('t', 6)]
    Solution #23: [('t', 4), ('l', 8), ('l', 9)]
    Solution #24: [('l', 7), ('t', 5), ('t', 6)]
    Solution #25: [('l', 7), ('t', 5), ('l', 9)]
    Solution #26: [('l', 7), ('l', 8), ('t', 6)]
    Solution #27: [('l', 7), ('l', 8), ('l', 9)]
    

    如果我为256场比赛随机生成一组排名,它似乎会立即运行。

答案 1 :(得分:1)

首先,让我们重新解释一下这个问题,以便抽象细节,并确保我们正在谈论同样的问题。

有3 ^ N个长度为N的元组。每个元组的组件a_i(a_1,a_2,...,a_N)是1到3N之间的不同整数,对于固定的i,a_i只能取值在基数S_i中,基数为3.对于[1,3N]中的每个值,元组中只有一个位置可以假定该值。

现在,通过排序(S)表示以整数的自然顺序对元组S的组件进行排序所产生的元组。我们说元组S小于元组T如果排序(S)小于按字典顺序排序(T)。

问题在于在现有的3 ^ N个中找到给定顺序中的前M个元组,其中M <&lt; 3 ^ N

我看到的解决方案原则基本上是修剪回溯。

以严格的方式修剪搜索空间,计算3的最大功率不大于M.假设这个幂是H.我们有3 ^(H + 1)&gt; M> = 3 ^ H.此时,您知道您的M元组位于一个集合中,其中(N-H-1)元组组件采用其最小可能值。可以按如下方式找到并修复这些组件:首先,获取值为1的组件i,并将其固定为1.然后,在组件i未采用的[1,3N]值中,选择最小的组件,将唯一能够获取该值的组件j修复为相同的值。以相同的方式继续,固定(N-H-1)组件。之后,您确定了一组最多3M元组。您可以生成完整的元组集(通过对剩余的H + 1组件进行穷举搜索),然后对此集进行排序,获得前M个元组。

答案 2 :(得分:0)

您可以在O(N.M)时间内构建解决方案。这里的解决方案将是M个排列的列表,每个排列都有相关的分数,按分数的降序排列。

如果N为1,则解决方案很简单:您只需根据他们的分数(如果M <3,则减少解决方案)来订购您获得的3个结果。也就是说,如果结果得分是胜利,失败和平局,那么解决方案就是:

  • 排序([('W',胜利),('L',输掉),('D',平局)])

(按分数排序,当然是降序)。

现在进行迭代步骤。给出大小为N-1的问题的解决方案(大小为M),然后考虑一个新的比赛结果(赢,输,抽奖)。

只需在每个M解决方案中添加胜利,输掉,绘制分数,然后求助。 也就是说,假设解决方案是[(res_1,score_1),(res_2,score_2),...(res_M,score_M)],那么:

  • wins = [(res_1 +'W',score_1 + win),(res_2 +'W',score_2 + win),...(res_M +'W',score_M + win)]
  • 失败= [(res_1 +'L',得分_1 +失败),(res_2 +'L',得分_2 +失败),...(res_M +'L',得分_M +失败)]
  • draw = [(res_1 +'D',score_1 + draw),(res_2 +'D',score_2 + draw),...(res_M +'D',score_M + draw)]

新解决方案将是sorted(wins + loses + draws)[:M],这是组合解决方案中的第一个M最大解决方案。注意,您可以使用mergesort中的合并步骤在O(M)时间内执行此排序步骤。

这是一些演示算法的草率代码。对于紧密的解决方案,列表切片和字符串附加需要用O(1)替换,并且使用合并步骤而不是一般排序。

def results(outcomes, M):
    if len(outcomes) == 1:
        return sorted(zip(outcomes[0], 'WLD'), reverse=True)[:M]
    tr = results(outcomes[1:], M)
    result = []
    for outscore, outcome in zip(outcomes[0], 'WLD'):
        result.extend((sc + outscore, pr + outcome) for sc, pr in tr)
    return sorted(result, reverse=True)[:M]

# Outcome scores for each contest, in order (win, lose, draw).
testcase = [(1, 7, 4), (2, 8, 5), (3, 9, 6)]
print results(testcase, 5)

输出结果为:

[(24, 'LLL'), (21, 'LLD'), (21, 'LDL'), (21, 'DLL'), (18, 'WLL')]

答案 3 :(得分:0)

GCC 4.7.3:g ++ -Wall -Wextra -std = c ++ 0x perm.cpp

#include <algorithm>
#include <cassert>
#include <functional>
#include <iostream>
#include <iterator>
#include <string>

using ints = std::vector<int>;

template <typename T>
bool equal(const std::vector<T>& a, const std::vector<T>& b) {
  return std::equal(std::begin(a), std::end(a), std::begin(b)); }

bool is_strictly_monotonic(const ints& p) {
  return std::is_sorted(std::begin(p), std::end(p), std::less_equal<int>()); }

ints gen_seq(int size, int init) {
  ints v(size);
  for (auto& e : v) { e = ++init; }
  return v; }

ints begin_combination(const ints& p, int mod) {
  return gen_seq(p.size(), -1); }

// Not the same as a normal STL end. This is actually the last combination.
ints end_combination(const ints& p, int mod) {
  return gen_seq(p.size(), mod - p.size()); }

// This is the most complicated bit of code, but what it does is
// straightforward. This function treats the input sequence as the
// (radix m+1) digits in a counter. It increments the counter by one, while
// maintaining the constraint that the digits in the sequence be strictly
// monotonic. This means that some numbers in the regular counter sequence
// will be skipped, for example the sequence after {1, 2, 9} (radix 10)
// is {1, 3, 4}. Like all digital counters it wraps on overflow.
void inc_monotonic_seq(ints& p, const int m) {
  assert(is_strictly_monotonic(p));

  int i = p.size() - 1;
  // scan right to left for number to increment
  while (i != -1) {
    if (p[i] < m) {
      ++p[i];
      ints::size_type j = i + 1;
      // propogate carry left to right
      while (j != p.size()) {
        p[j] = p[j - 1] + 1;
        if (m < p[j]) { break; }
        ++j; }
      if (j == p.size()) { break; } }
    --i; }

  // wrap around
  if (i == -1) { p = begin_combination(p, m); }

  assert(is_strictly_monotonic(p));
}

// A combination is valid if each contest is represented once.
bool is_valid_combination(const ints& p, const ints& contests) {
  auto t(p);
  for (auto& e : t) { e = contests[e]; }
  std::sort(std::begin(t), std::end(t));
  return is_strictly_monotonic(t); }

// This is the second most complicated bit of code. It calculates the
// combination following p in the ordered combination sequence. Combinations
// are ordered lexically by the sort of their elements, for example:
// {6, 1, 2} < {3, 1, 5} because {1, 2, 6} < {1, 3, 5}. Further, it enforces
// the constraint that each digit in the combination must be drawn from the
// contest in that position.
bool next_combination(ints& p, const ints& contests) {
  std::sort(std::begin(p), std::end(p));
  const auto mx = end_combination(p, contests.size() - 1);

  do {
    if (equal(p, mx)) { return false; }
    inc_monotonic_seq(p, contests.size() - 1);
  } while (!is_valid_combination(p, contests));

  // Sort in contest order: contest0, contest1, ...
  for (int i = 0; i < p.size(); ++i) {
    while (i != contests[p[i]]) {
      std::swap(p[i], p[contests[p[i]]]); } }

  return true;
}

int main() {
  const int N = 256; // number of contests
  const int M = 500; // number of most preferably ranked outcomes to display

  // This is the third most complicated bit of code. The following vector is
  // a map from priorities to contests. For example, the following 3 contest
  // win-tie-loss priorities {{0, 3, 6}, {1, 4, 7}, {2, 4, 8}} are stored as
  // {0, 1, 2, 0, 1, 2, 0, 1, 2}. This inversion is possible because the
  // contest outcome priorities are required to be disjoint since there is
  // a total ordering over the outcomes. Note, the highest priority is 0
  // (not 1 as in the specification).
  ints contests(3 * N); // map priorities to contests
  int c  = 0;
  for (auto& e : contests) { e = c % N; ++c; }

  // Highest priority combination.
  ints p(N);
  p = begin_combination(p, contests.size() - 1);

  int total = 1;
  do {
    // Finally, doing some sort of mapping here from priorities to strings,
    // as in the problem specification, is trivially done.
    std::copy(std::begin(p), std::end(p), std::ostream_iterator<int>(std::cout, " "));
    std::cout << "\n";
    if (M < ++total) { break; }
  } while(next_combination(p, contests));

  return 0;
}

几点说明:

  1. 如果你用N = 256和M = 500(就像它写的那样)运行它,你会发现它太慢了。我确信有一种聪明的方法可以避免我做的所有排序。
  2. 如果你想要M = 500,N不必非常大,N = 6就足够了(3 ^ 6 = 729)。如果N很大,你看到的是一个长的常量前缀/头部,后面是一个短的变化尾部。如果你反思一下,不难看出为什么会这样。