【C++学习笔记2】探索C++中的子集问题:搜索与回溯算法的应用与优化

哈喽大家好!今天胖鹅来继续给大家讲一讲C++的算法——搜索与回溯。

目录:

一、今日干货?

二、正文

1.引入

2.增量构造法 

2.位向量法

3.拆分自然数

一、今日干货?

1.了解子集问题的思想及代码框架

2.增量构造法与位向量法

3.子集问题在拆分数字的应用

二、正文

1.引入

先看个小问题:

给定一个N,表示集合S={1,2,3,4,……,N},输出其所有子集。其中{1,2}和{2,1}被认为是同一个解


例如当N=3时,子集为{1},{2},{3},{1,2},{2,3},{1,3},{1,2,3}

首先子集是什么

如果集合A的任意一个元素都是集合B的元素,那么集合A称为集合B的子集

我们回到这个问题,很多人第一反应是for循环枚举,但子集问题有这两个关键:

1.不可重复。如{1,2}和{2,1}就要去掉一个。

2.枚举长度不固定。这个子集问题不同于全排列问题,因为全排列问题长度固定为N,但子集问题长度不定。无法for循环枚举。

for循环都不行,那咋办🥵?

我们不着急,一个一个的解决。先解决第一个问题——去重

把问题稍微变一下:

给定一个N,表示集合S={1,2,3,4,……,N},输出其长度为3所有子集。其中{1,2}和{2,1}被认为是同一个解


例如当N=4时,子集为{1,2,3},{2,3,4},{3,4,1},{4,1,2}

新问题中每个解所包含的元素数量是确定的,解中每个元素各不相同,搜索过程中如果能保证后面的元素比前面的元素大,就可以保证一定不会重复

所以用for循环写就是:

for(int i=1;i<=n;i++){//枚举第一位
    for(int j=i+1;j<=n;j++){//枚举第二位
        for(int k=j+1;k<=n;k++){//枚举第三位
            cout<<i<<" "<<j<<" "<<k<<endl;//输出
        }
    }
}

现在问题升级:

给定一个N,表示集合S={1,2,3,4,……,N},输出其长度为M(1≤M≤N)所有子集。其中{1,2}和{2,1}被认为是同一个解


例如当N=4,M=3时,子集为{1,2,3},{2,3,4},{3,4,1},{4,1,2}

