找出指定大小的所有子集

3

我已经对这个问题思考了两天,但是还是没有想出一个解决方案。我需要一个函数f(s,n),它返回包含所有长度为ns子集的集合。

演示:

s={a, b, c, d}

f(s, 4)
{{a, b, c, d}}

f(s, 3) 
{{a, b, c}, {a, b, d}, {a, c, d}, {b, c, d}}

f(s, 2)
{{a, b}, {a, c}, {a, d}, {b, c}, {b, d}, {c, d}}

f(s, 1)
{{a}, {b}, {c}, {d}}

我觉得递归是解决这个问题的方法。我一直在尝试类似于以下内容的东西:
f(S, n):
   for s in S:
       t = f( S-{s}, n-1 ) 
       ...

但这似乎并没有起到作用。我注意到len(f(s,n))似乎是二项式系数bin(len(s), n)。我想这可能可以在某种程度上利用。

请问我能为您提供帮助吗?


搜索术语将是“查找子集排列”,类似于这样的东西。一些语言,如C ++,具有内置库支持。是的,从数学上讲,这可以是一个递归算法。如何在特定语言中最好地实现它是另一回事。 - Lundin
这似乎是一个相似的问题:https://stackoverflow.com/questions/61592209/how-can-i-generate-all-permutations-of-length-n-from-a-set-of-k-elements/61599670#61599670 - Damien
我认为他们正在寻找长度为n的组合,因为他们的结果是无序元组@Lundin - user1984
@Lundin 和 Damien。如果我没记错的话,排列是有序的,而集合则不是。 - klutt
算法是一样的。集合的优点在于没有重复项,而不是例如查找字符串的所有子串的算法。实际上,我不太明白如何在不排序的情况下以可行的方式实现一个集合,否则添加/删除将非常低效。例如,C++的std::set带有强制排序顺序,并且您必须提前定义一种比较集合项的方法才能使用容器类。 - Lundin
@klutt 你是对的,集合是无序的。实际上,这意味着你基本上可以使用相同类型的算法,只不过它们更简单。 - Damien
4个回答

1

解决这个问题的一种方法是通过回溯。以下是伪代码中可能的算法:

def backtrack(input_set, idx, partial_res, res, n):
  if len(partial_res == n):
    res.append(partial_res[:])
    return
  
  for i in range(idx, len(input_set)):
    partial_res.append(input_set[i])
    backtrack(input_set, idx+1, partial_res, res, n) # path with input_set[i]
    partial_res.pop()
    backtrack(input_set, idx+1, partial_res, res, n) # path without input_set[i]

这种方法的时间复杂度为O(2^len(input_set)),因为我们在input_set的每个元素上都进行了2个分支,无论路径是否通向有效结果。空间复杂度为O(len(input_set) choose n),因为这是您在问题中正确指出的有效子集数。
现在,有一种优化上述算法的方法,将时间复杂度降低到O(len(input_set) choose n),通过修剪递归树以仅保留可以导致有效结果的路径。
如果n - len(partial_res) < len(input_set) - idx + 1,我们确定即使在input_set[idx:]中取每个剩余元素,我们仍然短缺至少一个达到n的元素。因此,我们可以将其作为基本情况返回并进行修剪。
此外,如果n - len(partial_res) == len(input_set) - idx + 1,这意味着我们需要input_set[idx:]中的每个元素才能得到所需的长度为n的结果。因此,我们不能跳过任何元素,因此递归调用的第二个分支变得多余。
backtrack(input_set, idx+1, partial_res, res, n) # path without input_set[i]

Great! I am here to assist you in translating text from one language to another. Let me know what you need translated and into which language, and I will do my best to help you out.

1
让我们将数组的大小称为n,子数组中要放置的元素数量称为k。
考虑数组A的第一个元素A [0]。如果将此元素放入子集中,则问题变为类似的(n-1,k-1)问题。如果不这样做,则变成(n-1,k)问题。
这可以在递归函数中简单实现。
我们只需注意处理极端情况k == 0或k> n。
在过程中,我们还必须跟踪以下内容:
n:要考虑的A的剩余元素数
k:当前子集中剩余要放置的元素数
索引:下一个要考虑的A元素的索引
current_subset数组,用于记忆已选择的元素。
以下是C ++中说明算法的简单代码。
输出
对于5个元素和大小为3的子集:
3 4 5
2 4 5
2 3 5
2 3 4
1 4 5
1 3 5
1 3 4
1 2 5
1 2 4
1 2 3

