回溯法--排列组合类

本文详细介绍了深度优先搜索(DFS)的基本框架,并通过多个实例展示了如何使用DFS解决排列、组合、子集和棋盘问题。回溯法作为DFS的一种搜索策略,用于处理需要撤销操作的问题。文中提供了全排列、组合输出和组合总和的代码实现,强调了去重在搜索中的重要性,并给出了不同场景下的去重策略。此外,还涉及了在有重复元素的集合中寻找目标组合的案例,讲解了树层去重的概念。最后,文章提供了相关的练习题目以加深理解。
摘要由CSDN通过智能技术生成

视频讲解地址

一、DFS的框架,必须熟记

void dfs(int k)
{
	if(到达终点或者目的地)
	{
		输出问题解或者解得方案数+1}
	for(int i = 0;i<="可扩展状态总数";i++)
	{
		按照规则,产生新的状态;
		if(判断新状态是否合法)
		{
			 保存合法结果;
			 dfs(k+1);
			 恢复现场,回溯;//可以没有,根据情况来定
		}
	}
}
int main()
{
	.....
	dfs(k);
	return 0;
}

回溯法是DFS的一种搜索策略;那么它和DFS有什么区别呢?我们在应用的时候应该怎么辨别是否需要回溯呢?

听视频讲解吧,文字太难表述了!

回溯法是DFS的一种搜索策略
回溯法可以解决如下几种问题:
排列问题:N个数按照一定规则全排列,有几种排列方式
组合问题:如何按照一定规则在N个数中找出K个数的集合
切割问题:一个字符串按照一定规则切割,有几种切割方式
子集问题:一个N个数的集合中有多少符合条件的子集
棋盘问题:N皇后、数独等问题

二、排列组合类DFS

例1 全排列

输出1…n(n<100)的全排列。 输入:n 输出:输出全排列,每个排列换行
样例输入:
3
样例输出:
1 2 3
1 3 2
21 3
2 3 1
3 1 2
3 2 1

【分析】
首先,需要一个数组来保存已经产生的排列;我们用int a[102];
接下来根据上面的搜索框架来构造该题的代码框架;

int n;
void dfs (int k)
{
	if(k==n+1)
	{
		输出数组a[];
		}
	for(int i=1;i<=n;i++)
	{
		if(i是可用的)
		{
			a[k]=i;//保存当前的结果
			 dfs(k+1);//继续搜索第k+1个数
			  恢复现场,回溯;
		}
	}
}
int main()
{
	cin >> n;
	dfs(1);
 return 0;
 }

我们来解决这些问题:
1、输出数组a[],for(int i= 1;i <=n;i++) cout <<a[i]<<" ";
2、如何判定产生的i是否可用,也就是i是否已经出现过,两种方式:
(1)我们可以构造一个judge(int x,int y);来判断1-(x-1)的位置是否出现过y;

