组合与排列

参考:

https://www.bilibili.com/video/av34962180?t=1435

https://www.bilibili.com/video/BV1DX4y157r2

 

 

一,组合序列

1,dfs

  直接用 dfs 遍历所有可能

  注意点:

    ① 当前可选择的数字从上次被选择的数字的下一个开始:避免重复选择相同的数字

    ② 数字的固定顺序满足字典序:让选择出来的组合符合字典序

2,利用递归的搜索和回溯特性

  每个数字只有选择与不选择两种状态。因此,可以利用递归的搜索功能对所有数字进行选择操作,然后,就可以利用递归的回溯功能取消之前的选择,对当前数字进行不选择操作。

  当选择完了足够个数的数字后,在保存或者输出后一定要 return,不然对下一个数字进行不选择操作后,就会继续输出相同的序列。

3,参数说明

    s:当前搜索的数字

    k:还需要选择的个数

4,代码:

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<vector>
using namespace std;
#define N 101
vector<int>way;
int n, m;
void show()
{
    for (int i = 0; i < way.size(); i++)
        printf("%d ", way[i]);
    puts("");
}
void dfs(int s, int k)
{
    if (n + 1 - s < k)  // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解)
        return;
    if (k == 0)
    {
        show();
        return;
    }
    for (int i = s; i <= n; i++)
    {
        way.push_back(i);
        dfs(i + 1, k - 1);
        way.pop_back();
    }
}
void trace(int s, int k)
{
    if (n + 1 - s < k)  // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解)
        return;
    if (k == 0)
    {
        show();
        return;
    }

    way.push_back(s);
    trace(s + 1, k - 1);
    way.pop_back();
    trace(s + 1, k);
}
int main(void)
{
    // 组合:n 个数中选 m 个数
    while (scanf("%d%d", &n, &m) != EOF)
    {
        printf("dfs:\n");
        dfs(1, m);
        printf("trace:\n");
        trace(1, m);
    }
    return 0;
}
View Code

 

 

二,排列序列

1,dfs

  直接用 dfs 遍历所有可能

  注意点:

    ① 要想选择的数字相同,但顺序不同,需要固定数字的顺序,让可选择的数字下一个数从第一个数字开始

    ② 用 vis[] 对已经选择的数字:避免重复选择相同的数字

      vis[ i ] :为 1 代表第 i 个数字已经被选择了;为 0 代表第 i 个数字没有被选择。

2,利用递归的搜索和回溯特性

  因为回溯是只改变数字的选择与被选择状态,无法改变数字被选择的顺序。比方说你的选择的数字是 1 2 3,那么它被选择的顺序只能是按顺序选择的 1 2 3,再怎么回溯也不可能回溯出 1 3 2 或 2 1 3 之类的顺序。所以上述第二种方法无法求解排列问题。

3,参数说明

  k:还需要选择的个数

4,代码

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<vector>
using namespace std;
#define N 101
vector<int>way;
int n, m, vis[N];
void show()
{
    for (int i = 0; i < way.size(); i++)
        printf("%d ", way[i]);
    puts("");
}
void dfs(int k)
{
    if (k == 0)
    {
        show();
        return;
    }
    for (int i = 1; i <= n; i++)
    {
        if (vis[i])
            continue;
        vis[i] = 1;
        way.push_back(i);
        dfs(k - 1);
        way.pop_back();
        vis[i] = 0;
    }
}
int main(void)
{
    // 排列:n 个数中选 m 个数
    while (scanf("%d%d", &n, &m) != EOF)
    {
        dfs(m);
    }
    system("pause");
    return 0;
}
View Code

 

 

三,组合数

组合数公式:

  c(n, m)  = c(n-1, m) + c(n-1, m-1)

  可定性理解为:

    要从 n 个数中选出 m 个数,对于其中某个数,将其分为选与不选,则有

      不选,则变成从 n-1 个数中选  m 个数,即为 c(n-1, m)

      选,则变成从 n-1 个数中选出 m-1 个数,即为 c(n-1, m-1)

    所以,有 n 个数选 m 个数的情况为选与不选的情况之和。

