团队训练(三)

团队训练(三)-- 递归与递推(1)

开篇先吐槽自己一波,这周找的题目有点恶,递归本来就不太熟练,并且挺难理解的,然后选题时发现有些题感觉还可吧,比较经典,就直接丢进来,没想到至今未能AC,真是太有意思了,此外,附加题也够恶,真的是狼狈的一周,我在解题的时候我就在想,这周主讲的队友可能要凉了,不过周末的时候他居然差不多把所有题A掉,我只能说队友Tql。

递归的精髓:

  • 寻找前后递推关系
  • 寻找递归结束标志
  • 检查特判

例题:

1.奇怪的汉诺塔
题目

汉诺塔问题,条件如下:

1、这里有A、B、C和D四座塔。

2、这里有n个圆盘,n的数量是恒定的。

3、每个圆盘的尺寸都不相同。

4、所有的圆盘在开始时都堆叠在塔A上,且圆盘尺寸从塔顶到塔底逐渐增大。

5、我们需要将所有的圆盘都从塔A转移到塔D上。

6、每次可以移动一个圆盘,当塔为空塔或者塔顶圆盘尺寸大于被移动圆盘时,可将圆盘移至这座塔上。

请你求出将所有圆盘从塔A移动到塔D,所需的最小移动次数是多少。

输入格式
没有输入

输出格式
对于每一个整数n(1≤n≤12),输出一个满足条件的最小移动次数,每个结果占一行。

输入样例:
没有输入

输出样例:
参考输出格式

解析:这道题主要是建立在三个塔模型之上的吧,之前在大一上看递归的时候第一次接触汉诺塔模型,当时的题目主要是三柱汉诺塔,然后是要列举出最少的移动次数的过程,当时就琢磨挺久的,打算准备单独拿出来总结一下,三柱的汉诺塔有一个规律,假设a[i]是三柱汉诺塔问题中开始塔的个数为i至少需要移动的i次才能到达目标塔,a[1] = 1,a[2] = 3,a[3] = 7,a[4] = 15 ... a[i] = 2 * a[i-1] + 1。然后四塔的情况是先移动j个塔到B盘(相当于枚举吧),然后剩下(i - j)个塔变成三塔问题,移动完(i - j)个塔到D盘后再将B盘的j个塔移动到D盘(此时是四塔问题),然后取个min就行。不过这个dp我的确没能想出来,目前仅仅是肤浅的理解,等有更深层的理解再做一个总结吧。

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int n;
int dp[15], f[15];
int main()
{
	dp[1] = 1;
	for(int i = 2; i <= 12; i++)
		dp[i] = dp[i-1] * 2 + 1; //三柱汉诺塔至少移动次数 
	memset(f, 0x3f ,sizeof(f));
	f[1] = 1;
	for(int i = 1; i <= 12; i++)
		for(int j = 1; j < i; j++)
			f[i] = min(f[i], f[j] * 2 + dp[i-j]);
	for(int i = 1; i <= 12; i++)
		printf("%d\n", f[i]);	
	
	return 0;
}


2.未名湖边的烦恼
题目

每年冬天,北大未名湖上都是滑冰的好地方。北大体育组准备了许多冰鞋,可是人太多了,每天下午收工后,常常一双冰鞋都不剩。
每天早上,租鞋窗口都会排起长龙,假设有还鞋的m个,有需要租鞋的n个。现在的问题是,这些人有多少种排法,可以避免出现体育组没有冰鞋可租的尴尬场面。(两个同样需求的人(比如都是租鞋或都是还鞋)交换位置是同一种排法)

输入格式
两个整数,表示m和n
输出格式
一个整数,表示队伍的排法的方案数。

样例输入
3 2
样例输出
5

