算法基础——暴力递归(C++实现)

一、前言 

所谓的暴力递归就是尝试。尝试通过递归的方式得到所需要的结果,而不必去苛求递归进行的具体过程。换句话说,使用暴力递归的目的仅仅是通过多次尝试、整合得到最终的结果,只要结果正确,递归过程就正确。

暴力递归是动态规划的基础。

使用暴力递归的前提:

  • 可以把问题转化为规模缩小了的同类问题的子问题。
  • 有明确的不需要继续进行递归的条件(base case)。
  • 有当得到了子问题的结果之后的决策过程。
  • 不需要记录每一个子问题的解。

 二、汉诺塔问题

2.1 问题介绍

相传在古印度圣庙中,有一种被称为汉诺塔(Hanoi)的游戏。该游戏是在一块铜板装置上,有三根杆(编号A、B、C),在A杆自下而上、由大到小按顺序放置n个金盘。

游戏的目标:把A杆上的金盘全部移到C杆上,并仍保持原有顺序叠好。打印n层汉诺塔从最左边移动到最右边的全部过程。

操作规则:每次只能移动一个盘子,并且在移动过程中三根杆上都始终保持大盘在下,小盘在上,操作过程中盘子可以置于A、B、C任一杆上。

2.2 解题思路

本题采用分治+递归的模式,将大问题划分为小问题,递归解决。代码逻辑如下:

  1. 将n-1只碟子从A杆经C杆移动到B杆,B杆上的碟子大小应保持有序。
  2. 将A杆上的第n只碟子移动到C杆。
  3. 将B杆上的n-1只碟子经A杆移动到C杆,问题解决。

首先将i号圆盘上面的i-1个圆盘移动到过渡杆B杆,不用纠结计算机移动的过程,只需要知道最后的结果是第1~ i-1个圆盘都在B杆上即可;然后打印第i号盘从A杆移动到C杆的过程;随后将第1~ i-1个圆盘从B杆移动到C杆,同样不用纠结实现过程,只需要明确func函数的功能就是将圆盘从一个杆移动到另一个杆上。

核心代码为:

func(i - 1, A, C, B);//圆盘移动到B
printf("move %d from %c to %c\n", i, A, C);
func(i - 1, B, A, C);//将B上i-1个圆盘移动到C

值得注意的是递归通常伴随着回溯的过程。本题中递归的作用是分治,将大问题划分为小问题,例如我想将3只圆盘从A杆移动到C杆,需要先将较小的2只圆盘从A杆移动到B杆,然后将最大的圆盘放到C杆最下方;若要将较小的2只圆盘从A杆移动到B杆,需要先将最小的圆盘从A杆移动到C杆,然后将第二大的圆盘放到B杆最下方。

回溯就是将分治出来的子问题归并起来的过程。通过向下递归找到解决子问题的方法,然后将解决办法调用到上层函数,实现上层函数的功能。

2.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

//递归函数,目的是将i个圆盘从start移动到end
void func(int i, char start, char other, char end)
{
	if (i == 1)//只有一个圆盘,递归结束
		printf("move 1 from %c to %c\n", start, end);
	else
	{
		func(i - 1, start, end, other);//将i号圆盘上面的i-1个圆盘移动到other
		printf("move %d from %c to %c\n", i, start, end);
		func(i - 1, other, start, end);//将other上i-1个圆盘移动到end
	}
}

void hanoi(int n)
{
	if (n > 0)
	{
		func(n, 'A', 'B', 'C');
	}
}

int main()
{
	hanoi(3);
}


三、打印一个字符串的全部子序列

3.1 问题介绍

题目给出一个字符串,要求打印出它所包含的所有子序列,包括空字符串。

例如给出字符串“abc”,则需要打印出“abc”,“ab”,“ac”,“a”,“bc”,“b”,“c”,“ ”。注意需要保持字符的相对顺序不变。

3.2 解题思路

对于每一个字符,都有选择和不选择两种情况,可以将整个流程想象为一个二叉树,每个节点代表一个字符,左右孩子代表选择或者不选择该字符。

0:此时有两种情况,要不要'a',如果要,子序列变为a;如果不要则还是空字符串。

1:此时有两种情况,要不要‘b’,加上上一步的两种这里就有4种情况,分别是“a,ab,b,空”。

2:此时有两种情况,要不要‘c’,加上上一步的四种情况这里就有8种情况。

具体的实现过程中,复用chs字符串记录当前子串。两条路分别用两个递归函数表示,第一个递归函数实现要当前字符的路,即始终选择右子树;第二个递归函数实现不要当前字符的路,即始终选择左子树。实现过程代码如下:

	process(chs, i + 1);//要当前字符的路
	char tmp = chs[i];
	chs[i] = 0;//将当前字符改为空
	process(chs, i + 1);//不要当前字符的路
	chs[i] = tmp;//恢复当前字符

