1. 问题描述:
公园票价为5角。假设每位游客只持有两种币值的货币:5角、1元。
再假设持有5角的有m人,持有1元的有n人。
由于特殊情况,开始的时候,售票员没有零钱可找。
我们想知道这m+n名游客以什么样的顺序购票则可以顺利完成购票过程。
显然,m < n的时候,无论如何都不能完成; m>=n的时候,有些情况也不行。
比如,第一个购票的乘客就持有1元。
请计算出这m+n名游客所有可能顺利完成购票的不同情况的组合数目。
注意:只关心5角和1元交替出现的次序的不同排列,持有同样币值的两名游客交换位置并不算做一种新的情况来计数。
输入
一行:整数m和n,空格隔开。(m+n<=20)
输出
组合数目
样例输入
5 5
样例输出
42
2. 一看到题目是不是觉得有点似曾相识,没错,这也是未名湖畔那道题目转换场景之后的题目,只不过那道题目是关于溜冰鞋的,但是本质上是一模一样的,我们可以使用dfs来解决,因为问题规模完全不大,所以使用dfs来解决是完全不会超时的,而且dfs方便的是我们可以在调用的时候可以记录下中间的过程方便检查程序中的判断是否存在错误
很明显题目存在着两个平行的状态,接下来我们就需要考虑传入dfs方法的参数的数量了,一开始我写的时候只是传入了m, n两个参数,但是后面写的时候发现不行,因为涉及到在当前状态下是否能够继续搜索下去,因为题目要求要求解出能够找出零钱的排列,所以这个时候需要把调用dfs之前的状态下的m, n排列的人数给记录下来,方便后面判断是否可以继续搜索下去,这也就是dfs的提前剪枝,在调用之前预判这条路是否满足条件如果不满足那么不再搜索下去
我们假如确定了要使用dfs来解决之后那么画出调用的树那么可以很清楚的直到其中的调用情况,而且可看出dfs的出口是什么,需要传入的参数的是什么,参数如何变化的这些问题,下面是具体的代码:
import java.util.Scanner;
public class Main {
static int count = 0;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int n = sc.nextInt();
if(m - n >= 0){
dfs(m, n, 0, 0);
}
System.out.println(count);
sc.close();
}
private static void dfs(int m, int n, int count1, int count2){//dfs的出口每一次都是m等于0
//m = 0之后说明已经找到了一种排列的方法了
if(m == 0) {
if(count1 - count2 >= 0){
count++;
}
return;
}//下面n == 0判断的代码可以不用因为最终的情况是到达不了n = 0这个情况的,所以去掉也行
if(n == 0){
if(count1 - count2 >= 0){
count++;
}
return;
}//if语句中提前进行判断看这条路径是否能够走下去,假如不能那么久不会再搜索这条路径了
if(count1 - count2 >= 0){
dfs(m - 1, n, count1 + 1, count2);
}
if(count1 - count2 >= 0){
dfs(m, n - 1, count1, count2 + 1);
}
}
}
我们可以以一个简单的例子来进行分析问题,其中m = 4, n = 4分析的情况如下:
0
0.5 1 m = 4
0.5 1 0.5 1 m = 3
0.5 1 0.5 1 m = 3
0.5 1 ... m = 1(调用下一层的时候碰到递归出口那么结束调用退回这一层往
0.5 1 ... 下执行平行状态此时m = 1)
0.5 1... m = 1
0.5 1 ... m = 1 count1 = 3 count2 = 3两者相等那么尝试去执行平行状态dfs(...3,4)
但是调用的时候经过了if语句判断count1 < count2那么说明再调用下去这条路径就会排0.5的人数少于1的人数了就不可以了,那么经过if之后那么提前终止这条路径不再搜索下去,其他的平行状态也是这样进行分析
通过在调用dfs的前后或者输出其中的某个记录的变量值的变化的输出语句来进行分析也是对于深刻理解dfs的调用是很有帮助的
而且每一层对应的记录变量的值也是在动态地进行变化的,比如当前状态下数组的索引,记录中间过程的值List,数组这些也是在动态地进行变化的
所以退回到调用的这一层的时候m,n是不同的
3. 像上面这样使用dfs是比较简单的,有的情况下,调用dfs的过程需要记录下其中调用的中间过程,但是有的情况下调用dfs很简单,但是记录中间的过程会比较麻烦,像这道题目假如使用List来记录中间过程的值那么涉及到复制一个List的引用的过程,但是复制引用这个过程有点麻烦,除了List,像StringBuilder记录中间过程也是比较麻烦的,因为StringBuilder也类似于动态的数组涉及到引用也需要进行克隆但是过程比较麻烦。
我们不能够新创建一个List或者StringBuilder,然后把原来的List或者StringBuilder来进行复制,因为这样也是把原来的引用给了新创建的这个变量,修改新的变量的时候也会同事将旧的变量对应的值修改掉,所以这样做是不行的,这里我们采用String数组来进行记录,当碰到递归出口的时候把数组中存储的内容给输出来,然后把剩下的没有加进去数组里面的1也输出来那么就达到输出中间结果的目的了,其中数组赋值,然后dfs调用完返回到这一层的时候不需要进行回溯,因为可能平行状态下的元素会把当前数组原来填进去的值给覆盖掉,假如平行状态的元素不行那么会返回到上一层尝试把其他的适合的元素填充进去,所以当调用当这一层的时候假如有合适的元素那么最终这个数组中对应的元素就会被覆盖掉,假如尝试的全部元素不符合那么数组里面的元素是尝试的元素,但是不会输出到控制台上,所以不进行回溯是没有什么问题的,具体的代码如下:
import java.util.Scanner;
public class Main{
//发现使用List和StringBuilder来记录其中的情况再输出会比较麻烦而且对象不能直接赋值假如直接赋值的话那么
//接下来再修改新创建的变量也会修改掉原来的引用对应的值所以不能这样做
//下面我们是使用String数组来进行记录,这样更加方便而且数组不需要进行回溯
static int count = 0;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int m = sc.nextInt();
int n = sc.nextInt();
String rec[] = new String[m + n];
if(m - n >= 0){
dfs(m, n, 0, 0, rec, 0);
}
System.out.println(count);
sc.close();
}
private static void dfs(int m, int n, int count1, int count2, String rec[], int index){
if(m == 0){
if(count1 - count2 >= 0){
count++;
for(int i = 0; i < count1 + count2; i++){
System.out.print(rec[i] + " ");
}
for(int i = 0; i < n; i++){
System.out.print("1 ");
}
System.out.print("\n");
}
return;
}
if(count1 - count2 >= 0){
rec[index] = "0.5";
dfs(m - 1, n, count1 + 1, count2, rec, index + 1);
//System.out.println(index);
}
if(count1 - count2 >= 0){
rec[index] = "1";
dfs(m, n - 1, count1, count2 + 1, rec, index + 1);
}
}
对于记录中间过程有时候是需要进行回溯的,要不要进行回溯是要看对下一个平行状态是否存在着影响,没有影响那么不需要进行回溯了,有影响才要进行回溯