从集合中生成所有大小为 k 的子集

Posted

技术标签:

【中文标题】从集合中生成所有大小为 k 的子集【英文标题】:generate all subsets of size k from a set 【发布时间】:2010-12-29 15:51:24 【问题描述】:

我想从一个集合中生成所有大小为 k 的子集。

例如:-假设我有一组 6 个元素,我必须列出元素基数为 3 的所有子集。

我试图寻找解决方案,但那些是代码 sn-ps。 好久没写代码了,很难理解代码并围绕它构建可执行程序。

一个完整的 C 或 C++ 可执行程序会很有帮助。 希望使用递归的最佳解决方案。

【问题讨论】:

“请给我一个完整的程序”的请求在这里通常会遭到敌意。您应该展示您的工作,并准确说明您遇到的问题。 这个好像是写作业题的形式…… 提示:你将如何获得基数为 3 的所有子集?您不会只是随意刺伤它,对吗?记住这一点后,开始编写程序。 “递归”和“最优”几乎从不属于同一个句子 您查看了常见问题解答吗? ***.com/tags/algorithm/faq 【参考方案1】:

在下面找到一个工作代码

#include<iostream>
#include<string>
#include<list>

using namespace std;

void print( list<int> l)
    for(list<int>::iterator it=l.begin(); it!=l.end() ; ++it)
            cout << " " << *it;
    cout<<endl;


void subset(int arr[], int size, int left, int index, list<int> &l)
    if(left==0)
        print(l);
        return;
    
    for(int i=index; i<size;i++)
        l.push_back(arr[i]);
        subset(arr,size,left-1,i+1,l);
        l.pop_back();
    

     

int main()
    int array[5]=1,2,3,4,5;
    list<int> lt;   
    subset(array,5,3,0,lt);


    return 0;

【讨论】:

【参考方案2】:

(1&lt;&lt;nbits)-1初始化一个位数组,然后使用这个算法:

http://graphics.stanford.edu/~seander/bithacks.html#NextBitPermutation

对于大于最大整数大小的集合,您仍然可以将相同的算法应用于您自己的类型。

【讨论】:

你能提供一个用例吗?以防万一链接断开或什么? @Christian 检查这个:***.com/questions/8281951/…【参考方案3】:
#include <cstdio>
void g(int s[],int p,int k,int t[],int q=0,int r=0)

    if(q==k)
    
        for(int i=0;i<k;i++)
            printf("%d ",t[i]);
        printf("\n");
    
    else
    
        for(int i=r;i<p;i++)
        
            t[q]=s[i];
            g(s,p,k,t,q+1,i+1);
        
    


main()

    int s[]=1,2,3,4,5,t[5];
    g(s,5,3,t);

【讨论】:

@Muggen:实际上有两个。 @Muggen 我认为@John 上面的评论很好地说明了原因。 它给了我expected ‘;’, ‘,’ or ‘)’ before ‘=’ token 但别担心。由于操作标记了CC++,我认为他不在乎。 +1 哦。结果看起来是正确的。 如果k - q &gt; p - r,性能改进将立即返回。这样可以尽早消除不可能的配置。【参考方案4】:

这个问题可以用递归来解决。我们需要考虑以下递归情况。

    当前元素被选中。现在我们递归地选择 剩余集合中剩余的 k-1 个元素。(包含) 当前元素未被选中。现在我们递归地从剩余集合中选择 k 个元素。(排除)

以下是演示上述算法的 C++ 程序。

#include<iostream>
#include<vector>

using namespace std;    

void computeSubsets(vector<vector<int> > & results, vector<int> const& nums, vector<int> & curSet, int idx, int size) 
    
    if (idx == size) 
        results.push_back(curSet);
        return ;
    
    
    // Include
    curSet.push_back(nums[idx]);
    computeSubsets(results, nums, curSet, idx+1, size);
    curSet.pop_back();
    
    // Exclude
    computeSubsets(results, nums, curSet, idx+1, size);