简单解释一下流程,选择分支的过程实际上就是递归中“归”的过程。 还是以“abc”为例,进入第一行代码process中递归,最后的结果是chs = abc,i=2(走到叶子结点),因为i=strlen,所以输出abc后返回;进入二三行代码,将c去掉,chs = ab,i=2(相当于进入左子树的叶子结点了),因为i=strlen,所以直接输出ab后返回;最后将c加回去,chs = abc,i=2。

最底层递归(i=2)结束,返回第二层(i=1),此时chs = abc,i=1。然后str[i]=0,chs = ac,i=2;进入第二轮递归,不要c,输出a后返回……

3.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

void process(string chs, int i)
{
	if (i == chs.length())//求字符串长度
	{
		cout << chs << endl;
		return;
	}
	process(chs, i + 1);//要当前字符的路
	char tmp = chs[i];
	chs[i] = 0;//将当前字符改为空
	process(chs, i + 1);//不要当前字符的路
	chs[i] = tmp;//恢复当前字符
}

int main()
{
	string chs = "abc";
	process(chs, 0);
}

四、打印一个字符串的全排列

4.1 问题介绍

打印一个字符串的全部排列,要求不要出现重复的排列。本题与上题的不同之处在于本题中字符间的相对顺序可变,且所有字符都需要出现。

例如给出字符串“abc”,它的全排列包括abc,acb,bac,bca,cab,cba六种情况。

4.2 解题思路

主要思路:一个字符串,在遍历每一个字符时,该字符后面的每一个字符都可以替代它。

第一轮递归时pBegin和pChs都指向同一字符处,所以不改变字符位置,直接递归到底层,输出“abc”;然后开始回溯,上层递归中pBegin指向“b”,进入下一轮for循环pChs++后,pChs指向“c”;交换pChs和pBegin指向的字符,得到str=“acb”,继续递归到底层,输出“acb”。回溯到pBegin指向“c”,pChs指向“b”的状态,还原成str=“abc”,一号分支结束。

继续回溯到pBegin指向“a”的情况,pChs++后指向“b”,所以交换“a”和“b”,str=“bac”,进入二号分支,重复上面的操作即可。递归流程如下:

4.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

int visit[30] = { 0 };
void swap(char* i,char* j)
{
	char tmp = *i;
	*i = *j;
	*j = tmp;
}
//指针pStr指向整个字符串的第一个字符,pBegin指向当前做排列操作的字符串的字符
void process(char* pStr,char* pBegin)
{
	if (*pBegin=='\0')//指针指向结束符,表示一轮排列结束
	{
		cout << pStr << endl;
		return;
	}
	//pBegin指向当前正在操作的字符,pChs遍历pBegin后面的字符
	for (char* pChs = pBegin; *pChs != '\0'; pChs++)
	{
		swap(*pChs, *pBegin);//pBegin后面的字符都有机会到pBegin的位置上
		process(pStr, pBegin + 1);//走到分支的叶子结点
		swap(*pChs, *pBegin);//回溯还原场景,方便上层递归的下一次循环
	}
}

int main()
{
	char ptr[] = "abc";
	process(ptr, ptr);
}

五、栈转置问题

5.1 问题介绍

给你一个栈,请你逆序这个栈,不能申请额外的数据结构,只能使用递归函数。

例如:一个栈中依次压入1,2,3,4,5,那么从栈顶到栈底分别为5,4,3,2,1。将这个栈转置后,从栈顶到栈底为1,2,3,4,5。本题要求转置过程只能用递归来实现,并且不能使用额外的数据结构。

5.2 解题思路

主要思路:不能使用额外的数据结构,因此选择在递归中利用系统栈储存上层递归产生的信息。大体思路是需要一个方法,弹出并返回当前栈底元素,弹出的元素保存在系统栈里,也就是上层递归的临时变量里。循环直到栈中元素为空,然后从最下层递归依次把所有元素压入栈中,最终使元素逆序。本题代码里使用了“递归的递归”。

具体递归步骤:以栈1,2,3(1为栈底)为例,首先进入reverse函数,取得栈底元素1,然后不断递归,第2层递归中取出栈底元素2,第三层递归中取出栈底元素3,第四层递归是栈空,返回第三层递归,将第三层递归中取出的3压栈;返回第二层,将取出的2压栈;返回顶层,将取出的1压栈,实现栈转置。

getAndRemoveLastElement函数中同样采用上层递归中的临时变量来存储栈信息。第一层递归中result=3,第二轮递归中result=2,第三轮递归中result=1,然后发现栈空,返回第二层;第二层中的result=2,将2压栈,返回顶层;顶层中result=3,将3压栈,last赋值为1。这就实现了在不破坏栈的情况下取得栈底元素。

