【问题标题】:Finding all combinations, using at least n items from before index j查找所有组合,使用索引 j 之前的至少 n 个项目
【发布时间】:2017-04-18 07:29:07
【问题描述】:

算法需求

我正在尝试编写一个算法来查找数组中所有 k 长度的项目组合,但它还必须在索引 j 之前使用 n 项目,因为我们有很多 (n,j) 对渴望。

示例

  1. 使用索引二之前的至少一项(又名restrictions = {{1,2}})从{1,2,3,4} 中查找所有两项组合。这应该导致{{1,2},{1,3},{1,4},{2,3},{2,4}}

  2. 使用索引 2 之前的至少一项和索引 4 之前的两项,即 restrictions = {{1,2},{2,4}},查找来自 {1,2,3,4,5,6,7} 的所有三个项目组合。

进展

我已经找到了从一堆集合中选择一个项目的所有组合。

void add_combinations_to(vector<int> prefix, vector<vector<int>>::iterator start, 
                         vector<vector<int>>::iterator stop, vector<vector<int>>& ret) {
    if(start == stop) {ret.push_back(prefix); return;}
    for(auto v : *start) {
        auto next = prefix;
        next.push_back(v);
        add_combinations_to(next, start + 1, stop, ret);
    }
};

int main() {
    vector<vector<int>> v{{1,2},{3,4}};
    vector<vector<int>> ret;
    vector<int> init{};
    add_combinations_to(init, v.begin(), v.end(), ret);
    // ret now contains {{1,3},{1,4},{2,3},{2,4}}
}

我觉得有一种方法可以扩展它。现在我需要退后一步,但建议会很好。在大多数情况下,这只是一个

【问题讨论】:

  • 看来限制可以简化。对于您的示例 {1,2,3,4,5,6,7},
  • 我会使用std::next_permutation
  • 看来限制可以简化。对于您的示例数据集 ={1,2,3,4,5,6,7},限制 = {{1,2},{2,4}}。限制可以是{{1,2},{(2-1),4}},因为{1,2}假设前2个中至少有一个被使用,所以对于{2,4}只需要使用一项。
  • 例2的解是{{1,2,3}, {1,2,4},{1,3,4}, {2,3,4}} ?跨度>

标签: c++ algorithm combinations subset


【解决方案1】:

与许多序列枚举问题一样,这个问题产生于标准字典枚举算法(引自Algorithm for "consolidating" N items into K):

  • 从字典序上可能最小的序列开始
  • 尽可能:
    • 一个。向后扫描以找到最后一个可以增加的元素。 (“可能”意味着增加该元素仍会导致某些有效序列的前缀。)
    • 乙。将该元素增加到下一个可能的最大值
    • c。用尽可能小的后缀填写序列的其余部分。

如果没有其他限制,长度-j 序列p == p<sub>0</sub>, p<sub>1</sub>, …, p<sub>j−1</sub> 可以是长度-k 组合的前缀n 的东西iff k−j &lt; n−p<sub>j−1</sub>。我们可以将其重写为p<sub>j−1</sub> &lt; n − k &amp;plus; j)

因此,以下简单函数将完成上述通用算法的步骤 a 和 b,用于无限制的 k,n 组合:

/* Finds the (lexicographically) next prefix of a k-combination from n
 * values, and returns the length of that prefix.
 * If there is no possible next prefix, returns 0.
 * Note: k is implicit in the length of the combination
 */
size_t nextPrefix(std::vector<size_t>& comb, size_t n) {
  size_t k = comb.size();
  for (size_t j = k; j;) {
    --j;
    if (comb[j] < n - k + j) {
      ++comb[j];
      return j + 1;
    }
  }
  return 0;
}

要继续讨论实际问题,我们注意到“组合包括第一个 k<sub>i</sub> 的第一个 n<sub>i</sub> 值”形式的任何限制将最终成为完全相同的测试,因为该限制可以改写为“k<sub>i</sub>-length 前缀是 n<sub>i</sub> 值的组合。”

因此,我们可以将上述函数中的n 参数替换为一对向量(其中最后一对正好是&lt;k, n&gt;):

size_t nextPrefix(std::vector<size_t>& comb,
                   std::vector<std::pair<size_t, size_t>> const& restrictions) {
  for (size_t j = comb.size(); j;) {
    --j;
    /* Test all applicable conditions */
    if (std::all_of(restrictions.begin(), restrictions.end(), 
                    [&j, &comb](std::pair<size_t, size_t> const& rest) {
                      return j >= rest.first
                             or comb[j] < rest.second - rest.first + j;})) {
        ++comb[j];
        return j + 1;
    } 
  }
  return 0;
}

(这显然不是最优的。我们可以只计算一个简单的最大值向量,然后根据该最大值测试元素,而不是检查每个元素的所有限制。)

要真正生成序列,我们需要能够填写后缀(通用算法的最后一步)并构造循环:

void firstSuffix(std::vector<size_t>& comb, size_t pfx_length) {
  size_t val = pfx_length ? comb[pfx_length - 1] + 1 : 0;
  for (auto it = comb.begin() + pfx_length; it != comb.end(); ++it)
    *it = val++;
}

然后我们可以编写循环:

int main() {
  std::vector<std::pair<size_t, size_t>> restrictions = {{1, 2}, {2, 4}, {3, 7}};
  size_t k = std::max_element(restrictions.begin(), restrictions.end())->first;
  if (k == 0) return 0; /* Empty set */
  std::vector<size_t> comb(k);
  std::size_t pfx = 0;
  do {
    firstSuffix(comb, pfx);
    for (auto const& val : comb) std::cout << std::setw(3) << val;
    std::cout << '\n';
  } while (pfx = nextPrefix(comb, restrictions));
  return 0;
}

(live on coliru)

【讨论】:

  • 太棒了!感谢您的深入回答和所有解释。它有助于了解导致良好解决方案的背景主题。当然,示例代码也很棒。
猜你喜欢
  • 1970-01-01
  • 2017-03-15
  • 1970-01-01
  • 1970-01-01
  • 2012-01-10
  • 2018-06-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多