1. 程式人生 > >給定n個元素集合,求k個元素的組合數目

給定n個元素集合,求k個元素的組合數目

本篇摘要

本篇介紹一個非常給力的求組合的演算法!上一篇“c_c++刁鑽問題各個擊破之位運算及其例項(2)”介紹了6個比較複雜的位操作,但是沒有給出任何應用例項,本篇就之前談到的位操作進行應用,其主要內容是用位操作來實現求組合

引例

先來看一道題目,這個題目是理解利用位操作求組合的關鍵。。英文原題就不貼了,我用中文描述一下吧:給定一個正整數N,求最小的、比N大的正整數M,使得M與N的二進位制表示中有相同數目的1。

上面的題目描述或許有點拗口,舉個例子把,假如給定的N為78,其二進位制表示為1001110,包含4個1,那麼最小的比N大的並且二進位制表示中只包含4個1的數是83,其二進位制是1010011,因此83就是答案。那麼如何求解這個問題呢?

(2) 非常給力的方法

即將要介紹的方法到底有多給力呢?它神奇到只用如下幾行程式碼(事實上可以合併為一行程式碼)就能實現上述所有程式碼的功能,這幾行程式碼是:

  1. [cpp] view plaincopyprint?  
  2. int NextN(int N)    
  3. {    
  4.     int x = N&(-N);          
  5.     int t = N+x;    
  6.     int ans = t | ((N^t)/x)>>2;    
  7.     return ans;    
  8. }    

 看到了不,這就是所有程式碼,如果省去臨時變數,它就只包含一行程式碼,但是為了後面講述方便,我將它寫成三行程式碼。它的強大之處並非只是程式碼少,如你所見,它無需呼叫count1Bits函式(效率!

),也沒有前面列舉法中GetNextN函式中的while迴圈(效率!)。是的,我們這裡的NextN函式比之前的GetNextN函式效率要高很多,它的時間複雜度是O(1)!

如果您是大牛,請不要笑話我在這裡的大驚小怪,如果你跟兩三天前的我一樣不懂這幾句程式碼的話,那麼請往下看,我將嘗試著把我的理解詳細的表述出來,力求細緻,簡單,易懂。

以N=78為例(其二進位制表示為1001110),我們的任務是求得最小的比N大,二進位制表示中1的個數與N相同的數:83(其二進位制表示為1010011)。首先我們要總結出來從78變成83的規律,為了方便,將78和83的二進位制寫成豎式形式:

78:1 0 0 1 1 1 0

83:1 0 1 0 0 1 1

可以看出,為了得到83,我們只需要對N(78)的二進位制中最右邊的連續的1位串(加粗標紅)進行操作!其過程是:將連續的1位串中最左邊的1向左“移動”一位,其他的1位串“移動”到最右邊!這即保證了二進位制表示中1的個數不變,又保證了新得到的數比原來的數大,並且是最小的。其過程可以用如下圖示表示:

在上面的描述中我用引號把兩個“移動”引起來了,原因是,具體實現時,我們並不是對這些二進位制位進行移動,而是通過位操作來達到同樣的目的,而這些位操作就是本問題的關鍵。接下來我將分析前面那個“非常給力的程式碼”看看它是如何用位操作來實現對這些位的“移動”的。

首先來看語句int x = N&(-N);它的功能是找到N(即78)的二進位制表示中最右邊的1(這個1必定是N的二進位制表示中最右邊的連續的1位串的開始)。該過程圖示如下:

接下來看看int t = N+x;該語句實現了“將連續的1位串中最左邊的1向左“移動”一位”的功能,當然它帶來了副作用:使得連續的1位串中其他的1丟失了!其過程如下:

       最後的任務就是要將上面丟失的1補上,並放到最右邊,這就是語句int ans = t | ((N^t)/x)>>2;的功能。首先,要知道需要補多少個1,通過分析可以知道需要補上的1的個數等於N的二進位制表示中最右邊的連續的1位串中1的個數減1,然而如果通過位操作來求得呢?這就是N^t的功能了,如下圖所示,N^t的二進位制表示只包含1個連續的1串,並且1的個數正好等於N的二進位制表示中最右邊的連續的1位串中1的個數加1:

由上面的分析可知,N^t中的1的個數實際上比我們需要補的1的個數多2!這就使得我們可以通過N^t求得需要補的1的個數,接下來的任務就是如何補上這些1了。

