1. 程式人生 > >暴力求解-子集生成

暴力求解-子集生成

本篇介紹子集生成演算法:給定一個集合,列舉所有可能的子集。為了簡單起見,本節討論的集合彙總沒有重複元素。

輸入:整數n。

輸出:對於集合{0,1,.....n-1},列舉所有可能的子集。

執行結果:

增量構造法

第一種思路是一次選出一個元素放到集合中。

void print_subset_add(int n,int *A,int cnt)
{
    //增量構造法
    //每次都選一個元素放入集合中
    int i;
    for(i=0;i<cnt;i++) //列印當前集合
        printf("%d ",A[i]);
    printf("\n");
    int s=cnt?A[cnt-1]+1:0; //確定當期集合的最小值
    for(i=s;i<n;i++)
    {
        A[cnt]=i;
        print_subset_add(n,A,cnt+1); //遞迴構造子集
    }
}

和前面不同,由於A中的元素個數不確定,每次遞迴呼叫都要輸出當前集合。另外,遞迴邊界也不需要顯示確定-如果無法繼續新增元素,自然就不會再遞迴了。

上面的程式碼用到了定序的技巧:規定集合A中所有元素的編號從小到大排列,就不會把集合{1,2}按照{1,2}和{2,1}輸出兩次了。

在列舉子集的增量法巨集中,需要使用定序的技巧,避免同一個集合列舉兩次。

位向量法

第二種思路是構造一個位向量B[i],而不是直接構造子集A本身,其中B[i]=1,當且僅當i在子集A中,遞迴實現如下:

void print_subset_B(int n,int *B,int cnt)
{
    //位向量法
    //構造一個位向量B[i] 而不是直接構造子集A本身,其中B[i]=1當且僅當i在
    //子集A中
    if(cnt==n)
    {
        for(int i=0;i<cnt;i++) //列印當前集合
            if(B[i])
                printf("%d ",i);
        printf("\n");
    }
    else
    {
        B[cnt]=1;
        print_subset_B(n,B,cnt+1);
        B[cnt]=0;
        print_subset_B(n,B,cnt+1);
    }
}

必須當所有元素是否選擇全部確定完畢後才是一個完整的子集,因此仍然像以前那樣當 if(cur ==n )成立時才輸出。

二進位制法

另外,還可以用二進位制來表示{0,1,2,....,n-1}的子集S:從右往左第i位(各位從0開始編號)表示元素i是否在集合S中。

為了處理方便,最右邊的位總是對應元素9,而不是元素1.

可以用二進位制表示子集,其中從右往左第i位(從0開始編號)表示元素i是否在集合中(1表示“在”,0表示“不在”)

此時僅表示出集合是不夠的,還需要對集合進行操作。幸運的是,常見的集合運算都可以用位運算子簡單實現。

當用二進位制表示子集時,位運算中的按位與,或,異或對應集合的交、並和對稱差。

這樣,不難用下面的程式輸出子集S對應的各個元素:

void print_subset_binary(int n,int s)
{
    //二進位制法
    for(int i=0;i<n;i++) //列印子集S
        if(s&(1<<i))
           printf("%d ",i);
    printf("\n");
}

而列舉子集和列舉整數一樣簡單:

for(int i=0;i<(1<<n);i++) //列舉各子集對應的編碼0,1,....2^n-1;
    print_subset_binary(n,i);

從程式碼量看,列舉子集的最簡單方法是二進位制法。