解析:这道题有三种解法,不过准确的说是两种,毕竟dfs也是递归思想。
1.dfs解法:主要思想就是将还鞋人数,租鞋人数,还有剩余鞋数的可能遍历出来。有两种情况,第一种是当租鞋窗口没有鞋时只能排还鞋的人,第二种是当租鞋窗口有鞋子可租的时候排谁都可以。结束的条件就是当租鞋的人是n个,还鞋的人是m - 1的时候无论还有没有鞋子都可以排一个还鞋,或者是当还鞋的人是m,租鞋的是n-1,说明还有一个租鞋的人没排,那这时候窗口必须要剩有鞋子给租,假如有就满足条件。然后稍微进行一下剪枝,跑的快一些。
2.递归:从后往前来推,要么最后一个是租鞋的人,要么是还鞋的人(递推关系),当租鞋的人数大于还鞋的人数此时不符号条件,结束递归,当租鞋的人为0,无论有没有还鞋的人那么都是一种可能(结束条件)。
3.dp解法:
方程: * dp[i][0] = 1;
* dp[i][j] = dp[i][j-1] + dp[i-1][j]; //也就是要么排一个租鞋的人,要么排一个还鞋的,不过还鞋的必须比租鞋的人多

dfs解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long mod = 1e9 + 7;
const long long inf = 0x3f3f3f3f;
int cnt = 0;
int m, n;
void dfs(int rt, int br, int num)//还鞋人数,租鞋人数,剩余鞋数
{
	if((rt == m - 1 && br == n) || (rt == m && br == n-1 && num != 0))
	{
		cnt ++;
		return;
	}
	if(num < 0 || rt > m || br > n) //可行性剪枝 
		return;
	if(num == 0) //当租鞋窗口没有鞋时只能排还鞋的人 
		dfs(rt + 1, br, num + 1);
	if(num > 0)
	{
		dfs(rt + 1, br, num + 1); //排还鞋的人
		dfs(rt, br + 1, num - 1); //排借鞋的人 
	}
}
int main()
{
	scanf("%d%d", &m, &n); 
	dfs(0, 0, 0); //还鞋人数,租鞋人数,剩余鞋数
	printf("%d", cnt); 
	return 0;
}

递归解法:

int f(int m, int n)
{
    if (m < n) //当队伍里还鞋人数m小于租鞋人数n时,无论如何摆放,都不合法
        return 0;
 	else if (n == 0) //当队伍里的租鞋人数为0时如果还有还鞋的,都只有一种排法 
        return 1;
    else 
        return f(m - 1, n) + f(m, n - 1);
}

dp解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int m, n;
int dp[20][20];
int main()
{
	scanf("%d%d", &m, &n);
	for(int i = 1; i <= m; i++)
		dp[i][0] = 1; // 只要租鞋的人为0,还鞋的人 >= 1,方案数均为一种. 
	for(int i = 1; i <= m; i++)
		for(int j = 1; j <= i; j++) //j <= i 控制租鞋的人不能大于还鞋的人,一旦大于,排队方案则为0 
			dp[i][j] = dp[i][j-1] + dp[i-1][j];
	printf("%d", dp[m][n]);
	return 0;
} 

3.猴子分苹果
题目

  秋天到了,n只猴子采摘了一大堆苹果放到山洞里,约定第二天平分。这些猴子很崇拜猴王孙悟空,所以都想给他留一些苹果。第一只猴子悄悄来到山洞,把苹果平均分成n份,把剩下的m个苹果吃了,然后藏起来一份,最后把剩下的苹果重新合在一起。这些猴子依次悄悄来到山洞,都做同样的操作,恰好每次都剩下了m个苹果。第二天,这些猴子来到山洞,把剩下的苹果分成n分,巧了,还是剩下了m个。问,原来这些猴子至少采了多少个苹果。
输入格式
  两个整数,n m
输出格式
  一个整数,表示原来苹果的数目
样例输入
5 1
样例输出
15621
数据规模和约定
0<m<n<9