進步一分析得知,N^t的二進位制表示中最低位的1正好與x中那個1對應,因此我們就可以通過(N^t)/x將這些1全部移到最右邊了,然而此時1的個數比我們要補的個數多了2,沒關係我們在把結果右移2位就可以了,也就是((N^t)/x)>>2。如此一來我們求得了要補的1的個數和其位置。本段的描述可以用下圖形象地表示:

最後我們只需要用t | ((N^t)/x)>>2;就能得到所求之數了!其過程如下圖:

以上就是我對這個非常給力的程式碼的分析。短短3句程式碼(省去中間變數的話,就一句程式碼)居然包含了如此之多的東西,這就是位運算的強大之處,也是位運算的難學之處。本人以前也很少關注位運算,像這樣給力的程式碼我是寫不出來的,因此我也只能按照如上步驟那麼去讀懂這個程式碼。至此,本篇的引例算是完成了,不可思議吧,為了求組合,我居然用了這麼多篇幅來講一個引例,這會不會本末倒置啊?我自信不會的,因為有了這個引例,下面求組合就太easy了。

本篇主題:利用位操作求組合

組合就是從N各物件中選取m個物件,問有多少種選法,並且要求輸出每次的選法。比如給定1,2,3,4四個數,從中選擇2個的選法有:{1,2},{1,3},{1,4},{2,3},{2,4},{3,4}共6種選法。當然求組合的方法非常之多,我這裡只介紹如何利用位操作來求,其思路是:用2進位制bit位來標識某個物件是否被選中,1代表選中,0代表沒選中。比如前面的例子的組合可以用下圖來表示(最低位為1表示選中第一物件,以此類推)。

根據上面的分析,我們可以用一個包含N個bit位的數C來求N個物件中選取m個物件的組合:首先讓C的最低m位全部為1(對應到從N個物件中選擇前m個物件的組合情況),然後用我們引例的方法求出最小的比C大並且二進位制表示中包含的1的個數與C相同的數K,就能求得下一個組合情況。其流程如下:

1、  初始化C=(1<<m)-1;(選擇N個物件中的前m個作為第一個組合);

2、  根據C的二進位制表示輸出其所對應的組合;

3、  呼叫C=NextN(C);

4、  通過判斷C是否小於等於(1<<N)-(1<<(N-m))來確定是不是輸出了所有的組合(注意,當C==(1<<N)-(1<<(N-m))時,C就對應著從N個物件中選擇後m個物件的組合情況,也就是最後一個組合),如果C小於等於(1<<N)-(1<<(N-m)),則轉2,否則轉5;

5、  結束(已經輸出所有組合)。

上面流程中的關鍵部分我都標粗了,如果對該流程有疑問,可以與我聯絡([email protected])。下面我將根據上面的流程給出程式碼:

  1. #include <stdio.h>  
  2. #include"iostream"  
  3. usingnamespace std;    
  4. //定義包含4個元素的集合   
  5. char set[] ={'a','b','c','d','e','f','g','h','i'};    
  6. //根據C的二進位制表示輸出一個組合   
  7. void print(char* set,int C)    
  8. {    
  9.     int i = 0;    
  10.     int k;    
  11.     while((k=1<<i)<=C)    
  12.     {//迴圈測試每個bit是否為1   
  13.         if((C&k)!=0)    
  14.         {    
  15.             cout<<set[i];    
  16.         }    
  17.         i++;    
  18.     }    
  19. }     
  20. //這個NextN跟之前我們討論的是一樣的,只不過省去了臨時變數   
  21. int NextN(int N)    
  22. {    
  23.     return (N+(N&(-N))) | ((N^(N+(N&(-N))))/(N&(-N)))>>2;    
  24. }     
  25. //求從set中前N個元素 中選擇m個的組合   
  26. void Combination(char* set,int N,int m)    
  27. {    
  28.     int C = (1<<m)-1;    
  29.     while(C<=((1<<N)-(1<<(N-m))))    
  30.     {    
  31.         print(set,C);    
  32.         cout<<endl;    
  33.         C = NextN(C);    
  34.     }         
  35. }                  
  36. void main()    
  37. {    
  38.     Combination(set,4,2);    
  39. }      

