【刷题总结】BHOJ/HDU 栈 + 回溯题型总结
当肖老师刚把这个算法告诉给我们的时候,因为不是很了解,所以大家都是处于不明觉厉(一脸懵逼)的状态,直到后来对数据结构的感觉培养起来之后才悟出其中的高明和美感。
下面是运用此算法很经典的几道题。有生成排列的,也有生成组合的,还有按特殊方式生成序列的。总之这个算法就是用来生成这些有特定限制的序列的。当然其中有些题也有其他的一些解法,只是这里专门来讲此算法的应用,因此择其共性总结之。
目录:
【HDU 1027】Ignatius and the Princess II
【BHOJ 229】尾声·织梦行云(难)
核心:生成全排列
URL: 【BHOJ 229】尾声·织梦行云(难)
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 59 总提交人数: 63
题目描述
依次输出1-n的全排列
输入
多组测试数据。
每组数据只有一行,包含一个整数n(1≤ n ≤12)数据以一个0为结束
输出
对于每组数据,按照从小到大的顺序输出全排列。每组输出后多打一个空行。具体见样例。
输入样例
2
3
0
输出样例
12
21
123
132
213
231
312
321
分析
当然我们这里不选用 std::next_permutation...而是运用带栈递归的方式,后序遍历多叉树,当遍历到叶节点(栈中已经有n个数)的时候打印整个栈,然后回溯,继续生成下一个排列
AC代码
#include <cstdio>
#include <cstring>
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define PC putchar
void PRT(const int a){if(a>=10)PRT(a/10);putchar(a%10+48);}
constexpr int MN(1234);
int n;
int stack[MN], top;
bool used[MN];
void generPermutation()
{
if (top == n) // 到达搜索树的叶节点,打印排列
{
for (int i=0; i<top; i++)
PRT(stack[i]);
PC(10);
return;
}
for (int i=1; i<=n; ++i) // 需要按字典序输出,所以从小到大填充
{
if (!used[i])
{
used[i] = true;
stack[top++] = i;
generPermutation(); // 继续搜索
--top; // 回溯
used[i] = false; // 回溯
}
}
}
int main()
{
while (1)
{
sc(n)
if (!n) return 0;
generPermutation();
putchar(10);
}
}
【BHOJ 1059】选择排列
核心:生成限定选定个数的排列
URL: 【BHOJ 1059】选择排列
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 339 总提交人数: 358
题目描述
给定 n,m 输出从 1∼n 中选择 m 个数的所有排列。
要求按照字典序输出。
输入
单组数据。
一行,两个空格分隔的整数,分别表示 n,m(1 ≤ m ≤ n ≤ 8)。
输出
输出若干行,表示答案。
输入样例
3 2
输出样例
1 2
1 3
2 1
2 3
3 1
3 2
分析
稍微变了一下的排列题。不再是生成全排列了,而是可以额外选择一些数。
思路和 尾声·织梦行云(难)这个题几乎是一模一样的,只是把判断是否到叶节点的条件改成 top == m,其中 m 是要求选择数字的个数。
AC代码
#include <cstdio>
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define PC putchar
void PRT(const int a){if(a>=10)PRT(a/10);putchar(a%10+48);}
constexpr int MN(10);
int n, m;
int stack[MN], top;
bool used[MN];
void generPermutation(void)
{
if (top == m) // 到达叶节点,输出栈中的排列数
{
for (int i=0; i<top; i++)
PRT(stack[i]), PC(32);
PC(10);
return;
}
for (int i=1; i<=n; i++) // 遍历选择没用过的数,可以一直选到n,而不是m
{
if (!used[i])
{
used[i] = true;
stack[top++] = i;
generPermutation(); // 继续搜索
--top; // 回溯
used[i] = false; // 回溯
}
}
}
int main()
{
sc(n)sc(m)
generPermutation();
}
【HDU 1027】Ignatius and the Princess II
核心:生成第k小排列
URL: 【HDU 1027】Ignatius and the Princess II
Time Limit: 2000/1000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Total Submission(s): 11262 Accepted Submission(s): 6483
Input
输入到EOF。每次输入 N M (1<=N<=1000, 1<=M<=10000) 保证 M 合法(存在字典序第 M 小的排列)
Output
每组输入输出一行,是 N 个数的全排列中字典序第 M 小的排列。行末不能有空格否则 PE
Sample Input
6 4
11 8
Sample Output
1 2 3 5 6 4
1 2 3 4 5 6 7 9 8 11 10
Analysis & AC code
这道题又稍微变了一下限制,它仍然是要生成全排列,但是不要输出全部全排列,而是输出字典序第k小的排列
这道题官方解法应该是逆康托展开,当然也可以用 next_permutation 水过,
关于这道题的详细题解可以戳这里:
【HDU 1027】全排列 | 逆康托展开 | std::next_permutation | 栈 + 回溯搜索 | N
然后回到今天的主题上,栈 + 回溯搜索,那么代码还是跟之前的代码差不多,只是加了一个计数器而已:
#include <cstdio>
#include <cstring>
#define sc(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define se(x) {register char _c=getchar(),_v=1;for(x=0;_c<48||_c>57;_c=getchar())if(_c==45)_v=-1;else if(_c==-1)return 0;for(;_c>=48&&_c<=57;x=(x<<1)+(x<<3)+_c-48,_c=getchar());x*=_v;}
#define PC putchar
void PRT(const int a){if(a>=10)PRT(a/10);putchar(a%10+48);}
constexpr int MN(1234);
int n, m, cnt;
int stack[MN], top;
bool used[MN], printed;
void generPermutation()
{
if (top == n) // 到达叶节点
{
++cnt; // 计数器++
if (cnt == m) // 得到第k小排列,输出
{
for (int i=0; i<top-1; i++)
PRT(stack[i]), PC(32);
PRT(stack[top-1]), PC(10);
printed = true;
return;
}
}
for (int i=1; i<=n; ++i)
{
if (!used[i])
{
used[i] = true;
stack[top++] = i;
generPermutation(); // 继续搜索
if (printed)
return;
--top; // 回溯
used[i] = false; // 回溯
}
}
}
int main()
{
while (1)
{
se(n)sc(m)
memset(used, false, (n+1) * sizeof(*used));
cnt = top = 0;
printed = false;
generPermutation();
}
}
【BHOJ 1344】想吃糖嘛
核心:生成全部组合
URL: 【BHOJ 1344】想吃糖嘛
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 321 总提交人数: 456
题目描述
给n种糖和它们各自的价格,只准选m种,要输出所有不同的方案,并给出每种方案的总价格
输入
第一行输入两个整数,分别为糖总种类数 n 和允许选择的种类数 m(0< n <11,0 < m <= n)
接下来一行,输入 n 个整数代表 n 种糖的价格,价格为正数且保证运算在int范围内
输出
每种组合输出一行,为糖的编号组合与价格,组合输出顺序按字典序输出。组合与输出间有一个空格。
输入样例
5 3
1 2 3 4 5
输出样例
123 6
124 7
125 8
134 8
135 9
145 10
234 9
235 10
245 11
345 12
分析
这道题是另一大题型:生成组合数 的典型代表。当然也可通过栈 + 回溯来做。
生成排列数和生成组合数最大的区别就是:生成排列的过程中,每次填充下一个元素的时候,只要和栈中已有的元素不重复即可;而生成组合的过程中,每次填充的元素有明确规定的上下界。
比如从 1-5 中选三个数的排列的时候,如果当前栈中是 1 3,那么下一个可以是 2、4、5(只要避开已经选了的1、3即可),但是如果是在生成组合,那么下一个就只能是 4 或 5(下界是比 3 大 1 的 4,上界是 5 因为是从 1-5 选 3 个数,最后一个数最大就是 5)。
所以算法大框架还是不变的,只是填充下一个元素时的方式变化了一下而已。如果是生成组合数,那么:
- ① 下界是当前栈顶元素+1,如果栈空,则就是1(最开始也就是从1开始放的)
- ② 上界是 n - 当前还能放的元素的个数(m-top) + 1,比如 n==5,m==4,top==2,stack={1,2} 时,还能放 m - top == 2 个元素,那么下一个最大能放几呢?最大只能放到 n - 2 + 1 == 4,因为如果放 5 的话,那最后一个位置就没数放了。所以上界就是 n - (m-top) + 1
这样的话,思路就ok了。下面贴代码。
AC代码
#include <cstdio>
#include <cstring>
void PRT(const int a){if(a>=10)PRT(a/10);putchar(a%10+48);}
constexpr int MN(13);
int n, m;
int price[MN];
int stack[MN], top;
void DFS(void)
{
if (top == m) // 到达叶节点,打印栈中元素
{
int sum = 0;
for (int i=0; i<top; ++i)
{
PRT(stack[i]);
sum += price[stack[i]];
}
putchar(32);
PRT(sum);
putchar(10);
return;
}
int lower_bound = 1; // 生成下界
int upper_bound = n-(m-top) + 1; // 生成上界
if (top)
lower_bound = stack[top-1] + 1;
for (int i=lower_bound; i<=upper_bound; ++i) // 字典序填充
{
stack[top++] = i;
DFS(); // 继续搜索
--top; // 回溯
}
}
int main()
{
scanf("%d %d", &n, &m);
for (int i=1; i<=n; i++)
scanf("%d", price + i);
DFS();
}
【BHOJ 1227】Tarpe酋长的电话密码
核心:按序生成规则复杂的组合
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 264 总提交人数: 301
题目描述
Tarpe酋长的记性很差,自己的密码都记不住,所以一般会在自己的座位前贴一张纸条来提示密码,纸条上只有一串数字。但是Tarpe酋长的密码是很长的字符串。
酋长的密码映射规则是这样的(和电话号码一样):
酋长的问题是,给定一个数字字符串,按字典序返回数字所有可能表示的字符串。
特殊符号不用考虑
输入
一行,一个数字字符串(保证长度小于1010且不包含0和1)
输入字符在 '2'
到 '9'
之间。
输出
按字典序输出所有可能的字符串,每种结果一行,最后需要换行
输入样例
23
输出样例
ad
ae
af
bd
be
bf
cd
ce
cf
分析
这道题花架子比较多。剖开这些表面东西,其实也是在生成组合,不过是生成顺序限制条件多一些。所以仍然是我们这个算法大显身手的地方。
这道题的限制条件是:
- ① 字母组合是一块一块、按顺序生成的。比如输入 23,那生成的时候就是先生成 2 对应的几个字母
- ② 每个块又是按字典序每次生成一个字母的。比如生成 2 这个块的时候,先是 a,然后是b,然后才是c
-
③ 重点:整体顺序是:生成每一个后面的块的所有种字母之后,才继续生成前面的块的下一种字母。比如输入 234,那就是输出adg adh adi,然后继续第二块变成e,输出aeg aeh aei,然后继续第二块变成f,输出afg afh afi,然后第二块完了,继续第一块的下一个,第一块变成 b,输出bdg bdh bdi...
只要按照限制条件生成就行。另外这里可以先把号码盘布置好(定义一个结构体数组)
因为这道题比较麻烦,所以还是贴出全部代码
AC代码
#include <iostream>
#include <string>
constexpr int ML(13);
struct Button
{
char first, last;
};
const Button BUTTON[]
{
{0, 0},
{0, 0},
{'a', 'c'},
{'d', 'f'},
{'g', 'i'},
{'j', 'l'},
{'m', 'o'},
{'p', 's'},
{'t', 'v'},
{'w', 'z'}
};
int len;
int num[ML];
char stack[ML];
int top;
void DFS()
{
if (top == len) // 到达搜索树的叶节点
{
stack[top] = '\0';
puts(stack);
return;
}
char first = BUTTON[num[top]].first; // 生成下界
char last = BUTTON[num[top]].last; // 生成上界
for (char ch = first; ch <= last; ++ch) // 字典序填充
{
stack[top++] = ch;
DFS(); // 继续搜索
--top; // 回溯
}
}
int main()
{
std::string temp;
getline(std::cin, temp);
len = temp.length();
for (int i = 0; i < len; ++i)
num[i] = temp[i] - '0';
top = 0;
DFS();
}
【BHOJ 324】jhljx准备面试(II)
核心:按特殊方式生成序列
时间限制: 1000 ms 内存限制: 65536 kb
总通过人数: 82 总提交人数: 96
题目描述
jhljx最近正在准备面试,一天他听林老师说起大家正在学习栈,尤其是括号匹配问题是栈这个知识点中的经典问题。jhljx想了想,括号匹配确实非常神奇,但是究竟有多少个正确的括号串呢?还是问问神奇的海螺吧~
输入
输入多组数据。
每组数据为一个正整数 n (1<= n <=10),表示有n对括号。
输出
按照字典序升序输出所有正确匹配的括号串。每组数据之间用空行隔开。
输入样例
2
3
输出样例
(())
()()
((()))
(()())
(())()
()(())
()()()
分析
这道题的标准解法是栈,这个会在之后的博客中补充。今天我们的重点仍然是带栈回溯搜索。
这道题生成方式既不是排列,也不是组合。它的限制条件比较特殊。
首先肯定不是任意生成,如果是任意生成,那就会有诸如 )))))((((( 这样的输出。所以我们应该找一下约束条件:
如果当前栈内元素少于 2*n,需要继续填充,那么
- ① 前括号最多添加到 n 个,否则最终无法得到恰好 n 对括号(比如得到 ((((() 这样前括号过多的序列)
- ② 后括号最多添加到当前前括号个数个,否则后括号将会没有对应前括号的匹配(比如得到 (()))) 这样后括号过多的序列)
- ③ 判断 ② 的时候,“当前”前括号个数不包括本次的添加(稍后会在代码中说明)
- ④ 先考虑添加前括号,再考虑添加后括号,这样才是按字典序输出
这样思路就明晰了,下面贴出代码。
AC代码
#include <stdio.h>
#define MN 132
int n;
char stack[MN], top;
void generBrackets(int n_front, int n_back)
{
if (top == 2*n)
{
for (int i=0; i<top; i++)
putchar(stack[i]);
putchar(10);
return;
}
if (n_front < n)
{
stack[top++] = '(';
generBrackets(n_front + 1, n_back); // 传给下一层的时候 +1,而不是自己++,或者自己++后再--
--top; // 总之不能让下面判断 n_back < n_front 的时候 n_front 改变
} // 否则会多加后括号,因为这次加的前括号不属于这一层递归!
if (n_back < n && n_back < n_front)
{
stack[top++] = ')';
generBrackets(n_front, n_back + 1);
--top;
}
}
int main()
{
while(~scanf("%d", &n))
{
top = 0;
generBrackets(0, 0);
puts("");
}
}