DFS——组合与排列

引子

1.关于深搜:深度优先搜索是一种解决问题的算法策略。通常,首先它把问题解决过程分解成若干个阶段,然后递归地搜索(枚举)每个阶段所有可能的选项,得到组合式的解,到达边界后,检验解的合法性。
2.学习了那么久的深搜,再回头看一下,就是一串格子,按照题目的要求去填空,其本质就是求组合与排列
3.算法框架:

void dfs(int i)
{
    if(满足边界条件)
    {
        输出解
        return;
    }
    for(可选择的选择j)
        if(没有访问过j&&其它条件)
        {
            标记j已经访问过
            保存
            dfs(i+1);
            取消标记//回溯
        }
}

正题

排列

生成n维向量vector

n维向量是有n个元素的序对,每个元素的取值范围从1到k。例如3的5维向量为{1,1,1,1,1},{1,1,1,1,2},….,{3,3,3,3,3}。输入k和n,输出所有k的n维向量。  、
限制条件 :1<= k <=10, 1<=n<=6

分析

简单的一个搜索,直接用框架解决,而且元素可以重复,不用标记

#include<cstdio>
#define MAXN 6
int n,k,a[MAXN+5];
void dfs(int i)
{
    if(i>n)
    {
        for(int j=1;j<n;j++)
            printf("%d ",a[j]);
        printf("%d\n",a[n]);
        return ;
    }
    for(int j=1;j<=k;j++)
    {
        a[i]=j;
        dfs(i+1);
    }
}
int main()
{
    scanf("%d %d",&n,&k);
    dfs(1);
}
思考
  1. 这道题如果要输出序号,可以增加一个变量tot,like this:
#include<cstdio>
#define MAXN 6
int n,k,a[MAXN+5],tot;
void dfs(int i)
{
    if(i>n)
    {
        tot++;
        for(int j=1;j<n;j++)
            printf("%d:%d ",tot,a[j]);
        printf("%d\n",a[n]);
        return ;
    }
    for(int j=1;j<=k;j++)
    {
        a[i]=j;
        dfs(i+1);
    }
}
int main()
{
    scanf("%d %d",&n,&k);
    dfs(1);
}

2.k的n维向量的总方案数是多少?

对于每一个位置i,都有k个选择,一共n个位置,所以方案数应是k^n

全排列

输入n,输出数字1..n的所有排列。这里不是要计算排列有多少种,而是枚举所 有的排列,以字典顺序枚举。 
限制条件 1<=n<=10

分析

与第一题类似,要标记判重
也可以在存储的答案中查找一遍有无使用选项j,但此方法明显慢得多

#include<cstdio>
#define MAXN 10
int ans[MAXN+5],n;
bool vis[MAXN+5];
void dfs(int x)
{
    if(x>n)
    {
        for(int i=1;i<n;i++)
            printf("%d ",ans[i]);
        printf("%d\n",ans[n]);
        return;
    }
    for(int i=1;i<=n;i++)
        if(!vis[i])
        {
            vis[i]=1;
            ans[x]=i;
            dfs(x+1);
            vis[i]=0;
        }
}

int main()
{
    scanf("%d",&n);
    dfs(1);
}

还有一个方法:交换法
初始:将ans数组赋成1,2,…,n
递归参数x:每次将i从x枚举到n
交换ans[x]和ans[i]
递归x+1
换回ans[x]和ans[i]

#include<cstdio>
#include<iostream>
using namespace std;
# define MAXN 100
int ans[MAXN+5];
int n;
void dfs(int x)
{
    if(x==n)
    {
        for(int i=0;i<n;i++)
            printf("%d ",ans[i]);
        puts(" ");
        return ;
    }
    for(int i=x;i<n;i++)
{
        swap(ans[i],ans[x]);
        dfs(x+1);
        swap(ans[i],ans[x]);
    }
}
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        ans[i]=i+1;
    dfs(0);
    return 0;
}

生成下一个排列:next_permutation

STL 的next_permutation()提供了便捷的枚举排列的方法。它从字典序最小的排 列开始,调用一次,产生下一个排列。 
遵从STL算法库的惯例,next_permutation(begin, end)接受两个迭代器参数,
输入和结果均在迭代器所指容器(通常是vector或数组)。 
当能够产生一个按字典序的新排列时,next_permutation()返回true,否则返
回false。可以利用返回值,在一个循环中,生成所有排列。 
调用一次next_permutation()的时间复杂度为:O(n),大约是从当前排列到下 一个排列需要调用交换函数swap()的次数。 
另一个成对的函数是prev_permutation(),它生成上一个排列。

举个栗子:

生成可重集的全排列

输入一个包含n个整数的数组,元素可以重复。按字典序输出所有全排列,方案不重复。 
例如{1,2,2} 所有的排列就是{1,2, 2}、{2, 1, 2} 、 {2, 2, 1} 。 
限制条件 1<=n<=10 

分析

如果还像之前那样进行标记的话,由于有重复的元素,所以可能会造成重复(标记下标)或缺少元素(标记值),所以要进行去重。那我们就要思考在什么情况下是重复的。如果当前数字与上一次这个位置的数字的值是相同的,那么排列看起来没有区别,所以我们可以用一个变量last来记录上一次这个位置出现的值,进行判断。在做这个方法时要注意先排序,其目的是把相同元素排在一起,否则last会失去作用,因为last仅仅记录的是上一次的值。