5.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<stack>
using namespace std;

//将栈底元素返回,同时将自下而上第二个元素压到栈底
int getAndRemoveLastElement(stack<int> &s)//在栈s内部完成转置过程
{
	int result = s.top();
	s.pop();//弹出栈顶元素
	if (s.empty())//base case
		return result;//返回栈底元素
	else
	{
		int last = getAndRemoveLastElement(s);
		s.push(result);//将剩余元素压栈
		return last;
	}
}

void reverse(stack<int> &s)
{
	if (s.empty())
		return;
	int i = getAndRemoveLastElement(s);//得到栈底元素
	reverse(s);
	s.push(i);//压栈,最先压入的是原先的栈顶元素
} 

int main()
{
	stack<int> s;
	for (int i = 0; i < 10; i++)
	{
		s.push(i);
	}
	reverse(s);
	while (!s.empty())
	{
		cout << s.top() << " ";
		s.pop();
	}
}

六、十进制数转换为字符串

6.1 问题介绍

规定1和A对应、2和B对应、3和C对应... 那么一个数字字符串比如"111",就可以转化为"AAA"、"KA"和"AK"。 给定一个只有数字字符组成的字符串str,返回有多少种转化结果。

因为“111”可以分割成“1,1,1”或“11,1”或“1,11”,所有有三种可能的转换情况。

6.2 解题思路

主要思路:面对每个数字,都思考两条路:第一条路是直接将这个数转换为字母,然后将剩下的数送入递归函数;第二条路是将这个数和它后面的数一起转换为一个字母,然后将剩下的数送入递归。但需要注意的是当“0”单独出现时,说明此路不通,因为0必须依附于它的前一个数;另外当第一个数是2的时候,如果后一个数大于6,则找不到对应的字母,因为只有26个英文字母,此路不通。

6.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<stack>
using namespace std;

int process(string chs, int i)
{
	if (i == chs.length())//能够进行到最后,存在一个结果
		return 1;
	if (chs[i] == '0')//遇到单独出现的0,说明此路不同,剪枝
		return 0;
	if (chs[i] == '1')
	{
		int res = process(chs, i + 1);//将1直接转换为A
		if (i + 1 < chs.length())
			res += process(chs, i + 2);//将1和后面的一个数一起转换
		return res;
	}
	if (chs[i] == '2')
	{
		int res = process(chs, i + 1);//将2直接转换为B
		if (i + 1 < chs.length() && chs[i + 1] >= '0' && chs[i + 1] <= '6')
			res += process(chs, i + 2);//将2和后面的一个数一起转换
		return res;
	}
	return process(chs, i + 1);//其他情况时直接自身转换
}

int main()
{
	string chs = "111";
	cout << process(chs, 0) << endl;
}

七、01背包问题

7.1 问题介绍

给定两个长度都为N的数组weights和values,weights[i]和values[i]分别代表 i号物品的重量和价值。给定一个正数bag,表示一个载重bag的袋子,你装的物品不能超过这个重量。返回你能装下最多的价值是多少?

7.2 解题思路

显然,当遇到一件物品时,有两种选择,第一种是将其放入背包,另一种是不要;当拿走一件物品时,当前物品总价值和总重量改变;不拿走时则不变。递归遍历每一件商品,返回最大价值。

base case:总重量大于背包承载力是强行终止,并返回上层函数;当遍历完所有物品后,结束一轮选择,返回上层函数。

需要注意的是,采用递归的做法实现01背包速度较慢,不适用于大多数题目,面对01背包题目时最好选择动态规划的做法。

7.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int w[100];//质量
int v[100];//价值
int n, weight;
int maxValue = 0;

//sum为总价值,nowWeight为当前物品质量,step表示遍历到第几个物品
void process(int sum, int nowWeight, int step)
{
	if (nowWeight > weight)//超重,需要回退
		return;
	if (step == n)//完成所有物品的选择
	{
		if (sum > maxValue)
			maxValue = sum;
		return;
	}
	process(sum + v[step], nowWeight + w[step], step + 1);//拿走该物品
	process(sum, nowWeight, step + 1);//不要该物品
}

int main()
{
	cin >> weight;
	cin	>> n;//背包大小and物品数量
	for (int i = 0; i < n; i++)
	{
		cin >> w[i];
		cin >> v[i];
	}

	process(0, 0, 0);
	cout << maxValue << endl;
	return 0;
}

八、纸牌游戏

8.1 问题介绍

给定一个整型数组arr,代表数值不同的纸牌排成一条线。玩家A和玩家B依次拿走每张纸 牌,规定玩家A先拿,玩家B后拿,但是每个玩家每次只能拿走最左或最右的纸牌,玩家A 和玩家B都绝顶聪明。请返回最后获胜者的分数。