初始条件(递归出口):

  从 n 个数中选 0 个数为 1,即 c(n, 0) 全为 1。

代码见例题 3。

 

 

四,例题

1,组合

  ① 无重复的数字的组合序列输出。

    链接:https://leetcode-cn.com/problems/combinations/  

class Solution {
public:
    vector<vector<int> >res;
    vector<int>way;
    int n;
    void dfs(int s, int k)
    {
        if(n + 1 - s < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解)
            return;
        if (k == 0)
        {
            res.push_back(way);
            return;
        }

        for (int i = s; i <= n; i++)
        {
            way.push_back(i);
            dfs(i + 1, k - 1);
            way.pop_back();
        }
    }
    void trace(int i, int k)
    {
        if(n + 1 - i < k) // 剪枝:还未选择的数 < 还需选择的数(代表这种情况无解)
            return;
        if (k == 0)
        {
            res.push_back(way);
            return; 
        }

        way.push_back(i);
        trace(i + 1, k - 1);
        way.pop_back();
        trace(i + 1, k);
    }
    vector<vector<int>> combine(int n, int k) {
        this->n = n;
        //dfs(1, k);
        trace(1, k);
        return res;
    }
};
View Code

 

  ② 组合总和,无重复元素的数组,可以无限制的选择一个元素。

    链接:https://leetcode-cn.com/problems/combination-sum/

    解题思路:

      首先,求和是不针对选择的数字的顺序,所以是组合。

      其次,要想实现重复选择同一个元素,就相当于是利用递归实现类 while 循环。

      其中,无论是 dfs 还是 trace 都可以分成两步,

        Ⅰ判断该元素能否选择

        Ⅱ 调用递归函数时不跳到下一个元素,仍然还是选择该元素

      这样,就可以实现如果能够选择,就会一直选择了。

class Solution {
public:

    vector<int>a;
    vector<vector<int>> res;
    vector<int>way;

    void dfs(int s, int sum)
    {
        if(sum == 0)
            res.push_back(way);
        
        for(int i = s; i < a.size(); i++)
        {
            if(sum - a[i] < 0)  
                continue;
            way.push_back(a[i]);
            dfs(i, sum - a[i]);
            way.pop_back();
        }
    }
    void trace(int i, int sum)
    {
        if(sum == 0)
        {
            res.push_back(way);
            return; 
        }   
        if(i == a.size())
            return;

        trace(i + 1, sum);
        if(sum - a[i] >= 0)
        {
            way.push_back(a[i]);
            trace(i, sum - a[i]);
            way.pop_back();
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        this->a = candidates;
        //dfs(0, target);
        trace(0, target);
        return res;
    }
};
View Code

  ③ 组合总和,有重复元素,但每个元素只能选择一次。

    链接:https://leetcode-cn.com/problems/combination-sum-ii/

    解题思路:

      首先,求和是不针对选择的数字的顺序,所以是组合。

      其次,因为有重复元素,所以会出现在同一深度选择不同位置的相同元素的情况,导致出现了相同的序列。

        例如,数组 [ 1, 1, 1 ] ,会出现选择第一个元素和第二个元素的 [ 1, 1 ] 和 选择第一个元素和第三个元素的 [ 1, 1 ],这样就出现了重复了。

      要想在同一深度中不选择相同的数字的话,只能用 dfs。

        因为 dfs 的 for 循环里的是同一深度的元素,而 trace 无法知道当前的元素在哪个深度。

      因为,对于同一深度的元素的选择,对于相同值的元素,其选择应该都是一致的,即

        如果不选择这个值的话,则所有相同的值的元素都不能选择,

        如果选择这个元素的话,那么只能选择相同值中的一个,选择了之后,其它元素不能选择。

      所以,在 dfs 中可以将相同的元素通过排序放在一起,在 for 中只对相同值的元素的第一个元素进行选择操作,从而实现有重复元素的不重复选择。

class Solution {
public:

    vector<int>a;
    vector<vector<int>> res;
    vector<int>way;

