【BHOJ/HDU 栈 + 回溯题型总结】栈 | 回溯 | 递归 | DFS | CGUZ | N

                    【刷题总结】BHOJ/HDU 栈 + 回溯题型总结

当肖老师刚把这个算法告诉给我们的时候,因为不是很了解,所以大家都是处于不明觉厉(一脸懵逼)的状态,直到后来对数据结构的感觉培养起来之后才悟出其中的高明和美感。

 

下面是运用此算法很经典的几道题。有生成排列的,也有生成组合的,还有按特殊方式生成序列的。总之这个算法就是用来生成这些有特定限制的序列的。当然其中有些题也有其他的一些解法,只是这里专门来讲此算法的应用,因此择其共性总结之。

目录:

【BHOJ 229】尾声·织梦行云(难)

【BHOJ 1059】选择排列

【HDU 1027】Ignatius and the Princess II

【BHOJ 1344】想吃糖嘛

【BHOJ 1227】Tarpe酋长的电话密码

【BHOJ 324】jhljx准备面试(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酋长的电话密码

核心:按序生成规则复杂的组合

URL:   【BHOJ 1227】Tarpe酋长的电话密码

时间限制: 1000 ms 内存限制: 65536 kb

总通过人数: 264 总提交人数: 301

题目描述

Tarpe酋长的记性很差,自己的密码都记不住,所以一般会在自己的座位前贴一张纸条来提示密码,纸条上只有一串数字。但是Tarpe酋长的密码是很长的字符串。

酋长的密码映射规则是这样的(和电话号码一样):
Aaron Swartz

酋长的问题是,给定一个数字字符串,按字典序返回数字所有可能表示的字符串。

特殊符号不用考虑

输入

一行,一个数字字符串(保证长度小于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)

核心:按特殊方式生成序列

URL:   【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("");
	}
}

猜你喜欢

转载自blog.csdn.net/weixin_42102584/article/details/83247420
今日推荐