#include    <iostream>
#include    <vector>

void print (const std::vector<std::vector<int>>& subsets) {
    for (auto &v: subsets) {
        for (auto &x: v) {
            std::cout << x << " ";
        }
        std::cout << "\n";
    }
}
//  n: number of remaining elements of A to consider
//  k: number of elements that remain to be put in the current subset
//  index: index of next element of A to consider

void Get_subset_rec (std::vector<std::vector<int>>& subsets, int n, int k, int index, std::vector<int>& A, std::vector<int>& current_subset) {
    if (n < k) return;   
    if (k == 0) {
        subsets.push_back (current_subset);
        return;
    }  
    Get_subset_rec (subsets, n-1, k, index+1, A, current_subset);
    current_subset.push_back(A[index]);
    Get_subset_rec (subsets, n-1, k-1, index+1, A, current_subset);
    current_subset.pop_back();         // remove last element
    return;
}

void Get_subset (std::vector<std::vector<int>>& subsets, int subset_length, std::vector<int>& A) {
    std::vector<int> current_subset;
    Get_subset_rec (subsets, A.size(), subset_length, 0, A, current_subset);
}

int main () {
    int subset_length = 3;     // subset size
    std::vector A = {1, 2, 3, 4, 5};
    int size = A.size();
    std::vector<std::vector<int>> subsets;

    Get_subset (subsets, subset_length, A);
    std::cout << subsets.size() << "\n";
    print (subsets);
}

Live demo


我尝试在Java中实现这个,但是失败了。如果您能帮助我解决问题,我将不胜感激。https://dev59.com/pLL3oIgBc1ULPQZFSVr6 - klutt
1
@klutt 我不懂Java,但我会看一下。 - Damien
不要在那种情况下过度劳累。 - klutt

1
subseqs 0 _      = [[]]
subseqs k []     = []
subseqs k (x:xs) = map (x:) (subseqs (k-1) xs) ++ subseqs k xs

实时演示

该函数在给定序列中寻找长度为k的子序列。有三种情况:

  1. 如果长度为0:任何序列中都有一个空的子序列。
  2. 否则,如果序列为空:不存在任何长度为k的子序列。
  3. 否则,存在一个以x开始并继续使用xs的非空序列和一个正整数长度k。我们的所有子序列有两种类型:包含x的那些(它们是长度为k-1xs的子序列,每个子序列的前面都有x),以及不包含x的那些(它们只是长度为kxs的子序列)。

该算法是将这些笔记逐字翻译成Haskell的结果。符号速查表:

  • [] 空列表
  • [w] 一个带有单个元素w的列表
  • x:xs 一个头部为x,尾部为xs的列表
  • (x:) 一个将x添加到任何列表前面的函数
  • ++ 列表连接
  • f a b c 应用于参数abc的函数f

FP 对我来说总是像魔法一样 :D - klutt

0
这是一个非递归的Python函数,它接受一个列表superset并返回一个生成器,该生成器产生所有大小为k的子集。
def subsets_k(superset, k):
  if k > len(superset):
    return
  if k == 0:
    yield []
    return

  indices = list(range(k))
  while True:
    yield [superset[i] for i in indices]

    i = k - 1
    while indices[i] == len(superset) - k + i:
      i -= 1
      if i == -1:
        return
    
    indices[i] += 1
    for j in range(i + 1, k):
      indices[j] = indices[i] + j - i

测试一下:

for s in subsets_k(['a', 'b', 'c', 'd', 'e'], 3):
  print(s)

输出:

['a', 'b', 'c']
['a', 'b', 'd']
['a', 'b', 'e']
['a', 'c', 'd']
['a', 'c', 'e']
['a', 'd', 'e']
['b', 'c', 'd']
['b', 'c', 'e']
['b', 'd', 'e']
['c', 'd', 'e']

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接