bool judge(int x,int y)
{
	bool flag = true;
	for(int i = 1;flag && i < x;i++)
	{
		if(a[i]==y) flag = false;
	}
	return flag;

则“i是可用的”就可以使用上述函数来代替了if(judge(k,i);这时候我们不需要恢复现场的,也就是没有回溯过程。
(2)使用标记数组vist[102];来记录1…n是否被使用过了,具体完整代码如下:
强烈推荐这种方式,比较通用!
在这里插入图片描述

#include<iostream>
using namespace std;
int a[102];
int vist[102]={0};
int n;//两个函数公用n,要使用全局变量。
void dfs(int k)
{
  if(k==n+1)//已经放到n+1位置,说明已经完成任务
  {
      for(int i = 1;i <= n;i++) cout<<a[i]<<" ";
      cout<<endl;
  }
  for(int i = 1;i <=n;i ++)
  {
      if(vist[i]==0)//i可以使用
      {
          a[k]=i;
          vist[i]=1;//标记i已经被使用过了
          dfs(k+1);//继续搜索第k+1个数
          vist[i]=0;//回溯,把已经使用的i退回去
      }


  }

}
int main()
{
    cin >> n;
    dfs(1);
    return 0;
}

扩展思维:
1、如果给定的数组a[]中进行全排列,代码应该如何改呢?自己试着实现以下。

#include<iostream>
 using namespace std;
 const int N = 10;
 int path[N];
 int vis[N]={ 0 };
 int n;
 int a[N];
 void dfs(int k)
 {
     if(k==n+1)
     {
         for(int i= 1;i <=n;i++)
         {
              cout<< path[i]<<" ";
         }
         cout<<endl;
         return ;
     }
     for(int i = 1;i <=n;i++)
     {
         if(!vis[i])
         {
             path[k]=a[i];//注意此处的变化
             vis[i]=1;
             dfs(k+1);
             vis[i]=0;
         }
     }

 }
 int main()
 {
     cin >> n;
     for(int i = 1;i <=n ;i++) cin >> a[i];//注意该代码是什么意思
     dfs(1);
     return 0;

 }

排序和组合的本质区别在于是否有序。
体现在代码阶段:
那么既然是⽆序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,⽽不是从0开始!

例2、组合输出

从n个元素中抽出r个元素(不分顺序且r≤n),我们可以简单地将n个元素理解为自然数1,2,…,n,从中任取r个数。

现要求你用DFS的方法输出所有组合。

例如
n=5,r=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

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define N 30
using namespace std;
int n,r;
int a[N];
int vis[N];
void dfs(int step)
{
    int i;

    if(step==r+1)
    {
        for(i=1;i<=r;i++)
            cout<<"  "<<a[i];
        cout<<endl;
        return;
    }

    for(i=a[step-1];i<=n;i++)//从当前已经选择的位置向后选
    {
        if(vis[i]==0)
        {
            a[step]=i;
            vis[i]=1;

            dfs(step+1);

            vis[i]=0;
        }
    }
}
int main()
{
    cin>>n>>r;
    a[0]=1;
    dfs(1);
    return 0;
}

视频讲解

例题3组合总和

给定一个数组c和一个目标数t,找出c中所有可以使数字和为t的组合。 c中的数字每个组合中只能使用一次。
说明:
所有数字(包括目标数)都是正整数。
解集中不能包含重复的组合。
输入
第一行为n和t,表示数组c中包含n个正整数和目标数t;
第二行为n个正整数,中间用一个空格隔开
输出
若干行,一行表示一个符合题意的划分
样例:
输入
7 8
10 1 2 7 6 1 5
输出
1 1 6
1 2 5
1 7
2 6

【解析】
1、本题和例3有如下区别:
本题中c中的数字在每个组合中只能使用一次
本题中c中有重复的元素,而例3中没有重复元素
2、本题的难点在于:**集合c中有重复元素,但不能有重复组合 **
【知识点】
1、借助本题,请理解好搜索中的两种去重“树层去重”和“树枝去重”
2、所谓去重,其实就是使用过的元素不能重复选取。
”使用过“在树形结构上是有两个维度的,一个维度是同一树枝上使用过,另一个维度是同一树层上使用过。
在这里插入图片描述

回看一下题目,元素在同一个组合内是可以重复的,但是两个组合不能相同。所以我们要去重的是同一树层上”使用过“的元素,同一树枝上的元素都是一个组合里的,不用去重。
强调一下,树层去重是需要对数组排序的
本题中增加一个bool类型数组used[],用来记录同一树枝上的元素是否使用过。

#include<bits/stdc++.h>
using namespace std;
int a[105];
bool used[105];
int ans[105];
int n,t;
int sum = 0;
int x = 0;
void dfs(int index)
{
	if(sum==t)
	{
		for(int i = 1;i<=x;i++) cout<<ans[i]<<" ";
		cout<<endl;
		return ;
	}
	for(int i = index;i<=n;i++)
	{
	//used[i-1]==ture,说明 同一树枝使用过a[i-1]元素;used[i-1]==false,说明同一树层使用过a[i-1]元素 
		if(i>0&&a[i]==a[i-1]&&used[i-1]==false) continue;
		sum+=a[i];
		ans[++x]=a[i];
		used[i]=true;
		dfs(i+1);
		//开始回溯
		sum-=a[i];
		used[i]=false;
		x--;	 
	}
}
int main()
{
	cin>>n>>t;
	for(int i = 1;i<=n;i++) cin>>a[i];
	sort(a+1,a+n+1); 
	dfs(1);
	return 0;
}

视频讲解

例四选数【NOIP普及组2002】

提示性图解
在这里插入图片描述
可以适当减纸
视频讲解

练习题目:(实践出真知,实践中体会编程成就感)

1、挑选数字
视频讲解
2、小明爱正方形
视频讲解
3、分书问题
视频讲解

搜索相关内容

1、DFS第一课
3、回溯法–分割问题与子集问题

  • 7
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

信奥教练Andy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值