递归实现指数型枚举(递归)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//https://www.acwing.com/problem/content/94/
#include <iostream>
#include <vector>

using namespace std;

int n;
vector<int> num;

void calc(int u)
{
if(u == n + 1)
{
for(auto a : num) cout << a << ' ';
cout << endl;
return;
}

num.push_back(u);
calc(u + 1);
num.pop_back();

calc(u + 1);
}

int main()
{
cin >> n;
calc(1);

return 0;
}

按照朴素做法来思考,每个数字都有选和不选两个方案。共2^n^个数字,所以是指数枚举。

所以,我们可以使用递归,每次枚举”选“和“不选”两个分支。


由于C语言函数调用是通过栈来实现的,所以这个相当于深度优先搜索(DFS)。

即先处理全部不选的情况,然后处理选一个的情况,选完后恢复现场。继续处理其它情况。

这就是递归的强大之处,将问题分为若干小问题,使用一个函数不断调用进行求解。

递归实现组合型枚举(递归)

只需在指数型枚举加入下列条件即可:

1
2
//https://www.acwing.com/problem/content/95/
if(num.size() > m || num.size() + (n - u + 1) < m) return;

这个操作也叫做搜索算法的剪枝,后面会讲解。

即:遇到不可能的答案,直接回溯,不再继续向下递归寻找。

通过这个优化操作,将上面的O(2^n^)降至O($C_n^m$)。

递归实现排序型枚举(递归)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//https://www.acwing.com/problem/content/96/
#include <iostream>

using namespace std;

const int N = 9;

int n, k;
int num[N];
bool choose[N];

void calc(int u)
{
if(u == n + 1)
{
for(int i = 0; i < n; i++) cout << num[i] << ' ';
cout << endl;
return;
}

for(int i = 1; i <= n; i++)
{
if(choose[i]) continue;

choose[i] = true;
num[k++] = i;

calc(u + 1);

k--;
choose[i] = false;
}
}

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

一共n!种情况,在每次递归中,把所有可用的数作为下一个数,然后求解剩余n-1个数次序问题。

典型的搜索问题,通过递归实现。注意记得保存现场和还原现场。

费解的开关(递推)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//https://www.acwing.com/problem/content/97/
#include <iostream>
#include <cstring>

using namespace std;

int T;
char g[5][5];
char backUp[5][5];
int dx[5] = { -1, 0, 1, 0, 0 }, dy[5] = { 0, 1, 0, -1, 0 };

void turnOn(int x, int y)
{
for (int i = 0; i < 5; i++)
{
int a = x + dx[i], b = y + dy[i];

if (a < 0 || a > 4 || b < 0 || b > 4) continue;

g[a][b] ^= 1;
}
}

int calc()
{
int ans = 0x3f3f3f3f;

for (int k = 0; k < 32; k++) //枚举第一行所有可能按下情况
{
memcpy(backUp, g, sizeof g);

int step = 0;

for (int i = 0; i < 5; i++)
{
if (k >> i & 1) turnOn(0, i), step++;
}

for (int i = 0; i < 4; i++)
{
for (int j = 0; j < 5; j++)
{
if (g[i][j] == '0') turnOn(i + 1, j), step++;
}
}

bool allLight = true;
for (int i = 0; i < 5; i++)
{
if (g[4][i] == '0')
{
allLight = false;
break;
}
}

if (allLight) ans = min(ans, step);

memcpy(g, backUp, sizeof backUp);
}

if (ans > 6) return -1;
else return ans;
}

int main()
{
cin >> T;
while (T--)
{
for (int i = 0; i < 5; i++)
for (int j = 0; j < 5; j++)
cin >> g[i][j];

cout << calc() << endl;
}

return 0;
}

比较有意思的一道题。给你一个5x5的矩阵,0代表关灯,1代表开灯。

每次开灯会导致上下左右和中间五个位置的开关变动(0->1,1->0)。

问能不能在6次操作内全部打开,若能,输出最少需要多少步操作。


很容易发现:

若第一行固定,并且第一行某个位置是数字是0,则第二行必定需要点击这个0对应的列的位置。

只有这样,才能在不影响第一行其它位置的情况下,将这个0修改为1。

以此类推,第i行确定,则第i+1行也就能随之确定。


因此,我们可以将第一行所有点击方案枚举出来,共2^5^ = 32种可能。

然后基于第一行每种可能的方案,递归出后一行直至最后一行的值。

此时,只需要判定最后一行是否全为1。

注意:有一个小细节,g[a][b] ^= 1,这里使用到了成对变化。

奇怪的汉诺塔(递推)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//https://www.acwing.com/problem/content/98/
#include <iostream>
#include <cstring>

using namespace std;

const int N = 15;

int d[N], f[N];

int main()
{
d[1] = 1;
for(int i = 2; i <= 12; i++)
{
d[i] = 2 * d[i - 1] + 1;
}

memset(f, 0x3f, sizeof f);
f[0] = 0;

for(int i = 1; i <= 12; i++)
{
for(int j = 0; j < i; j++)
{
f[i] = min(f[i], f[j] * 2 + d[i - j]);
}
}

for(int i = 1; i <= 12; i++) cout << f[i] << endl;

return 0;
}

汉诺塔问题,递推思想。简单的汉诺塔是有三个塔组成的。

每次移动的最优方案是:将前n-1个盘子移动到另一座塔,然后将第n个盘子移动,最后将n-1个放至最上面。

也就是说,d[n] = d[n - 1] * 2 + 1。

这道题有四个塔,移动前i个盘子时,是四座塔的问题,移动后n-i个盘子时是三塔问题。

可以先将前i个盘子移走,然后将n-i个盘子在三塔问题下移动,然后把前i个盘子放至最上面。