【举例】 arr=[1,2,100,4]。 开始时,玩家A只能拿走1或4。如果开始时玩家A拿走1,则排列变为[2,100,4],接下来 玩家 B可以拿走2或4,然后继续轮到玩家A... 如果开始时玩家A拿走4,则排列变为[1,2,100],接下来玩家B可以拿走1或100,然后继 续轮到玩家A... 玩家A作为绝顶聪明的人不会先拿4,因为拿4之后,玩家B将拿走100。所以玩家A会先拿1, 让排列变为[2,100,4],接下来玩家B不管怎么选,100都会被玩家 A拿走。玩家A会获胜, 分数为101。所以返回101。 arr=[1,100,2]。 开始时,玩家A不管拿1还是2,玩家B作为绝顶聪明的人,都会把100拿走。玩家B会获胜, 分数为100。所以返回100。

8.2 解题思路

暴力递归,在每次选择的时候都分先手和后手。在我先手的时候,显然需要选择“当前选的牌加上下面n轮的总点数”较大的递归路线;在后手的时候,我永远获得的是较小牌的组合,因为对手选择时会保证我选择的牌总点数较小。

本题的关键在于递归函数的相互调用。当我先手时,我需要考虑让接下来后手的情况下能拿到更多点数;当我后手时,我需要考虑接下来先手的情况下能拿到更多点数。

总流程大概为:我先手拿走一张牌,局势马上转变为对手先手,我后手;对手拿走一张牌,局势又变化为我先手对手后手,一直交替往复。我后手时同理。

8.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;

int f(int arr[], int i, int j);
int s(int arr[], int i, int j);


int f(int arr[], int i, int j)//先手情况,当前我先选,i表示左指针,j表示右指针
{
	if (i == j)
		return arr[i];
    //我要使自己在面对i+1到j或者i到j-1范围内后手情况下的总点数最大
	return max(arr[i] + s(arr, i + 1, j), arr[j] + s(arr, i, j - 1));//返回“当前选择的牌+后手递归结果”中最大点数的情况
}
int s(int arr[], int i, int j)//后手情况,当前对手选,i表示左指针,j表示右指针
{
	if (i == j)
		return 0;
    //对方要使我在面对i+1到j或者i到j-1范围内先手情况下的总点数最小
	return min(f(arr, i + 1, j), f(arr, i, j - 1));//对手有最左边和最右边的两个选择,对手肯定会让我们先手的情况下总点数最小
}

int win(int arr[],int length)
{
	return max(
		f(arr, 0, length - 1),s(arr,0,length-1)//较大总点数的人获胜
	);
}

int main()
{
	int arr[] = { 1,100,2 };
	cout << win(arr, 3);
}

九、N皇后问题

9.1 问题介绍

N皇后问题是指在N*N的棋盘上要摆N个皇后,要求任何两个皇后不同行、不同列, 也不在同一条斜线上。 给定一个整数n,返回n皇后的摆法有多少种。

n=1,返回1。 n=2或3,2皇后和3皇后问题无论怎么摆都不行,返回0。 n=8,返回92。

9.2 解题思路

先将第一个皇后放在第一行的第一列上,符合题目要求

开始放置第二个皇后。放在第二行的第一个与第一行的皇后为同一列,不符合题意,继续向后搜素,放在第二列上面与第一个皇后在同一斜线上,不符合题意,继续向后搜素,发现放在第三列符合题意

开始放置第三个皇后。放在第三行的任意位置都会出现冲突,此时需要回溯,将第二个皇后放置在第四列,此时符合题意,继续放置第三个皇后,发现第三个皇后放置在第三行的第二列符合题意

继续放置第四个皇后。放在第四行的任意位置都会出现冲突,此时需要回溯,第三个皇后向后移动,发现依然不符合题意,继续回溯,第二行的皇后无法再向后移动,继续回溯,将第一个皇后向后移动到第二列,符合题意

移动第二个皇后,发现放在第四列符合题意

移动第三个皇后,发现放在第一列符合题意

移动第四个皇后,发现放在第三列符合题意

回溯结束。

9.3 代码实现

#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
bool isValid(int record[], int i, int j)//判断位置是否合法,record[i] = j意为第i行第j列有皇后
{
	for (int k = 0; k < i; k++)
	{
		if (j == record[k] || abs(record[k] - j) == abs(i - k))//斜率判断是否有两个皇后在一条斜线上
			return false;
	}
	return true;
}

int process(int i, int record[], int n)
{
	if (i == n)//base case 递归到最后一行
		return 1;
	int res = 0;
	for (int j = 0; j < n; j++)
	{
		if (isValid(record, i, j))
		{
			record[i] = j;//记录点位
			res += process(i + 1, record, n);
		}
	}
	return res;
}
int main()
{
	int record[8];
	cout << process(0, record, 4);
}

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值