vector<vector<int>> getSubsets(vector<int> const& nums) 
    vector<vector<int> > results;
    vector<int> temp;
    int size = nums.size();
    
    temp.reserve(size);
    results.reserve(1 << size);
    
    computeSubsets(results, nums, temp,  0, size);
    
    return results;
    


int main()

    vector<int> nums = 1, 2, 3;
    auto subsets = getSubsets(nums);
    for (auto const& subset : subsets) 
        for (auto const& x : subset) 
            cout << x << " ";
        
        cout << "\n";
    

    return 0;

【讨论】:

【参考方案5】:

最直观的算法确实会使用递归。当你有一个集合时,我们会假设你可以迭代它的所有元素。

如果我将 tail(e) 称为元素 e 之后的所有元素的集合。

所以我现在想要组合(s,k)

遍历 s 中的每个元素并获得 e :: combination(tail(e), k-1) 其中 :: 表示“连接到每个元素”

当然有时不会有这样的组合(你不在列表的末尾)。

您只需要一个主集合(一组集合)来添加您的组合和一种创建方式

所以假设我们在任何地方都没有全局变量,我们可以有类似的东西:

getCombinations( headset [in], tailset [in], count [in], output [append] )

headset 或 tailset 可能为空。 count 可以为 0,在这种情况下,我们将“headset”写入输出(基本条件),否则我们遍历 tailset 中的每个元素,将其(本地)添加到耳机,使 tailset(本地)成为我们元素的尾部,减去 1通过调用函数从计数和“递归”。

【讨论】:

【参考方案6】:

这是一些伪代码。您可以通过在执行过程中存储每个调用的值以及在递归调用之前检查调用值是否已经存在来减少相同的递归调用。

以下算法将包含除空集之外的所有子集。

list * subsets(string s, list * v)
    if(s.length() == 1)
        list.add(s);    
        return v;
    
    else
    
        list * temp = subsets(s[1 to length-1], v);     
        int length = temp->size();

        for(int i=0;i<length;i++)
            temp.add(s[0]+temp[i]);
        

        list.add(s[0]);
        return temp;
    

【讨论】:

【参考方案7】:
 #include <stdio.h>
 #define FIN "subsets.in"
 #define FOUT "subsets.out"
 #define MAXSIZE 100

 void performSubsets(int n, int k)

 int i, j, s, v[ MAXSIZE ]; 

 freopen(FOUT, "w", stdout);


 memset(v, 0, sizeof( v ));

 do 

    v[ n - 1 ]++;

    for(i = n - 1; i >= 1; i--) 

        if(v[ i ] > 1) 

           v[ i ] -= 2;

           v[ i - 1 ] += 1;  
        
    

    s = 0;

    for(j = 0; j < n; j++) s += v[j];

    for(j = 0; j < n; j++) 

        if( v[ j ] && s == k) printf("%d ", (j + 1));

   if(s == k) printf("\n");

  while(s < n);     

fclose( stdout );      



int main() 

    int n, k; 

    freopen(FIN, "r", stdin);

    //read n and size k
    scanf("%d %d", &n, &k);

fclose( stdin );

performSubsets(n,k);


这个问题可以使用非递归算法来解决。

【讨论】:

关闭 stdin/stdout 是一个不好的习惯。【参考方案8】:

我的旧代码给出以下结果:

111000
110100
110010
110001
101100
101010
101001
100110
100101
100011
011100
011010
011001
010110
010101
010011
001110
001101
001011
000111

足够优化:

#include <iostream>

int firstPermutation(int n, int k) 
    return ((1 << k) - 1) << (n - k);


int shiftLast1(int a) 
    return (a - 1) ^ ((a^(a - 1)) >> 2);


int add1AfterLast1(int a) 
    return a | (((a^(a - 1)) + 1) >> 2);


int nextPermutation(int a) 
    if ((a & (a + 1)) == 0) 
        return 0;
    

    if (a & 1) 
        return add1AfterLast1(nextPermutation(a >> 1) << 1);
    
    else 
        return shiftLast1(a);
    


