m个数中取n个的组合

组合数生成与打印
#include <stdio.h>   
#include <conio.h> 
#include <stdlib.h>
#include <time.h> 
int a[100];//a[0]舍弃不用,a[1]至a[m]存组合数
void zh(int n,int m,int num)
{
 int i;
 if(m==num-1) 
 {
  for(i=1;i<=m;i++)
   printf("%d ",a[i]);
  printf("\n");
  return;
 }
 for(i=a[num-1]+1;i<n+1;i++)//有顺序的排列即为组合
 {
  a[num]=i;
  //printf("a[%d]=%d\n",num,a[num]);
  zh(n,m,num+1);
 }
};

int main(){
 int n,m;
 a[0]=0;
 printf("please input n,m (n>m)\n");
 scanf("%d %d",&n,&m);
 zh(n,m,1);
 getch();
 system("pause");
 return 0;
}


 


 

<think> 我们参考引用[1]中描述的排列生成思路:通过模拟进位的方式生成排列,并处理重复数字的问题。 引用[1]提到:从1-5中出4个数字,共120种排列。其规律是: - 每次尾数循环结束后,倒数第2列(m-1)进位+1(即2-3行) - 当进位列的数字达到最大值(n)时,继续向前1列(m-2)进位+1(即6-7行) - 当进位后的数字在之前出现过时,数字继续递增+1(即8-9行) - 进位结束后,排列在进位点之前的数字不变,之后的数字,按照1-5的顺序填入,且数字不重复 同时,引用[2]和[3]提供了组合的递归方法,但排列与组合不同,排列考虑顺序。 因此,我们可以设计一个VBA函数,用于生成从m个数n个数的排列(无重复排列,即m个数互不相同,且每个组合元素不重复)。 思路: 1. 初始化一个数组,用于存储当前的排列,初始值为最小的n个数字(假设原始数组已经排序,例如1,2,...,m,我们前n个) 2. 设置一个循环,每次循环输出当前排列,然后尝试生成下一个排列。 3. 生成下一个排列的方法(模拟进位): a. 从最后一个位置(第n位)开始向前检查,尝试将当前位置的数字增加1(进位起点)。 b. 如果增加后该位置的数字超过了最大值m(即超出了原始数组范围),或者该数字在之前的位已经出现过,则继续增加直到找到一个未使用且不超过m的数字。 c. 如果该位置增加到超过m,则将该位置重置为可的最小值(例如1),然后向前一位进位(即前一位增加1),并重复步骤b。 d. 如果进位一直到第一位置且第一位置也超过了m,则结束。 e. 当进位停止后,进位点之后的位置需要重置:从进位点之后的位置开始,重新填入可用的最小数字(且不能与前面的重复),直到填满n位。 注意:由于排列要求不重复,所以每个位置上的数字不能相同。 但是,引用[1]中的方法描述的是从1到m的数字中n个排列,且原始数字是从1开始的连续整数。我们可以假设原始数组是arr(1 to m),其中arr(i)=i。 如果原始数组不是连续整数,我们可以先索引,然后映射到实际元素。 然而,用户要求的是从m个数n个数的排列组合,这里的m个数可能是任意数字(不一定是连续的整数)。因此,我们有两种思路: 思路一:使用索引(1到m)生成排列,然后映射到实际数组元素。 思路二:直接使用实际数组元素,但需要定义元素的顺序(例如按数组顺序,或者要求用户提供排序后的数组)。 为了方便,我们按照思路一:先生成索引的排列,然后根据索引实际数组的元素。 步骤: 1. 假设原始数组为Arr,长度为m,要n个数的排列。 2. 创建一个索引数组IdxArr,其中IdxArr(i)=i(索引从1到m,或者0到m-1,VBA中通常用1开始)。 3. 生成从IdxArr中n个索引的所有排列(排列的每个元素是索引,且不重复)。 4. 将每个排列的索引映射到Arr中的实际元素。 下面我们重点实现生成索引排列的函数。 根据引用[1]的进位思想,我们可以这样设计: 初始化:创建一个数组perm(1 to n)用于存储当前排列的索引(初始前n个索引,即1,2,...,n) 使用数组used()来记录每个索引是否已经被使用?但是引用[1]的进位方法并不需要全局记录,而是通过递增和检查前面位置是否有重复来避免重复。 算法步骤: 1. 输出当前排列(将perm中的索引映射到实际数组元素)。 2. 从后往前(从n到1)找到第一个可以增加的位置i(即perm(i) < max_value,注意max_value应该是m,但当前位i的最大值应该是m - (n-i)?实际上,由于是从m个数,且不能重复,所以perm(i)的最大值不能超过m,并且要保证后面还有足够的数字?但是我们的进位方法不要求连续,所以采用递增,然后跳过重复的方式) 3. 将perm(i)增加1。 4. 检查perm(i)是否超过m?如果是,则重置perm(i)为1(最小索引),然后i-1(进位到前一位),重复步骤2(如果i已经为0,则结束)。 5. 如果perm(i)没有超过m,检查perm(i)是否与前i-1个位置的任意一个重复?如果重复,则perm(i)再增加1,并重复步骤4(直到不重复或超过m,超过m则进位)。 6. 当找到一个不重复且不超过m的值后,将位置i之后的数字重置:i之后的每个位置j(从i+1到n)都从最小的索引1开始逐个尝试,直到找到一个没有在perm(1..j-1)中出现的最小索引。 7. 然后回到步骤1。 但是这个算法可能会比较慢,因为每次都要检查重复。另一种思路是使用字典或者数组记录每个索引的使用情况,但进位算法中每次进位后使用情况会发生变化,需要动态更新。 然而,引用[1]的方法中提到“进位结束后,排列在进位点之前的数字不变,之后的数字,按照1-5的顺序填入,且数字不重复”,所以我们可以这样做: 进位点i之后的数字,我们重新填入从1到m中未出现在perm(1..i)中的最小几个数(按从小到大顺序n-i个)。 因此,我们需要一个函数来获当前未被占用的最小k个数字(按顺序)。 但是,注意:我们的索引是1到m,所以我们可以: 步骤6:对于位置j从i+1到n,设置perm(j)为当前可用的最小索引(可用指未在perm(1..j-1)中出现)。 具体方法:从1开始递增,如果当前数字没有被perm(1..j-1)使用,则填入,然后继续下一个位置。 这样,我们可以避免重复,并且保证从小到大顺序。 算法流程: 初始化perm数组为1到n(表示第一个排列) 循环: 输出当前排列 设置i = n 循环以下步骤直到进位成功或者结束: a. 将perm(i)增加1 b. 如果perm(i)大于m,则重置perm(i)=0(表示未设置),然后i=i-1(进位到前一位),如果i<1则退出循环(结束) c. 如果perm(i)<=m,则检查perm(i)是否在perm(1..i-1)中已经出现(即重复),如果重复则回到a(继续增加1) d. 如果没有重复,则说明perm(i)设置成功,然后设置j=i+1到n:对每个j,设置perm(j)为从1开始的最小可用数字(即从1开始,第一个没有在perm(1..j-1)中出现的数字) e. 设置完成后,跳出循环,继续输出下一个排列 如果i<1,退出循环 注意:步骤d中,设置perm(j)时,需要保证该数字在之前位置(包括已经重置的j前面的位置)没有出现过。 但是,这个算法在重置j位置时需要知道哪些数字已经被使用了。我们可以用一个数组used来记录当前排列中已经使用的数字(但注意,在进位过程中,进位点之前的数字不变,之后的需要重置,所以我们需要在每次进位后动态更新used数组?或者不记录used,而是每次检查perm(1..j-1)中的数字) 由于n一般不会太大,我们可以每次检查重复时遍历前面位置。 实现细节: 1. 初始化perm数组,并设置i_end标志为False。 2. Do While循环,直到结束(i<1) 输出当前排列(将perm映射到实际数组元素) 设置i = n 设置进位标志carry = True Do While i>=1 and carry carry = False perm(i) = perm(i) + 1 ' 如果超过m,则需要进位 If perm(i) > m Then perm(i) = 0 ' 重置当前位置 i = i - 1 ' 进位到前一位 carry = True ' 继续进位循环 Else ' 检查perm(i)是否与前面的重复 isDuplicate = False For j = 1 To i-1 If perm(j) = perm(i) Then isDuplicate = True Exit For End If Next j If isDuplicate Then ' 重复,则继续增加当前位(不需要进位,所以carry=False,但需要继续当前位的循环) ' 注意:这里直接回到perm(i)增加1,所以我们不做任何操作,继续循环(但当前还在Do While循环内,如何继续?) ' 实际上,我们让carry=False,然后继续当前位的增加(通过Do While循环的条件?我们这里用另一个循环) ' 因此,我们可以设置一个内层循环来处理当前位的递增直到不重复或超过m ' 所以,我们不应该用Do While循环来处理递增,而是用内层循环。 ' 修改:将当前位的递增和重复检查放在一个循环内,直到找到一个不重复的数字或者超过m。 ' 因此,我们跳出这个内层For循环,回到步骤a(即继续增加perm(i)并检查) ' 但此时我们已经在Do While循环内,所以我们需要继续循环(不改变i),并且再次执行perm(i)增加1和检查?所以不能跳出,而是继续Do While循环(即carry=True?这里我们并不进位,所以carry=False,但需要继续处理当前位) ' 这里我们可以用Goto语句,但避免使用。所以我们可以将当前位的递增和重复检查放在一个内层循环。 ' 因此,我们暂时跳出这个Do While循环,用一个While循环来处理当前位。 ' 修改算法:将进位和当前位递增分开处理。 ' 重新设计:在当前位置i,我们尝试增加perm(i),然后检查是否可用(不重复且不超过m),如果不可用则继续增加,直到可用或者超过m(超过则进位)。 ' 所以我们将步骤b和c合并为一个循环: ' 重新设计内部: ' 在位置i,循环: ' perm(i)=perm(i)+1 ' 如果perm(i)>m,则设置perm(i)=0,i=i-1,并设置carry=True,退出当前循环(退出处理i的循环) ' 否则,检查重复: ' 如果重复,则继续(下一轮循环) ' 否则,设置该位置有效,然后退出循环 ' 如果进位(carry=True),则继续处理前一位(即继续外层Do While循环) ' 如果没有进位(即找到了一个不重复的数字),则设置后面位置并重置进位标志,然后退出进位循环。 ' 因此,我们在这里用一个内部循环处理当前位: ' 但是我们已经在一个Do While循环内,所以我们可以将上面步骤写在一个内部循环内。 ' 具体: Do perm(i) = perm(i) + 1 If perm(i) > m Then ' 当前位超过m,需要进位:重置当前位,进位到前一位 perm(i) = 0 i = i - 1 carry = True Exit Do ' 退出内部循环,然后继续外层循环(进位) Else ' 检查重复 isDuplicate = False For j = 1 To i-1 If perm(j) = perm(i) Then isDuplicate = True Exit For End If Next j If Not isDuplicate Then carry = False Exit Do ' 找到一个可用数字,退出内部循环 End If End If Loop Else ' 没有重复,则当前位设置成功 carry = False End If End If Loop ' 结束处理进位循环 如果进位过程中i<1,则退出主循环(结束) 如果没有进位(即carry=False)且i>=1,则设置后面位置(从i+1到n)的值: For j = i+1 To n ' 设置perm(j)为1到m中未在perm(1)到perm(j-1)中出现的最小数字 perm(j) = 0 ' 先置0,表示未设置 ' 对位置j,从1开始递增,找到第一个未出现在perm(1..j-1)中的数字 For num = 1 To m isUsed = False For k = 1 To j-1 If perm(k) = num Then isUsed = True Exit For End If Next k If Not isUsed Then perm(j) = num Exit For End If Next num Next j 3. 输出排列 注意:这个算法在设置后面位置时,每次都是最小可用数字,所以生成的排列是递增顺序(字典序)。 但是,这个算法效率可能不是最优,因为每次设置后位都需要多次循环检查,但n一般较小,所以可以接受。 下面我们编写VBA函数: 函数功能:生成从m个数n个数的排列(m个数存储在数组arr中) 输出:将排列输出到工作表的某一列,或者存储在一个二维数组中,这里我们输出到工作表。 假设:数组arr是变体类型,包含m个元素,索引从1到m(或者0?VBA数组默认0开始,但我们可以择使用1到m,所以将arr转为1-based) 步骤: 1. 初始化:将原始数组处理为1-based(如果用户输入是0-based,则转为1-based数组,或者用索引1..m映射) 2. 初始化排列数组perm(1 to n)为1,2,...,n 3. 初始化计数器count=0,用于记录排列个数(也可用于输出位置) 4. 循环直到结束 5. 将当前perm映射到实际元素:输出arr(perm(1)), arr(perm(2)), ..., arr(perm(n)) 6. 然后按照进位算法生成下一个排列 注意:如果m<n,则没有排列,直接退出。 代码实现如下: 由于VBA中数组和循环较多,我们需要注意效率,但对于排列问题,n和m都不大(比如m<=10, n<=5),所以没问题。 下面我们写一个Sub过程,假设原始数组在Sheet1的A列(从A1到A(m)),输出排列到Sheet1的B列开始(每行一个排列,每列一个元素)。 但是,为了通用,我们可以写一个函数,输入数组和n,返回一个二维数组(所有排列),但考虑到排列数量很大(P(m,n)=m!/(m-n)!),所以当m和n较大时,数组会很大。因此,我们可以直接输出到工作表。 这里采用直接输出到工作表的方式。 代码: 注意:我们使用1-based索引。 步骤如下: 1. 获原始数组arr,假设arr是工作表的某一列,我们读为数组,并假设第一个元素在索引1(我们使用ReDim为1-based) 2. 初始化perm数组(1 to n)为1,2,...,n 3. 计算总排列数total = Application.WorksheetFunction.Permut(m, n) ' 用于进度显示,可 4. 设置输出位置,比如从B1开始输出,每行一个排列,每列一个元素。 5. 计数器count=0 6. 循环直到结束 count=count+1 输出:For j=1 to n: Cells(count, j+1) = arr(perm(j)) ' 输出到第count行,第2列到第n+1列 然后生成下一个排列(使用上面进位算法) 注意:生成下一个排列的算法中,如果遇到进位到i=0,则结束。 下面我们具体实现代码: 由于代码较长,我们写一个Sub。 注意:算法中重置后位置时需要循环检查,可能效率不高,但n较小,可以接受。 代码实现如下: </think> 以下是根据引用内容整理的VBA实现从m个数n个数进行排列组合的方法,结合了进位算法和递归思想: ### VBA排列组合算法思路 1. **核心逻辑**:采用进位机制生成排列(类似引用[1]描述) 2. **递归组合**:先获所有组合,再对每个组合生成全排列(参考引用[2][3]) 3. **去重处理**:对原始数据去重(引用[3]建议) ```vba ' 主函数:生成排列组合 Sub GeneratePermutations() Dim arrSource() As Variant ' 原始数据数组 Dim n As Integer, m As Integer Dim combResults As Collection, permResults As Collection ' 步骤1: 获原始数据(示例为1-5) arrSource = Array(1, 2, 3, 4, 5) ' 替换为实际数据 m = UBound(arrSource) - LBound(arrSource) + 1 n = 4 ' 择数量 ' 步骤2: 生成组合 Set combResults = New Collection ReDim temp(1 To n) As Integer Call GetCombinations(1, 1, n, m, temp, combResults) ' 步骤3: 对每个组合生成排列 Set permResults = New Collection Dim vComb As Variant For Each vComb In combResults Call GetPermutations(vComb, n, permResults) Next vComb ' 步骤4: 输出结果(120行) OutputResults permResults, "Sheet1", "B2" End Sub ' 递归生成组合(引用[2]改进) Private Sub GetCombinations(start As Integer, depth As Integer, n As Integer, m As Integer, _ ByRef temp() As Integer, ByRef results As Collection) If depth > n Then ' 存储当前组合 results.Add ArrayCopy(temp) Exit Sub End If For i = start To m - n + depth temp(depth) = i Call GetCombinations(i + 1, depth + 1, n, m, temp, results) Next i End Sub ' 生成排列的进位算法(引用[1]实现) Private Sub GetPermutations(arrComb() As Integer, n As Integer, ByRef results As Collection) Dim arrPerm() As Integer arrPerm = ArrayCopy(arrComb) ' 初始排列 Do ' 存储当前排列 results.Add ArrayCopy(arrPerm) ' 进位算法生成下个排列 Dim i As Integer, j As Integer i = n Do arrPerm(i) = arrPerm(i) + 1 ' 尾数递增 ' 进位处理 If arrPerm(i) > UBound(arrPerm) Then arrPerm(i) = LBound(arrPerm) i = i - 1 If i < 1 Then Exit Do Else ' 检查重复(引用[3]) Dim isDuplicate As Boolean isDuplicate = False For j = 1 To i - 1 If arrPerm(j) = arrPerm(i) Then isDuplicate = True Exit For End If Next j If Not isDuplicate Then Exit Do End If Loop ' 重置后续位置(引用[1]核心) If i >= 1 Then For j = i + 1 To n arrPerm(j) = LBound(arrPerm) Do arrPerm(j) = arrPerm(j) + 1 isDuplicate = False For k = 1 To j - 1 If arrPerm(k) = arrPerm(j) Then isDuplicate = True Exit For End If Next k Loop While isDuplicate Next j Else Exit Do End If Loop End Sub ' 辅助函数:数组拷贝 Private Function ArrayCopy(arr() As Integer) As Integer() Dim ret() As Integer, i As Integer ReDim ret(LBound(arr) To UBound(arr)) For i = LBound(arr) To UBound(arr) ret(i) = arr(i) Next i ArrayCopy = ret End Function ' 结果输出 Private Sub OutputResults(results As Collection, sheetName As String, startCell As String) Dim ws As Worksheet Set ws = ThisWorkbook.Sheets(sheetName) Dim cell As Range Set cell = ws.Range(startCell) Dim i As Long, j As Integer For i = 1 To results.Count Dim arr As Variant arr = results(i) For j = LBound(arr) To UBound(arr) cell.Offset(i - 1, j - 1).Value = arr(j) Next j Next i End Sub ``` ### 关键算法说明 1. **组合生成**(`GetCombinations`) - 递归从`m`个数择`n`个数 - 避免重复组合(如[1,2]和[2,1]视为相同) - 时间复杂度:$O(C_m^n)$[^2] 2. **排列生成**(`GetPermutations`) - 采用引用[1]的进位算法: ```mermaid graph LR A[初始化排列] --> B[存储当前排列] B --> C{尾数递增} C -->|超出范围| D[进位+重置尾数] C -->|有重复值| E[继续递增] D --> F{前位是否超限?} F -->|是| G[结束] F -->|否| C E --> C C -->|有效| H[重置后续位置] H --> B ``` - 处理规则: - 当尾数递增超过范围时向前进位 - 重置后续位置时按最小可用值填充 - 检查数字重复(确保元素不重复)[^1][^3] 3. **去重机制** - 原始数据预处理去重(如`[2,3,3]→[2,3]`) - 排列时检查重复值(引用[3]) ### 应用示例 输入:`m=5 (1,2,3,4,5)`, `n=4` 输出: ``` 1 2 3 4 1 2 3 5 1 2 4 5 ...(共120行)... 5 4 3 2 ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值