    void dfs(int s, int sum)
    {
        if(sum == 0)
            res.push_back(way);

        for(int i = s; i < a.size(); i++)
        {
            if(sum - a[i] < 0) // 超出
                return;
            if (i > s && a[i] == a[i - 1]) // 剔除重复的元素
                continue;

            way.push_back(a[i]);
            dfs(i + 1, sum - a[i]);
            way.pop_back();
        }
    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end());
        this->a = candidates;
        dfs(0, target);
        return res;
    }
};
View Code

   ④ 集合的子集遍历

    链接:https://leetcode-cn.com/problems/subsets/

    解题思路:

      首先,子集是不针对选择的数字的顺序,所以是组合。

      其次,因为是子集,所以不用限制选择的数量,就可以遍历所有可能了。

class Solution {
public:
    vector<vector<int>>res;
    vector<int>a;
    vector<int>way;
    void dfs(int s)
    {
        if (s == a.size())
        {
            res.push_back(way);
            return;
        }
        for (int i = s; i < a.size(); i++)
        {
            way.push_back(a[i]);
            dfs(i + 1);
            way.pop_back();
        }
    }
    void trace(int i)
    {
        if (i == a.size())
        {
            res.push_back(way);
            return;
        }
        way.push_back(a[i]);
        trace(i + 1);
        way.pop_back();
        trace(i + 1);
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        this->a = nums;
        //trace(0);
        trace(0);
        return res;
    }
};
View Code

 

 

2,排列

① 无重复的数字的排列序列输出。

  链接:https://leetcode-cn.com/problems/permutations/

class Solution {
public:
    vector<vector<int> >res;
    vector<int>a;
    vector<int>way;
    int vis[10];
    void dfs(int k)
    {
        if (k == 0)
        {
            res.push_back(way);
            return;
        }

        for (int i = 0; i < a.size(); i++)
        {
            if(vis[i])
                continue;
            way.push_back(a[i]);
            vis[i] = 1;
            dfs(k - 1);
            vis[i] = 0;
            way.pop_back();
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        this->a = nums;
        dfs(a.size());
        return res;
    }
};
View Code

② 有重复元素的数字的排列序列输出。

  链接:https://leetcode-cn.com/problems/permutations-ii/

class Solution {
public:
    vector<vector<int> >res;
    vector<int>a;
    vector<int>way;
    int vis[15];
    void dfs(int k)
    {
        if (k == 0)
        {
            res.push_back(way);
            return;
        }

        for (int i = 0; i < a.size(); i++)
        {
            if(vis[i])
                continue;
            if (i > 0 && a[i] == a[i - 1] && !vis[i - 1]) 
                continue;

            way.push_back(a[i]);
            vis[i] = 1;
            dfs(k - 1);
            vis[i] = 0;
            way.pop_back();
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        sort(nums.begin(), nums.end());
        this->a = nums;
        dfs(a.size());
        return res;
    }
};
View Code

 

3,组合数的计算

  http://poj.org/problem?id=1306

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define N 105
#define ll long long
ll c[N][N];
void combination(int n)
{
    for (int i = 0; i <= n; i++)
    {
        for (int j = 0; j <= i; j++)
            if (j == 0)
                c[i][j] = 1;
            else
                c[i][j] = c[i - 1][j] + c[i - 1][j - 1];
    }
}
int main(void)
{
    combination(101);
    int n, m;
    while (scanf("%d%d", &n, &m) && (m + n))
    {
        printf("%d things taken %d at a time is %lld exactly.\n", n, m, c[n][m]);
    }
    system("pause");
    return 0;
}
View Code

 

 

 

========== ========== ========= ======= ======== ====== ===== ==== == =

    一棵开花的树  席慕容

  如何让我遇见你
  在我最美丽的时刻
  为这
  我已在佛前求了五百年
  求它让我们结一段尘缘
  佛于是把我化作一棵树
  长在你必经的路旁
  阳光下慎重地开满了花
  朵朵都是我前世的盼望
  当你走近请你细听
  颤抖的叶是我等待的热情
  而当你终于无视地走过
  在你身后落了一地的
  朋友啊那不是花瓣
  是我凋零的心

 

posted @ 2020-03-20 23:08  叫我妖道  阅读(405)  评论(0编辑  收藏  举报
~~加载中~~