int main() 
    int n = 6;
    int k = 3;
    int a = firstPermutation(n, k);
    do 
        for (int i = 0; i < n; i++) 
            std::cout << ((a >> (n - 1 - i)) & 1);
        
        std::cout << std::endl;
     while ((a = nextPermutation(a)));

【讨论】:

【参考方案9】:

由于 user843453 的帖子的编辑队列已满,我将创建一个新帖子并解释其工作原理。我还对变量名称进行了一些更改,以便更清楚算法在做什么。这是代码:

#include<iostream>
#include<string>
#include<list>

using namespace std;

void print( list<int> l)
    for(list<int>::iterator it=l.begin(); it!=l.end() ; ++it)
            cout << " " << *it;
    cout<<endl;


void subset(int arr[], int size, int choices_left, int index, list<int> &l)
    if(choices_left==0)
        print(l);
        return;
    
    for(int i=index; i<size;i++)
        l.push_back(arr[i]);
        subset(arr,size,choices_left-1,i+1,l);
        l.pop_back();
    

     

int main()
    int array[5]=1,2,3,4,5;
    list<int> lt;   
    subset(array,5,3,0,lt);


    return 0;

要了解其工作原理,我们应该了解该示例调用中发生的情况:

A = [a0, a1, ..., a4]

ss(A, size = 5, choices_left = 3, index = 0, [])
|
|   i = 0
|   
|   [] ~> [a0]
|   
|   ss(A, size = 5, choices_left = 2, index = 1, [a0])
|   |
|   |   i = 1
|   |
|   |   [a0] ~> [a0, a1]
|   |   
|   |   ss(A, size = 5, choices_left = 1, index = 2, [a0, a1])
|   |   |
|   |   |   i = 2
|   |   |
|   |   |   [a0, a1] ~> [a0, a1, a2]
|   |   |
|   |   |   ss(A, size = 5, choices_left = 0, index = 3, [a0, a1, a2])
|   |   |   |   [a0, a1, a2] is printed
|   |   |   -------------------- POPPED ---------------------
|   |   |
|   |   |   [a0, a1, a2] ~> [a0, a1]
|   |   |
|   |   |   i = 3
|   |   A1
|   |   |   [a0, a1] ~> [a0, a1, a3]
|   |   |
|   |   |   ss(A, size = 5, choices_left = 0, index = 4, [a0, a1, a3])
|   |   |   |   [a0, a1, a3] is printed
|   |   |   -------------------- POPPED ---------------------
|   |   |
|   |   |   [a0, a1, a3] ~> [a0, a1]
C   |   |
|   |   |   i = 4
|   |   |
|   |   |   [a0, a1] ~> [a0, a1, a4]
|   |   |
|   |   |   ss(A, size = 5, choices_left = 0, index = 5, [a0, a1, a4])
|   |   |   |   [a0, a1, a4] is printed
|   |   |   -------------------- POPPED ---------------------
|   |   |
|   |   |   [a0, a1, a4] ~> [a0, a1]
|   |   |
|   |   -------------------- POPPED ---------------------
|   |       
|   B   [a0, a1] ~> [a0]
|   |   
|   |   i = 2
|   |
|   |   [a0] ~> [a0, a2]
|   |   
|   |   ss(A, size = 5, choices_left = 1, index = 3, [a0, a2])
|   |   |
|   |   |           ...
|   |   |
|   |   |   [a0, a2, a3] is printed
|   |   |
|   |   A2           ...
|   |   |
|   |   |   [a0, a2, a4] is printed
|   |   |
|   |   |           ...
|   |   |
|   |   -------------------- POPPED ---------------------
|   |       
|   |   [a0, a2] ~> [a0]
|   |
|   |            ...
|   |
|   -------------------- POPPED ---------------------
|
|   [a0] ~> []
|
|   i = 1
|   
|   [] ~> [a1]
|   
|   ...
-------------------- POPPED ---------------------

理解这一点的一个好方法是注意到在任何可能的大小为 3 的子集中,我们知道原始列表 A 的任何元素要么是此列表的一部分,要么不是列表的一部分。

