n个数的k-组合生成算法:回溯搜索与递推

问题描述:

  • 给定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]的值要满足:
    1. res[i,j]>res[i-1,j] 注:因为数组的每一位是严格递增且互不重复的。
    2. n-res[i,j]>=k-(j+1) 注:因为剩下的可选数n-res[i,j]要大于等于剩余未确定的位数。
    3. 在第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,...,(nk+j+1)enk+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;
    }
};

参考:

题目来源:

leetcode

解法参考:

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值