給定n個元素集合,求k個元素的組合數目
本篇摘要
本篇介紹一個非常給力的求組合的演算法!上一篇“c_c++刁鑽問題各個擊破之位運算及其例項(2)”介紹了6個比較複雜的位操作,但是沒有給出任何應用例項,本篇就之前談到的位操作進行應用,其主要內容是用位操作來實現求組合。
引例
先來看一道題目,這個題目是理解利用位操作求組合的關鍵。。英文原題就不貼了,我用中文描述一下吧:給定一個正整數N,求最小的、比N大的正整數M,使得M與N的二進位制表示中有相同數目的1。
上面的題目描述或許有點拗口,舉個例子把,假如給定的N為78,其二進位制表示為1001110,包含4個1,那麼最小的比N大的並且二進位制表示中只包含4個1的數是83,其二進位制是1010011,因此83就是答案。那麼如何求解這個問題呢?
(2) 非常給力的方法
即將要介紹的方法到底有多給力呢?它神奇到只用如下幾行程式碼(事實上可以合併為一行程式碼)就能實現上述所有程式碼的功能,這幾行程式碼是:
- [cpp] view plaincopyprint?
- int NextN(int N)
- {
- int x = N&(-N);
- int t = N+x;
- int ans = t | ((N^t)/x)>>2;
- return ans;
- }
看到了不,這就是所有程式碼,如果省去臨時變數,它就只包含一行程式碼,但是為了後面講述方便,我將它寫成三行程式碼。它的強大之處並非只是程式碼少,如你所見,它無需呼叫count1Bits函式(效率!
如果您是大牛,請不要笑話我在這裡的大驚小怪,如果你跟兩三天前的我一樣不懂這幾句程式碼的話,那麼請往下看,我將嘗試著把我的理解詳細的表述出來,力求細緻,簡單,易懂。
以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])。下面我將根據上面的流程給出程式碼:
- #include <stdio.h>
- #include"iostream"
- usingnamespace std;
- //定義包含4個元素的集合
- char set[] ={'a','b','c','d','e','f','g','h','i'};
- //根據C的二進位制表示輸出一個組合
- void print(char* set,int C)
- {
- int i = 0;
- int k;
- while((k=1<<i)<=C)
- {//迴圈測試每個bit是否為1
- if((C&k)!=0)
- {
- cout<<set[i];
- }
- i++;
- }
- }
- //這個NextN跟之前我們討論的是一樣的,只不過省去了臨時變數
- int NextN(int N)
- {
- return (N+(N&(-N))) | ((N^(N+(N&(-N))))/(N&(-N)))>>2;
- }
- //求從set中前N個元素 中選擇m個的組合
- void Combination(char* set,int N,int m)
- {
- int C = (1<<m)-1;
- while(C<=((1<<N)-(1<<(N-m))))
- {
- print(set,C);
- cout<<endl;
- C = NextN(C);
- }
- }
- void main()
- {
- Combination(set,4,2);
- }
上面的程式碼在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]長度都為k。求前k個最小的(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