上面的程式碼在VC6.0中測試通過,其執行結果如下:

       最後,或許您對Combination函式中的while中的條件表示式:C<=((1<<N)-(1<<(N-m)))不理解,那麼請看下圖,該圖示意了最後一個組合所對應的C,其值正好等於(1<<N)-(1<<(N-m))

分析

由於我這裡沒有給出求組合數的其他演算法,因此無法對該演算法與其他演算法的效能做對比,有興趣的朋友可以做一個對比。事實上這個演算法的效率相當之高,因為它直接根據前一個組合一步就能求得後面一個組合。當然它也並非沒有一點瑕疵,我個人認為它有兩點不足:

1、  由於它需要用一個整數的二進位制位來標識哪些物件被選中,而整數是有範圍的,因此如果N比較大(大於32),那麼該演算法就不能直接利用了。

2、  得到的組合並非有序的,如上面的結果所示輸出ac之後並非輸出ad,而是bc,其原因是NextN(N)函式,它返回的是“最小的、比N大的、二進位制表示中1的個數與N相同的數”然而這個約束並不能保證根據它求得的組合是有序的。如果一定要求有序的組合,那麼,可以修改NextN這個函式,但本文的核心就是它,因此修改它很可能就失去了意義,當然您可以想出另外一個位運算,在不損失效率的情況下改變NextN的功能,從而得到有序的組合,這個也是我在思考的問題。

結束語

到此本篇即將結束,個人感覺對引例說得比較清晰透徹,如果您有不清楚的地方或者發現有不妥之處,請留言,最後感謝您的閱讀!


相關推薦

給定n元素集合k元素組合數目

本篇摘要 本篇介紹一個非常給力的求組合的演算法!上一篇“c_c++刁鑽問題各個擊破之位運算及其例項(2)”介紹了6個比較複雜的位操作,但是沒有給出任何應用例項,本篇就之前談到的位操作進行應用,其主要內容是用位操作來實現求組合。 引例 先來看一道題目,這個題目是理

程式設計題:給定集合集合的交集