这时候问题就转变成了全排列问题。(还没学习全排列的戳这:搜索与回溯算法之全排列问题

长度固定为M,所以就是看第一个位置放什么,第二个位置放什么,一直放到第M个位置

分析递归内容:

定义一个叫cur的变量表示当前放置数字的位置,定义一个ans数组存储全排列

1.递归边界

cur>m时,说明已经构造好了一个全排列。

2.内容

由于要枚举第cur个位置上的数字,而且保证不与前一个数重复,所以要对ans[cur-1]+1到n进行for循环。

for循环里要存储枚举的数:ans[cur]=i;

随后遍历下一位:solve(cur+1);


实现代码:

const int N=20;
int n,m,ans[N];
void solve(int cur){
	if(cur>m){//说明已经填完一个排列
		for(int i=1;i<=m;i++)cout<<ans[i]<<' ';
		printf("\n");
        return ;
	}
	for(int i=ans[cur-1]+1;i<=n;i++){//否则遍历
		ans[cur]=i;
		solve(cur+1);
	}
}

那么刚开始的问题就可以解决了。

2.增量构造法 

观察这棵树,我们发现每到一个节点,就构成一个解。这时就可以输出。

这就是增量构造法

实现代码:

const int N=20;
int n,ans[N];
void solve(int cur){
	if(cur>1){//说明已经填完一个排列
		for(int i=1;i<cur;i++)cout<<ans[i]<<' ';//输出 
		printf("\n");
	}
	for(int i=ans[cur-1]+1;i<=n;i++){//否则遍历ans[cur-1]+1------>n
		ans[cur]=i;//存储 
		solve(cur+1);//下一位 
	}
}

运行结果:

2.位向量法

我们拓展另一个解法。还是刚刚的问题。

下面是一张表:(我没有骂人的意思)

S(集合)123
B(数组)0/10/10/1

在生成集合S的子集时,对于集合中的任一元素都可选可不选,定义bool类型数组B,B[i]==1表示选择了,B[i]==0表示未选择

例如当N==3时,B的解空间有:(0, 0, 0), (0, 0, 1), (0, 1, 0), (0, 1, 1), (1, 0, 0), (1, 0, 1), (1, 1, 0), (1,1,1)

所以问题就完全被转换成了全排列问题对0,1进行全排列

实现代码:

const int N=20;
int n,b[N];
void solve(int cur){
	if(cur>n){//说明已经填完一个排列
		for(int i=1;i<cur;i++){
			if(b[i])cout<<i<<' ';//输出 
		}
		printf("\n");
		return ;
	}
	for(int i=0;i<=1;i++){//否则遍历0->1
		b[cur]=i;//存储 
		solve(cur+1);//下一位 
	}
}


运行结果:

悄悄问一下:你知道为什么第一行是空白么?

答:第一种情况为0,0,0

3.拆分自然数

题目:

给定一个自然数N,则任何一个大于1的N,总可以拆分成若干个小于N的正整数之和,例如当N=3时,有两种划分,即3=1+2和3=1+1+1。试求出N的所有拆分。

输入格式

一个整数即N,N≤45。

输出格式

输出每一种划分方案,每种划分方案占一行,最后一行为方案总数。

分析题目,由于拆分长度不固定,且不能重复不能同时出现3=1+2和3=2+1),所以这是一个典型的子集问题

由于是正整数,所以每个数的可选范围为[1,n-1]。且单个数可以重复使用。如3=1+1+1。

分析递归:

定义一个叫cur的变量表示当前放置数字的位置,一个cnt的变量记录方案数,定义一个ans数组存储排列

1.参数

虽然子集问题没有固定长度,但我们知道当和为N时就找到了一个解

所以除了cur,我们还需要一个参数sum,表示当前的和

2.递归边界

sum==n,输出排列并cnt++。

3.内容

由于不能重复,但可以与前一个数相同,所以要对ans[cur-1]到N-1进行for循环。

接下来可以进行剪枝优化。若sum+i<=n,就存储并solve(cur+1,sum+i)。

实现代码:

void solve(int cur,int sum){
	if(sum==n){//若和=N
		cnt++;
		print(cur);//输出
		return ;
	}
	for(int i=ans[cur-1];i<n;i++){//否则遍历
		if(sum+i<=n){//剪枝
			ans[cur]=i;
			solve(cur+1,sum+i);
		}
	}
}

这时运行发现:

啥也没有? 

通过分析代码,ans定义为全局数组,初始值为0。当第一次进入solve是,for循环从ans[0]->n-1,所以第一次尝试为0。那后面就会一直是0。所以sum不可能为N。总结:死循环了。

所以ans[0]初始值应为1

总体代码:

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<vector>
#include<cstring>
#include<queue>
using namespace std;
const int N=50;
int n,ans[N],cnt;
void print(int cur){
	printf("%d=%d",n,ans[1]);
	for(int i=2;i<cur;i++){
		printf("+%d",ans[i]);
	}
	printf("\n");
}
void dfs(int cur,int sum){
	if(sum==n){
		cnt++;
		print(cur);
		return ;
	}
	for(int i=ans[cur-1];i<n;i++){
		if(sum+i<=n){
			ans[cur]=i;
			dfs(cur+1,sum+i);
		}
	}
}
int main()
{
	cin>>n;
	ans[0]=1;
	dfs(1,0);
	cout<<cnt;
	return 0;
}

小彩蛋:N=45时有89311种拆分方案。可以试试哦! 

子集问题算讲完了。有什么问题可以私信我。同时欢迎各位大佬指出文章不足。最后点个赞吧!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值