#include<cstdio>
#define MAXN 20
using namespace std;
int a[MAXN+5],ans[MAXN+5],n,last;
bool vis[MAXN+5];
void dfs(int i)
{
    if(i>n)
    {
        for(int j=1;j<n;j++)
            printf("%d ",ans[j]);
        printf("%d\n",ans[n]);
    }
    last=-1;
    for(int j=1;j<=n;j++)
        if(!vis[j]&&a[j]!=last)
        {
            ans[i]=a[j];
            vis[j]=1;
            last=a[j];
            dfs(i+1);
            vis[j]=0;
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dfs(1);
}

第二种方法是改进一下vis[],用一个cnt数组来记录这个数字有多少个,用去一个就–,如果cnt[i]为0,表示i已经用完了。

#include<cstdio>
#define MAXN 10
#define MAXVAL 30
int n;
int ans[MAXN+5];
int cnt[MAXVAL+5];
void dfs(int i)
{
    if(i>n)
    {
        for(int j=1;j<n;j++)
            printf("%d ",ans[j]); 
        printf("%d\n",ans[n]);
        return;
    }
    for(int j=1;j<=MAXVAL;j++)
        if(cnt[j])
        {
            cnt[j]--;
            ans[i]=j;
            dfs(i+1);
            cnt[j]++;
        }
}
int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        int t;
        scanf("%d",&t);
        cnt[t]++;
    }
    dfs(1);
}

第三种方法,理解为交换法中如果交换的两个数字是相同的,则没有区别

Part:组合

枚举组合Combination

枚举组合就是生成n个元素的各种组合方式。本质上说,就是枚举子集。
例如{1,2,3} 所有的组合就是{} 、 {1} 、 {2} 、 {3} 、 {1,2} 、 {1,3} 、 {2,3} 、 {1,2, 3},一共有8 个组合

位向量法

计算组合个数的方法
1 可取可不取,有两种情形、 2 可取可不取,有两种情形、 3 可取可不取,有两种情形。根据 乘法原理,总共2×2×2 = 2^3 种情形。
用程序实现时,模拟这个过程。设立标记数组vis[],vis[i]=true,表示集合中包含第i个元素。在 DFS中依次考虑每个元素,取还是不取,把决策信息记录在vis[]中。到达边界后,扫描vis[],输 出一组解。
算法思想是:依序枚举每个位置。针对每个位置,试着填入取或不取

实现

#include MAXN 10
bool vis[MAXN+5];
int A[MAXN+5];
int n;
void dfs(int i)
{
    if(i>=n)
    {
        for(int j=0;j<n;j++)
            if(vis[j]) printf("%d ",A[j]);
        puts("");
        return ;
    }
    vis[i]=0;
    dfs(i+1);

    vis[i]=1;
    dfs(i+1);
    vis[i]=0;
}
增量法(能实现字典序)

思路是往子集里不断放入新元素。每次递归进入后,当前子集都是一个合法解, 先输出解。再考虑试着往子集里新增一个元素。子集里的元素应该升序生成,避免{1,2},{2,1} 这种重复,故设立变量i指示新增元素的最小值。
增量法生成的组合是按字典序排列的。
实现

#define MAXN 10
int S[MAXN+5];
int n;
void dfs(int i,int sz)
//i:下一次放入子集的最小值  sz:当前子集的大小
{
    for(int j=0;j<sz;j++)
        printf("%d ",S[j]);
    puts("");
    for(int j=i;j<=n;j++)
    {
        S[sz]=j;
        dfs(j+1,sz+1);
    }
}

思考
把枚举子集中的元素看成是下标,就可以输出元素值为任意类型的组合。
输入任意类型的元素,存放在数组A中。先排序。
再把输出子集的语句修改成输出特定元素:

for(int j = 0; j < sz; j++) 
      printf("%d ", A[S[j]]);
二进制(位运算)法

把十进制数0~15写成二进制形式:
0000
0001
0010
0011
0100
0101
0110
0111
1000
1001
1010
1011
1100
1101
1110
1111
把数位从右往左分别看成是第0,1,2,3个元素,二进制数该位为0,表示该元素不在子集中; 为1,表示在子集中。例如,0110表示第1,2号元素在子集中,0,3号元素不在子集中。
从0到15正好有16个数,而包含4个元素的所有组合的个数也是16,每一个数就 对应了一个子集,该整数中1的位置就指示了属于子集的元素。
因此一个循环就可以枚举出n个元素的所有组合:

 up = 1 << n;     //up -1的二进制形式恰好有n个1 
 for(int s = 0; s < up; s++)   

要检验一个整数所代表的子集中有哪些元素,需要用到位运算:

1<<i //表示把1左移i位  
s & (1<<i)//表示检验s的右起第i位是否为1,为1则表示第i号元素在子集中  
for(int i = 0; i < n; i++)  
    if( s & (1 << i)) 
         printf(“%d “, A[i]); //输出第i号元素

实现

#include<cstdio>
#define MAXN 10
int A[MAXN+5];
int n;
int main()
{
    scanf("%d",&n);
    for(int i=0;i<n;i++)
        scanf("%d",&A[i]);
    int up=1<<n;
    for(int s=0;s<up;s++)
    {
        for(int i=0;i<n;i++)
            if(s&(1<<i))
                printf("%d",A[i]);
        puts("");
    }
    return 0;
}

思考
二进制法没有用到递归。
联想集合的二进制整数表示


Tip:内容相照应《算法竞赛入门经典》中第七章

猜你喜欢

转载自blog.csdn.net/cqbzlytina/article/details/78646018