解析:这道题当初推出了递推方程,不过不会巧用,后来咨询各路神仙,最终得出正解吧。首先安利一个有趣的数学题讲解:猴子分桃,这是李政道教授当时给中科大少年班出的一道数学题,这道题可以用这种方法做引申,不过我还没想好怎么去理解,所以还是按照我思路来。
首先我们可以推出第(i+1)个猴子吃完m个并且藏起来他自己那份之后还剩下的( f(i) - m ) * ( (n - 1) / n ),也就是递推式是f(i + 1) = ( f(i) - m ) * ( (n - 1) / n ),然后我们移向一下f(i) = ( f(i+1) * n / (n - 1) ) + m。然后我当时就在想终止条件是什么,一直卡住,刚开始想是f(n) % n = f(n + 1) = m,但发现一直行不通,最后的解决方案是利用一个巧妙的枚举,首先,当第n个猴子吃完m个并且藏起来他自己那份之后至少还会剩n + m个,这样第二天分成n份就还剩下m个(满足条件),不过这样前面的f(x)不一定会有整数解,所以我们可以枚举当第n个猴子吃完m个并且藏起来他自己那份之后可能还会剩(kn + m)个,直到第一次解出所以的f(x)有整数解即可。

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int f[15];
int n, m;
int main()
{
	scanf("%d%d", &n, &m);
	f[n] = m + n; //当第n个猴子拿完他的苹果至少有m + n个去给第二天分 
	for(int i = n - 1; i >= 0; i--)
	{	
		f[i] =	(f[i+1] * n / (n - 1)) + m;
		if( ( (f[i] - m) * (n - 1) / n) != f[i+1]) //if(f[i+1] * n % (n-1) != 0)
		{
			f[n] += n;
			i = n;
		}
	}
	printf("%d", f[0]);
	return 0;
} 

4.瓷砖铺放
题目
有一长度为N(1<=N<=10)的地板,给定两种不同瓷砖:一种长度为1,另一种长度为2,数目不限。要将这个长度为N的地板铺满,一共有多少种不同的铺法?
  例如,长度为4的地面一共有如下5种铺法:
  4=1+1+1+1
  4=2+1+1
  4=1+2+1
  4=1+1+2
  4=2+2
  编程用递归的方法求解上述问题。
输入格式
  只有一个数N,代表地板的长度
输出格式
  输出一个数,代表所有不同的瓷砖铺放方法的总数
样例输入
4
样例输出
5

解析:
递归解法:这道题就把爬楼梯问题换了一个包装,爬楼梯是要么一次爬一格要么一次爬两格,就两种方案,当剩下一格时只有一种方法,剩下两格时有两种方法,这里特别注意,必须要有剩下两格的结束条件,假设没有,会造成f(2-1) + f(2-2) 这种方案出现,f(0) 没有结束条件,会无限循环下去,不过可以加一个当楼梯数为0的时候方案数为1,也就是什么原地不动的方案,这个比较扯,铺瓷砖也是这个理。
dp解法:
方程:* dp[1] = 1;
* dp[2] = 2;
* dp[i] = dp[i-1] + dp[i-2];

递归解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int f(int n)
{
	if(n < 0)
		return 0;
	if(n <= 2)
		return n;
	return f(n - 1) + f(n - 2); //选择走一阶/走两阶 
}


int main()
{
	int n;
	scanf("%d", &n);
	int cnt = f(n);
	printf("%d", cnt);
	return 0;
}

dp解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int main()
{
	int n, dp[35];
	scanf("%d", &n);
	dp[1] = 1, dp[2] = 2;
	for(int i = 3; i <= n; i++)
		dp[i] = dp[i-1] + dp[i-2];
	printf("%d", dp[n]);
	return 0;
}

5.递归实现排列型枚举
题目

把 1~n 这 n 个整数排成一行后随机打乱顺序,输出所有可能的次序。

输入格式
一个整数n。

输出格式
按照从小到大的顺序输出所有方案,每行1个。

首先,同一行相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面。

数据范围
1≤n≤9

输入样例:
3

输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1

