洛谷T476751 递归实现指数型枚举
题目描述
从 1~n 这 n(n<16) 个整数中随机选取任意多个,输出所有可能的选择方案。
输入格式
一个整数n。
输出格式
每行一种方案。同一行内的数必须升序排列,相邻两个数用恰好1个空格隔开。对于没有选任何数的方案,输出空行。本题的输出顺序请参照样例。
样例 #1
样例输入 #1
3
样例输出 #1
3
2
2 3
1
1 3
1 2
1 2 3
主要思路
1. 理解子集生成
对于给定的整数 n,数字范围是从 1 到 n。每个数字都有两种选择:选中或者不选,因此我们可以用二进制来表示选择状态。
- 当某一位为 1 时,表示选择该数字。
- 当某一位为 0 时,表示不选择该数字。
例如,n = 3 时,我们有 8 个可能的选择状态,对应的输出如下:
- 000 表示不选任何数字,即输出空集 " "
- 001 表示选择 3,输出 3
- 010 表示选择 2,输出 2
- 011 表示选择 2, 3,输出 2 3
- 100 表示选择 1,输出 1
- 101 表示选择 1, 3,输出 1 3
- 110 表示选择 1, 2,输出 1 2
- 111 表示选择 1, 2, 3,输出 1 2 3
2. 递归实现子集生成
采用深度优先搜索(dfs)的思想,使用递归,我们可以按顺序来尝试对每个数字做出选择。
- 递归定义:对于每一个数字,做两件事情:不选这个数字,递归进入下一层;选这个数字,递归进入下一层。
- 递归终止条件:当到达数字 n+1 时(即所有数字都被选择过了),根据当前的状态输出选中的数字。
3.状态表示
在递归过程中,我们用 state 的二进制位来表示当前选中的数字。例如,如果 state = 101(二进制),则表示第 1 位和第 3 位被选中了,即选中了数字 1 和 3。
代码实现
#include <iostream>
using namespace std;
// 定义全局变量 n,用于存储用户输入的整数
int n;
// 递归函数 dfs 用于生成所有选择方案
void dfs(int u, int state) {
// 当 u 达到 n 时,表示已经遍历完所有可能的数位
if (u == n) {
// 输出当前选择方案中的所有选中的数
for (int i = 0; i < n; i++) {
if (state >> i & 1) // 检查第 i 位是否被选中
cout << i + 1 << " "; // 输出选中的数
}
cout << endl; // 输出换行
return; // 结束当前递归调用
}
// 不选择第 u 个数,继续递归
dfs(u + 1, state);
// 选择第 u 个数,使用位运算将第 u 位设为 1,继续递归
dfs(u + 1, state | (1 << u));
}
int main() {
// 读取用户输入的整数 n
cin >> n;
// 从第 0 个数开始,初始状态 state 为 0
dfs(0, 0);
return 0;
}
代码理解
- 递归遍历每一个数的选择情况:我们使用一个递归函数 dfs,每次递归都会传入当前处理的数 u 和当前选择的状态 state。
- 状态变量 state 的作用:state 是一个二进制表示的整数,用来表示当前哪些数被选择了。二进制的每一位代表一个数的位置,1 表示该位置被选择,0 表示该位置未被选择。
- 递归终止条件:当 u 达到 n 时,表示我们已经遍历了所有数(0 ~ n-1)。此时检查 state,将其中被选择的数输出。
- 递归分支:对于每一个数 u,有两个选择——不选和选:
- dfs(u + 1, state) 表示不选择第 u 个数,状态 state 保持不变。
- dfs(u + 1, state | (1 << u)) 表示选择第 u 个数,使用位运算 state | (1 << u) 将第 u 位设置为 1。
难点分析
二进制运算
在代码中,我们使用了二进制的位运算,下面简单的介绍几个常用的位运算,特别是代码中用到的位移运算和按位与运算。
在二进制中,每一位(0 或 1)代表一个开关状态。位运算可以在二进制层面上直接操作这些开关状态,效率高且适合状态组合问题。
- 位移运算(<< 和 >>)
左移(<<):将数字的二进制位向左移动,右边补零。每左移一位,相当于乘以 2。
例如,1 << 2 (二进制 0001) 左移两位变成 0100,即 4。
右移(>>):将数字的二进制位向右移动,左边补零。每右移一位,相当于除以 2。
例如,4 >> 1 (二进制 0100) 右移一位变成 0010,即 2。
在代码中,我们使用 1 << u 来生成一个值,这个值的二进制形式只有第 u 位是 1,其他位都是 0。这样可以用来标记选中第 u 个元素的状态。 - 按位与运算(&)
按位与运算会比较两个二进制数的每一位:只有在对应位置上都是 1 的情况下,结果才是 1。如果任意一位是 0,结果就是 0。
例如:0101&0011=0001,只有最后一位都是1。 - 按位或运算符(|)
按位或运算会把对应的两个数的每一位进行比较,只要有一位是 1,结果的该位就为 1。
例如:0101|0011=0111。只有一位上没有1。
通过上面所介绍的二进制运算,就可以理解:
通过state | (1 << u),将 state 的第 u 位设置为 1,而 state 中的其他位保持不变(不会影响之前递归层的结果)。
通过state >> i & 1,用来比较二进制数state第i位的数字是否为1。
这样就可以用一个整数 state 来表示数的选择情况,它将多个布尔值整合到一个整数中,简化了代码结构。
递归流程的理解
由于每一层递归都可能选择或不选择当前数,所以会产生不同的选择方案。理解递归树的展开过程以及如何逐步构建出所有可能的组合方案是解决本题的关键。我们可以画一个递归树用来帮助我们理解代码执行流程:
dfs(0, 000)
/ \
不选1 -> dfs(1, 000) dfs(1, 001) <- 选1
/ \ / \
不选2 -> dfs(2, 000) dfs(2, 010) dfs(2, 001) dfs(2, 011) <- 选2
/ \ / \ / \ / \
不选3 -> dfs(3,000) dfs(3,100) dfs(3,010) dfs(3,110) dfs(3,001) dfs(3,101) dfs(3,011) dfs(3,111) <- 选3
| | | | | | | |
null 3 2 2,3 1 1,3 1,2 1,2,3
由于state打印时,是按照从小到大遍历的,所以输出的数自然是按照升序排列,满足题目要求。
有了对上面这题的理解,就能很容易理解下面这题~~
洛谷U113177 递归实现排列型枚举
题目描述
把 1~n 这 n(n<10) 个整数排成一行后随机打乱顺序,输出所有可能的次序。
输入格式
一个整数n。
输出格式
按照从小到大的顺序输出所有方案,每行1个。 首先,同一行相邻两个数用一个空格隔开。其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。
样例 #1
样例输入 #1
3
样例输出 #1
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
主要思路
- 选择状态表示:
- 使用一个二进制数 state 来记录当前已经选择的数字。
- state 的第 i 位为 1 表示第 i 个数字已经被选择。
- 递归生成排列:
- 递归函数 dfs(u, state) 表示我们正在生成第 u 个位置的数字,并记录当前已选数字的状态 state。
- 对每个位置 u,我们依次尝试放入 1 到 n 的数字,如果数字尚未被选过,则将其加入当前路径 path 中,递归到下一层。
- 回溯:
- 每次递归完成后,我们将选择的数字从路径 path 中移除,以便尝试其他的数字组合。
#include <iostream>
#include <vector>
using namespace std;
int n;
vector<int> path; // 用于存储当前排列路径
// 深度优先搜索函数
void dfs(int u, int state) {
// 递归结束条件:当前已选择的数字数等于 n
if (u == n) {
for (int i=0;i<n;i++) // 输出当前排列
cout << path[i] << ' ';
cout << endl;
return;
}
// 遍历 1 到 n 的所有数字,尝试将其加入排列
for (int i = 0; i < n; i++) {
// 检查数字 i+1 是否已被选中,如果已选则跳过
if (!(state >> i & 1)) {
path.push_back(i + 1); // 将数字 i+1 加入当前排列路径
dfs(u + 1, state | (1 << i)); // 更新状态并递归到下一层
path.pop_back(); // 回溯,移除当前选择的数字
}
}
}
int main() {
cin >> n;
dfs(0, 0); // 从第一个位置开始,初始状态为 0(未选中任何数字)
return 0;
}
代码理解
递归函数 dfs(u, state)
- u:当前递归的深度,即正在选择第 u+1 个位置的数字。
- state:一个整数,使用二进制表示当前选中的状态。例如,state = 101 表示数字 1 和 3 已经被选中。
if (u == n)
- 当 u 达到 n 时,表示我们已经选好了 n 个数字,形成了一个完整的排列,因此可以输出 path 中的数字。
for (int i = 0; i < n; i++)
- 遍历数字 i+1,检查该数字是否已被选中。
- 若 state 的第 i 位为 0,表示数字 i+1 尚未被选中,可以加入当前排列。
state | (1 << i)
- 使用按位或运算将第 i 位设为 1,表示选择了数字 i+1,并将新状态传递给下一层递归。
path.push_back(i + 1) 和 path.pop_back()
- push_back:将数字 i+1 放入容器 path 中。
- pop_back:回溯时移除path的尾元素(当前数字),以便下一次递归尝试不同的排列组合。
递归树(部分):
dfs(0, 000) [path: []]
|
+----------------+----------------+
| |
选择 1 选择 2
| |
dfs(1, 001) [path: [1]] dfs(1, 010) [path: [2]]
| |
+----------+-----------+ +--------+----------+
| | | |
选择 2 选择 3 选择 1 选择 3
| | | |
dfs(2, 011) [path: [1, 2]] dfs(2, 101) [path: [1, 3]] dfs(2, 110) [path: [2, 3]]
| | | |
+------+-------+ +-------+-----+ +---+-----+ +---+-----+
| | | | | | | |
选择 3 回溯 选择2 回溯 选择3 回溯 选择1 回溯
1,2,3 1,3,2 2,1,3 2,3,1
难点分析
- 递归和回溯结合:递归进入下一层选择,而在递归返回时(通过 return 语句)触发回溯。
- 每层递归都有一个独立的 for 循环,尝试选择不同的数字。当 for 循环结束时,如果当前层没有其他选择,递归自动返回到上一层,进行回溯。
- 字典序输出:因为每一层递归都是按顺序选择数字(从小到大遍历),所以输出自然符合字典序排列。
以上两题用递归实现指数型和排列型枚举,主要理解了深度优先(dfs)的思想和递归回溯。