问题描述:
- 给定n个不相同的数 a 1 , a 2 , a 3 , . . . , a n a_1,a_2,a_3,...,a_n a1,a2,a3,...,an,以及正整数k满足(k<n)。请输出全部的 C n k C_n^k Cnk种的n个数的k-组合。
规约:
- 十分显然,该问题可以规约到一个更基础的问题——“输出正整数[1,n]中的全部k-组合”。
- 可以将正整数1-n中的k-组合视为数字,则按照其大小顺序可以得到输出。
(n=5,k=3):
1
2
3
1
2
4
1
2
5
1
3
4
1
3
5
1
4
5
2
3
4
2
3
5
2
4
5
3
4
5
\begin{matrix} 1 & 2&3 \\ 1 & 2&4\\ 1&2&5\\1&3&4\\1&3&5\\1&4&5\\2&3&4\\2&3&5\\2&4&5\\3&4&5 \end{matrix}
111111222322233433443454554555
算法:
- 规律:
对于输出矩阵res的每一行进行遍历,res[i,j]的值实际上是和组合数有关的。
实际上,res[i,j]的值要满足:- res[i,j]>res[i-1,j] 注:因为数组的每一位是严格递增且互不重复的。
- n-res[i,j]>=k-(j+1) 注:因为剩下的可选数n-res[i,j]要大于等于剩余未确定的位数。
- 在第j列,从上向下看这一列,可以看到它实际上是数组 ( j + 1 ) e j + 1 , ( j + 2 ) e j + 2 , . . . , ( n − k + j + 1 ) e n − k + j + 1 (j+1)^{e_{j+1}},(j+2)^{e_{j+2}},...,(n-k+j+1)^{e_{n-k+j+1}} (j+1)ej+1,(j+2)ej+2,...,(n−k+j+1)en−k+j+1的全部后缀的多次拼接。而且易求得 e j + i = C n − ( j + i ) k − ( j + 1 ) e_{j+i}=C_{n-(j+i)}^{k-(j+1)} ej+i=Cn−(j+i)k−(j+1),而拼接次数和其前序数字res[i,j-1]相关。
较复杂的递推算法:
- 思想:
先通过动态规划打表求出 [ C 0 0 , C n k ] [C_{0}^{0} \space , \space C_{n}^{k}] [C00 , Cnk]的所有组合数值。(对于不同的n,k旧值可以重复利用,只需要更新未知值即可)。然后依据规律1~3,设计出一个O(m*k)的算法。其中m为k-组合的个数。但是因为频繁地进行按列遍历,导致cache利用率低,因而速度未达到预期。 - 代码:
class Solution
{
public:
int last_n = 1, last_k = 1;
vector<vector<int>> matrix;
public:
Solution()
{
matrix.reserve(2);
matrix.emplace_back(1, 1);
matrix.emplace_back(2, 1);
}
public:
vector<vector<int>> combine(int n, int k)
{
vector<vector<int>> res(0);
if (k > n)
return res;
//计算出矩阵,并且给res开辟一个初始空间
calculate_matrix(n, k);
int len = matrix[n][k];
res.reserve(len);
res.resize(len);
//计算结果矩阵的第一列
int line0=0;
for(int entry=1;entry<=n;entry++)
{
for(int m=0;m<matrix[n-entry][k-1];m++)
res[line0++].push_back(entry);
}
//计算其余列
for (int i = 1; i < k; i++)
{
int line_index = 0;
int first_entry = i + 1;
int last_entry = n - k + i + 1;
int entry_now=first_entry;
while (entry_now <= last_entry)
{
for (int j = entry_now; j <= last_entry; j++)
{
for (int m = 0; m < matrix[n - j][k - i - 1]; m++)
res[line_index++].push_back(j);
}
entry_now++;
//根据同一行的前一个数字重置当前后缀起始点
if(entry_now>last_entry)
{
if(line_index<len)
entry_now=res[line_index].back()+1;
}
}
}
return res;
}
public:
void calculate_matrix(int n, int k)
{
/**
*
1 0 0 0
1 1 0 0
1 2 1 0
1 3 3 1
1 4 6 4
1 5 10 10
*
**/
//确保矩阵的大小:
matrix.reserve(n + 1);
matrix.resize(n + 1);
for (int i = 0; i <= n; i++)
{
vector<int> &v = matrix.at(i);
v.reserve(k + 1);
v.resize(k + 1);
}
//对矩阵进行行扩展
for (int i = last_n + 1; i <= n; i++)
{
matrix[i][0] = 1, matrix[i][1] = i;
int last_m = i;
for (int j = 2; j <= k; j++)
{
int tmp = (last_m * (i - j + 1)) / j;
matrix[i][j] = tmp;
last_m = tmp;
}
}
//对矩阵进行列扩展
for (int i = last_k + 1; i <= n; i++)
{
int last_m = matrix[i][last_k];
for (int j = last_k + 1; j <= k; j++)
{
int tmp = (last_m * (i - j + 1)) / j;
matrix[i][j] = tmp;
last_m = tmp;
}
}
last_n = n;
last_k = k;
}
};
简单高效的回溯搜索算法:
- 思想:
不需要考虑规律3,只需要考虑规律1,2就可以写出高效而简单的回溯搜索算法。 - 代码:
class Solution1
{
public:
vector<vector<int>> combine(int n, int k)
{
vector<vector<int>> list;
vector<int> result;
dfs(list, result, n, k, 0, -1);
return list;
}
//dfs搜索
void dfs(vector<vector<int>> &list, vector<int> &result, int n, int k, int pos, int pre)
{
if (pos == k)
{
list.push_back(result);
return;
}
if ((pos + (n - pre)) <= k)
return;
//剪枝,添加之后用时节省了2/3
//在当前对不合理的取值进行判断,结束下一层的递归操作。
//如果当前剩余可操作数字的个数即(n-pre)< k-pos+1(即组合中有待赋值的位置个数),
//(+1是因为当前pos还没有进行操作),则可以判断该条路径不可能得到正确的解,不再向下探寻。
for (int i = pre + 1; i < n; i++)
{
result.push_back(i + 1);
pre = i;
dfs(list, result, n, k, pos + 1, pre);
result.pop_back(); //回溯
}
return;
}
};