【问题标题】:Finding all subsets of specified size查找指定大小的所有子集
【发布时间】:2024-01-09 10:18:01
【问题描述】:

这两天我一直在摸不着头脑,我想不出解决办法。我正在寻找的是一个函数f(s, n),它返回一个包含s 的所有子集的集合,其中每个子集的长度为n

演示:

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++)具有内置库支持。是的,从数学上讲,这可能是一种递归算法。如何以某种语言在实践中最好地实现它是另一回事。
  • 这似乎很相似的问题:*.com/questions/61592209/…
  • 我认为他们正在寻找长度组合n,因为他们的结果是无序元组@Lundin
  • @Lundin 和达米安。如果我没记错的话,排列是有序的,哪些不是。
  • 无论如何,它都是相同的算法。集合的优点是您没有重复项,而不是用于查找字符串的所有子字符串的算法。同样在实践中,我不太明白如何以可行的方式实现一个集合而不对其进行排序,或者添加/删除将非常低效。例如,C++std::set 带有强制排序顺序,您必须提前定义一种方法来比较集合中的项目,以便使用容器类。

标签: algorithm set subset


【解决方案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]

我们可以通过条件检查跳过这个分支。 正确实现这些基本情况,将算法的时间复杂度降低到O(len(input_set) choose k),这是一个硬性限制,因为这是子集的数量。

【讨论】:

    【解决方案2】:
    subseqs 0 _      = [[]]
    subseqs k []     = []
    subseqs k (x:xs) = map (x:) (subseqs (k-1) xs) ++ subseqs k xs
    

    Live demo

    该函数在给定序列中查找(非负)长度为 k 的子序列。分三种情况:

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

    该算法或多或少是这些注释到 Haskell 的直译。符号备忘单:

    • [] 一个空列表
    • [w] 具有单个元素的列表 w
    • x:xs 头部为x 尾部为xs 的列表
    • (x:) 一个将x 粘贴在任何列表前面的函数
    • ++ 列表连接
    • f a b c 一个函数 f 应用于参数 a bc

    【讨论】:

    • FP 对我来说总是很神奇:D
    【解决方案3】:

    我们称n 为数组的大小,k 为子数组中要输出的元素数。

    让我们考虑数组A的第一个元素A[0]
    如果把这个元素放在子集中,问题就变成了(n-1, k-1)类似的问题。
    如果没有,就会变成(n-1, k) 问题。

    这可以简单地在递归函数中实现。

    我们只需要注意处理极端情况k == 0k > n

    在此过程中,我们还需要跟踪:

    • n:要考虑的 A 的剩余元素个数

    • k:当前子集中剩余的元素个数

    • index:要考虑的 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 实现这个,但我失败了。如果您能帮助我,我将不胜感激。 *.com/q/70976317/6699433
    • @klutt 我不懂java,但我会看看。
    • 在这种情况下不要让自己筋疲力尽