这个想法对应于将元素推到后面的代码部分,然后执行其他操作,然后删除该元素。

做“其他事情”的部分是最重要的部分,我们可以通过查看递归中的A1部分来弄清楚它的作用,在递归的这个深度,我们得到列表[a0, a1]并负责为我们的子集生成最后一个元素,或者我们可以将其改写为生成大小为 1 的有效子集,它仅使用列表中 index 之后的元素。

我们必须从index 开始的原因是为了不产生重复的情况。要完全理解我的意思,你需要了解这个算法的从左到右的过程。

在它要求的整个算法的第一次迭代中:

“假设该元素在列表中,使用剩余元素生成所有可能的子集,现在假设该元素不在列表中,使用剩余元素生成所有可能的子集”

一般来说,它的核心是“给定这个大小为 n - k 的有效子集,使用我尚未考虑使用 k 个元素的元素生成一个有效子集”。这就是为什么我们只需要考虑从index 开始的元素。

具体来说,我们可以看到递归级别 B 表示“假设 a0 将成为我们子集的一部分,让我们找到大小为 2 的子集,然后将它们连接到列表的末尾 [a0]

在该递归级别的下一个调用会说“假设 a0 不是子集的一部分,但 a1 是子集的一部分,生成大小为 2 的子集,然后将它们连接到列表[a1]

最后再来一次,它会说“假设a0a1 不是子集的一部分,但a2 是子集的一部分......


备注

正如其他解决方案指出的那样,您可以使用二进制数来生成子序列,因为如果我们有列表A = [a0, a1, a2, a3, a4],我们可以看到二进制数 1 0 1 1 1 可以是一个指定是否该索引处的元素将成为该子集的一部分,因此它将生成子集[a0, a2, a3, a4]

然后完成两个不同算法之间的连接,你可以看到这个算法将通过这样做来生成二进制:

    for(int i=index; i<size;i++)
        l.push_back(arr[i]); // THIS IS FLIPPING BIT AT INDEX I to a 1
        // THE BELOW GENERATES ALL POSSIBLE BINARY NUMBERS SO THAT WHEN ANY ONE IS APPENDED TO OUR CURRENT BINARY NUMBER IT'S LENGTH IS N
        subset(arr,size,choices_left-1,i+1,l); 
        l.pop_back(); // FLIPPING THE BIT AT INDEX I BACK TO 0
    

【讨论】:

【参考方案10】:

这是一个迭代解决方案:

#include <stdio.h>
#include <stdlib.h>
void printer(int locations[],int a[],int r)

    int i;
    for(i=0; i<r; i++)
    
        int x=locations[i];
        printf("%d ",a[x]);
    
    printf("\n");

int main()

    int a[100000];
    int locations[1000];
    int i,n,r;
    printf("Enter N: ");
    scanf("%d",&n);
    printf("Enter K: ");
    scanf("%d",&r);
    for(i=0;i<n;i++)
        scanf("%d",&a[i]);
    for(i=0; i<r; i++)
        locations[i]=i;
    printer(locations,a,r);
    while(locations[0]<n-r)
    
        for(i=r-1; i>0; i--)
        
            if(locations[i-1]<n-r+i-1)
            
                if(locations[i]<n-r+i)
                
                    locations[i]++;
                    printer(locations,a,r);
                    break;
                
                else
                
                    locations[i-1]++;
                    int j;
                    for(j=i; j<r; j++)
                    
                        locations[j]=locations[j-1]+1;
                    
                    printer(locations,a,r);
                    break;
                
            
        
    
    return 0;

【讨论】:

以上是关于从集合中生成所有大小为 k 的子集的主要内容,如果未能解决你的问题,请参考以下文章

如何生成集合的所有唯一子集?

通过从每个集合中仅选择一个值来选择 k 个大小的子集

在Matlab中生成包含给定集合中至少一个元素的所有组合

是否有一种快速算法可以将集合的所有分区生成为大小为 2 的子集(和一个大小为 1 的子集)?

698. 划分为k个相等的子集(Python)

大小为 K 的子集的乘积之和