解析:整体思路是dfs,主要把走过的数进行一个标记,接着进行一个回溯即可,截止条件为走到第n + 1步。

AC代码:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int n;
int vis[15], val[15];
void dfs(int step)
{
	if(step == n + 1) //枚举到最后一步 (接下来无数可枚举)
	{
		for(int i = 1; i <= n; i++)
			printf("%d%c", val[i], i == n ?'\n':' ');
		return;
	} 
	for(int i = 1; i <= n; i++)
	{
		if(!vis[i]) //判断之前是否枚举过
		{
			vis[i] = 1; //标记已枚举过i 
			val[step] = i; //记录第x步的数
			dfs(step + 1); //向下搜索
			//回溯
			vis[i] = 0;
			val[step] = 0; 
		} 
	}
}

int main()
{
	scanf("%d", &n);
	dfs(1);
	return 0;
}

6.数的计算
题目

我们要求找出具有下列性质数的个数(包含输入的自然数n):

先输入一个自然数n(n≤500),然后对此自然数按照如下方法进行处理:

1、不作任何处理;

2、在它的左边加上一个自然数,但该自然数不能超过原数的一半;

3、加上数后,继续按此规则进行处理,直到不能再加自然数为止.
Input
1个自然数n(n≤1000)
Output
1个整数,表示具有该性质数的个数。
Sample Input
6
Sample Output
6

备注:
满足条件的数为
6,16,26,126,36,136

解析:
解法一:记忆剪枝 + 递归
开一个f_remember数组对之前求过符合条件的f[n]进行几个记忆优化,基本思路就是对一个数进行分半,然后从1 - n/2的数所产生的符合条件的数进行累加,最后再加上自己,比如样例6,实际上是6/2 = 3,1-3进行遍历,因为1-3都满足添加到6的左侧,所以就有16,26,36,接着2/2 = 1, 也就有1符合添加到2的左侧,这时就有126产生,接着又有3/2 = 1,这时就有136的产生。
解法二:递推
f[1] = f[0] + 1;
f[2] = f[1] + 1;
f[3] = f[1] + 1;
f[4] = f[2] + f[1] + 1;
f[5] = f[2] + f[1] + 1;
f[6] = f[3] + f[2] + f[1] + 1;
解法三:dp
用f[i]代表前i个数共有的方案数,那方程则为:f[i] = f[i-1] + f[i/2] + 1,也就是前(i-1)的一共方案数加上前i/2的方案数再加上自己本身,其实前i/2的方案数就是第i个数所需的条件。

递归解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int f_remember[1005];
int f(int n)
{
	int cnt = 0;
	if(n == 1)
		return 1;
	if(f_remember[n])
		return f_remember[n];
	for(int i = 1; i <= n / 2; i++)
		cnt += f(i);
	return f_remember[n] = cnt + 1; //还要加上自己本身 
}

int main()
{
	int n, ans = 0;
	scanf("%d", &n);
	ans = f(n);
	printf("%d", ans);
	return 0;
}

递推解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int f[1005];

int main()
{
	int n;
	memset(f, 0, sizeof(f));
	scanf("%d", &n);
	for(int i = 0; i <= n; i++)
	{
		for(int j = 1; j <= i / 2; j++)
		{
			f[i] += f[j];
		}
		f[i] ++; //加上本身 
	}
	printf("%d", f[n]);
	return 0;
}

dp解法:

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
typedef long long LL;
const int maxn = 1e5 + 7;
const int maxm = 2e5 + 7;
const long long inf = 0x3f3f3f3f;
const long long mod = 1e9 + 7;
int f[1005]; //f[i]代表0 - i 共有几种解法 
int main()
{
    int n;
    scanf("%d", &n);
   	f[0] = 1, f[1] = 1;
    for(int i = 2; i <= n; i++)
        f[i] = f[i-1] + f[i/2] + 1;
    printf("%d", f[n] - f[n-1]);
}

猜你喜欢

转载自www.cnblogs.com/K2MnO4/p/12688912.html