題目:給定兩個整數集合,求兩個集合的交集。 法一:排序法(先將集合排序,在找交集)             排序時間複雜度O(nlogn),對集合遍歷查詢O(n);總的時間複雜度O(nlogn); void main() { int a[] = { 1, 5, 9, 8,

SDUT 3503 有兩正整數N!的K進制的位數

pos class 進制 amp code cpp ref clu lan 有兩個正整數,求N!的K進制的位數 題目鏈接:action=showproblem&problemid=3503">http://sdutacm.org/sdutoj/prob

分治演算法n元素序列中第k大的元素

     首先,我們應該設定產生隨機數的序列儲存在陣列中,然後我們應該最容易想到的是排序對吧,做一個降序排序,就很容易找到第k個大的元素。但是用排序演算法的話,時間複雜度最快的也是快速排序O(logn),如果我們使用分治演算法求得話,會得到一個線性的時間複雜度O(n)。分治演

百度的一道筆試題:N從大到小排好序的整型佇列top M元素

題意詳解:有N個佇列,其中的元素均已經從大到小排序,求出最大的M個元素。 分析: 很容易想到,top elements問題的通用解法是堆(優先佇列),但是N和M的大小關係不確實,所以不好處理。 這裡,我們分2種情況來考慮。 (我們假設資料輸入規則是:第一行輸入N和M;接下

【C++程式設計練習】任意給定 n 有序整數n 有序整數序列的最大值中位數和最小值

題目來源 CCF模擬試題>>小中大>>201903-1 題目描述 老師給了你n個整陣列成的測量資

n有序數組取出k最大值

ole turn uniq sort .so 取出 ons 排序 class 思路:先合並數組,在去重,然後排序,再取出k個最大的值; var arr = [ [10, 2, 3, 4, 5], [2, 3, 4, 5, 6], [5, 7, 8,

合唱團 N學生中選K相鄰兩的位置編號不超過D使得K學生乘積最大

網易2016內推筆試題: 有 n 個學生站成一排,每個學生有一個能力值,從這 n 個學生中按照順序選取 k 名學生,要求相鄰兩個學生的位置編號的差不超過 d,使得這 k 個學生的能力值的乘積最大,返回最大的乘積。 每個輸入包含 1 個測試用例。每個測試資料的第一行包含一個整

創新工場筆試題----有1分,2分,5分,10分四種硬幣每種硬幣數量無限給定n分錢有多少種組合可以組合成n分錢?

【題目】有1分,2分,5分,10分四種硬幣,每種硬幣數量無限,給定n分錢,求有多少種組合可以組合成n分錢? 程式碼如下 void Combination(int *a,int index,int n,vector<int>& vec) { if (n=

Java實現O(log(n+m))兩有序陣列中第K元素或中位數

假設有兩個從小到大的有序陣列A和B,他們的元素個數為N和M,那麼怎麼求得其第K大元素呢?同理,求其中位數就是當N+M為奇數求其第(N+M+1)/2大元素,為偶數時求(N+M)/2和(N+M+2)/2大元素的平均值。 那麼我們怎麼才能求得第K大元素呢? 分別取兩個陣列中間索

有序陣列A[k]和B[k]長度都為kk最小的(a[i]+b[j])

設A={A1,A2,A3,A4,A5,A6,.......} ,B={B1,B2,B3,B4,B5,B6,.......} 因為A和B都是有序的陣列,必須充分的利用這點,可能有同學,看到有同學覺得這個題目比較容易,直接將所有的組合都計算出來,然後取最小的K個,其實出題的人是

給定一個數組出陣列元素的排列和組合——Java實現

1. 思路 組合數C(n,m)和全排列A(n,n)可以通過遞迴的方式,直接實現。 而A(n,m)則可以通過組合數和全排列間接求出A(n,m)=C(n,m)*A(m,m),即對得到的組合數中的每個元素進行全排列 2. Java實現 package com.zfy.test

.分析以下需求並用程式碼實現 1.定義List集合存入多字串 2.刪除集合元素字串中包含0-9數字的字串 只要字串中包含0-9中的任意一個數字就需

public class MyText2 {public static void main(String[] args) {/** 2.分析以下需求,並用程式碼實現 1.定義List集合,存入多個字串*  2.刪除集合元素字串中包含0-9數字的字串* (只要字串中包含0-9

python面試題List各個元素相減絕對值最小是多少

春暖花開,人心浮動,吾思當左遷之,一則工資上漲,二則環境變好。奈何世道不然,吹牛空談者大受歡迎,而吾實事求是者則落寞如此,知之為知之,不知為不知。 投遞無數,才得一二,某國有電信公司邀請面試,始記得吾曾於去年三月去過,現復一年又至三月,碰運氣吧! 約至午後兩點,前臺等候,看

LightOJ 1248 - Dice (III) 給一個質地均勻的n的骰子 投擲出所有點數至少一次的期望次數。(概率)

pri std printf 有一個 return main tdi algorithm style 題意:http://www.lightoj.com/volume_showproblem.php?problem=1248   投擲出第一個未出現的點數的概率為n/n =

校招試題 n個數裏最小的k stringstream運用

sum fail mes DC AC 升序 \n 超過 include 找出n個數裏最小的k個 輸入描述: 每個測試輸入包含空格分割的n+1個整數,最後一個整數為k值,n 不超過100。 輸出描述: 輸出n個整數裏最小的k個數。升序輸出 輸入例子1:

Codeforces 463D Gargari and Permutations(k序列的LCS)

std open sin problems name targe 題目 math 情況 題目鏈接:http://codeforces.com/problemset/problem/463/D 題目大意:給你k個序列(2=<k<=5),每個序列的長度為n(1&l

Python常見十六錯誤集合你知道那些?

學習 錯誤 程序員 使用python會出現各種各樣的錯誤,以下是Python常見的錯誤以及解決方法。 1.ValueError: ‘Conv2d_1a_3×3’ is not a valid scope name 這個是剛遇到的問題,在LZ自己手打Inception net的時候,想賦一個名字的時

Gym-101673: A Abstract Art (模板多邊形的面積並)

tor rac -s define its -1016 truct std opera 手抄碼板大法。 #include<bits/stdc++.h> using namespace std; #define mp make_pair typedef

一個足夠大的數字刪去k數字後得到最小值

直接上程式碼了 /** * 刪除整數的k個數字,獲得刪除後的最小值 * @param num 目標整數(用String做引數是因為考慮到num的值足夠大) * @param k 刪除數量 * @return */ public