【蓝桥杯备赛笔记 01】 递归实现枚举算法(C++实现)
笔记
这是博主准备蓝桥杯的时候的学习笔记,今天是准备的第一天,学习枚举算法。本文是博主在学习的时候做的一些笔记整合。
指数型枚举
题目链接 : 递归实现指数型枚举
思路 1 :利用集合关系:
指数型枚举,实际就是求出集合 a = [0,1,2,…,n]的所有真子集的组合和排列。求出所有真子集可能全排列的方法为求出长度为0~n的所有排列,求出组合的办法就是求出所有的固定顺序的排列这样一定能保证结果为组合
例如我要求长度为3的集合a的所有排列,我们只需要将一个长度为1的空集合b1填满,把集合a的数选择进去。再准备一个长度为2的b2,填b2。以此类推填b3。最后的所有b即为所求;而我要求出长度为3的集合a的所有组合,只需要在原来排列的基础上在选数的时候规定一个顺序。
排列
集合b的第1个位置选了2之后,第2个位置可以填1(非升序),也可以填3(升序)。即 2、1、3 与 1、2、3是不同的两个结果。
代码:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 15;//数据长度
int b[N + 1];//待求集合b
bool state[N];//状态数组,用以记录该数是否被选择
//经典的dfs
//参数: cur : 当前选到的位置 tar : 待求b集合的长度 ,n : a集合的长度
void Solution(int cur,int tar,int n)
{
//递归出口
if(cur == tar + 1)
{
for(int i = 1;i <= tar; i ++)
{
printf("%d ",b[i]);
}
puts("");//打印一个换行
return;//退出递归
}
//选数填坑 , 范围为1 ~ n(可以理解为a集合里的数据,
//这题比较特殊,a集合代表的是1~n的所有数据)
for(int i = 1 ;i <= n; i++)
{
if(!state[i])
{
state[i] = true;
b[cur] = i;
Solution(cur + 1,tar,n);
state[i] = false;
}
}
}
int main() {
int n;//a集合的长度
cin >> n;
cout << endl; // 打印一下长度为0的子集
//求出长度为1~n的子集长度
for (int i = 1; i <= n; i ++ )
Solution(1,i,n);
return 0;
}
组合且升序
和上面不同的是,上面是第1个位置选了2之后,第2个位置还可以从2之前的数开始填坑,现在是第1个位置选了2之后,第2个位置只能从大于2的数里选了。
即当cur处选择了num之后,则下次的cur只能从num开始选择,而不能选择之前的数。这样就可以严格控制一种情况且保持升序。
总结:dfs需要四个变量记录当前状态:
当前位于的坑cur,当前可以选的最小数字start,当前的目标总坑数n,当前已经填的坑数组b[]。
代码如下:
#include <iostream>
#include <cstdio>
using namespace std;
const int N = 15;//数据长度
int b[N + 1];//待求集合b
bool state[N];//状态数组,用以记录该数是否被选择
//经典的dfs
//参数: cur : 当前选到的位置 start : 选到的数,用以保证升序
// tar : 待求b集合的长度 ,n : a集合的长度
void Solution(int cur,int start,int tar,int n)
{
//递归出口
if(cur == tar + 1)
{
for(int i = 1;i <= tar; i ++)
{
printf("%d ",b[i]);
}
puts("");//打印一个换行
return;//退出递归
}
//选数填坑 , 范围为start ~ n
for(int i = start ;i <= n; i++)
{
if(!state[i])
{
state[i] = true;
b[cur] = i;
Solution(cur + 1,i + 1,tar,n);
state[i] = false;
}
}
}
int main() {
int n;//a集合的长度
cin >> n;
cout << endl; // 打印一下长度为0的子集
//求出长度为1~n的子集长度
for (int i = 1; i <= n; i ++ )
Solution(1,1,i,n);
return 0;
}
思路2 :状态压缩
枚举集合a中的每一个元素,每一个元素都有选择或不选两个写入集合b的方式。使用一个二进制数state 来表示该元素是否选择。根据计算可以得出一共有2^n种组合
例如n = 3:第一位表示1,第二位表示2…第n位表示n,其值为1代表选择,0代表不选,枚举每种方案即为所求。
000 -> (空)
001 -> 1
010 -> 2
100 -> 3
011 -> 1 2
101 -> 1 3
110 -> 2 3
111 -> 1 2 3
使用循环
#include <iostream>
using namespace std;
int main()
{
int n;
cin >> n;
// state 是每一个状态 1 << n 等价于 2^n
for (int state = 0; state < 1 << n; state ++ )
{
// 用指针j遍历二进制数state中的每一位
for (int j = 0; j < n; j ++ )
{
if (state >> j & 1) cout << j + 1 << " ";
}
cout << endl;
}
return 0;
}
状态压缩递归(参考yxc大佬的代码)
#include <iostream>
using namespace std;
int n;
// u是当前枚举到的数,state是二进制数记录哪些数被选
void dfs(int u, int state) {
if (u == n) {
for (int i = 0; i < n; i ++)
if (state >> i & 1)
cout << i + 1 << " ";
cout << endl;
return ;
}
dfs (u + 1, state); // 不用u这个数
dfs (u + 1, state | (1 << u)); // 用u这个数
}
int main() {
cin >> n;
dfs(0, 0);
return 0;
}
解释yxc大佬的代码
- state 是一个n位二进制数,第1位表示1,第二位表示2,…,第n位表示n。一个state 可以表示2^n个数。
- state >> i & 1 表示取出二进制数state的第i位。
- state | (1 << u)表示将state的第n位转为1,而不改变其他位的值,例如state = 10012 ,u = 3 时的运算结果为10112
排列型枚举
题目链接:94. 递归实现排列型枚举
思路:
利用我们求解指数型枚举的思想,其实求长度为n的集合a的全排列,其实就是求a的 长度为n的所有排列。
例如求解 n = 3 的全排列,其实就是求解 集合a = [1,2,3]的全部长度为3的真子集 b 有 n!个
=> 1 2 3
=> 1 3 2
=> 2 1 3
=> 2 3 1
=> 3 1 2
=> 3 2 1
有了这个理论。我们很快就能根据上面求解指数型枚举的方法求出排列型枚举,由于我们这里只需要知道集合a的长度n即可,因为我们只需要求出n的排列结果,所以我们不需要参数tar。
代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 20;
bool vis[N];
int b[N];
void dfs(int cur, int n)
{
if(cur == n + 1)
{
for(int i = 1;i <= n; i++)
{
printf("%d ",b[i]);
}
cout << endl;
return;
}
for(int i = 1;i <=n ;i ++)
{
if(vis[i] == false)
{
vis[i] = true;
b[cur] = i;
dfs(cur + 1,n);
vis[i] = false;
}
}
}
int main()
{
int n;
cin >> n;
dfs(1,n);
return 0;
}
组合型枚举
思路
组合型枚举其实就是指数型枚举的升序版一个变种,我们只需要求出升序指数型枚举的某一个确定长度的n的结果b[]其实就是结果。例如我们要求C(m,n)就是求一个长度为n的集合a的长度为m的所有真子集。
代码
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 20;
bool vis[N];
int b[N];
void dfs(int cur,int start, int n,int m)
{
if(cur == m + 1)
{
for(int i = 1;i <= m; i++)
{
printf("%d ",b[i]);
}
cout << endl;
}
for(int i = start;i <=n ;i ++)
{
if(vis[i] == false)
{
vis[i] = true;
b[cur] = i;
dfs(cur + 1,i + 1,n,m);
vis[i] = false;
}
}
}
int main()
{
int n,m;
cin >> n >> m;
dfs(1,1,n,m);
return 0;
}
例题:
带分数:
100 可以表示为带分数的形式:100=3+69258/714
还可以表示为:100=82+3546/197
注意特征:带分数中,数字 1∼9 分别出现且只出现一次(不包含 0)。
类似这样的带分数,100 有 11 种表示法。
输入格式
一个正整数。
输出格式
输出输入数字用数码 1∼9 不重复不遗漏地组成带分数表示的全部种数。
数据范围
1≤N<106
输入样例1:
100
输出样例1:
11
输入样例2:
105
输出样例2:
6
思路
- 利用前面求解全排列的思路求解出数字1~9的全排列
- 将全排列结果划分成三个部分,每一个部分表示一个数 例如将 123456789 划分为 1234 567 89 三个数.
- 枚举这三个部分,检验是否符合题目条件
代码:
#include<iostream>
#include<cstdio>
using namespace std;
const int N = 10;
int target; //题目输入的正整数
int num[N]; //用以保存全排列的结果
bool vis[N]; //dfs记录数组 : vis[i]表示数字i是否被访问过
int cnt; // 记录答案
//用以将dfs函数中划分的部分排列组成一个整数
int calc(int l,int r)
{
int res = 0;
for(int i = l;i <= r;i ++) res = 10*res + num[i];
return res;
}
int dfs(int cur)
{
int a,b,c;
if(cur == 9)
{
//使用两个循环,将全排列划分为3部分
for(int i = 0;i < 7;i ++)
for(int j = i + 1;j < 8;j ++)
{
a = calc(0,i);
b = calc(i + 1,j);
c = calc(j + 1,8);
if(a * c + b == c * target) //C++的除法会取整,所以得转换为乘法
cnt++;
}
return 0;
}
//搜索模板
for(int i = 1;i <= 9;i ++)
{
if(!vis[i])
{
vis[i] = true;
num[cur] = i;
dfs(cur + 1);
vis[i] = false;
}
}
}
int main()
{
cin >> target;
dfs(0);
cout << cnt << endl;
return 0;
}