递归求解复杂的枚举问题

一、问题概述

    枚举就是列举出所有的情况。数据量较小时,比较容易列举,但是随着数据量增大,如果依然一一列举,可能会出现遗漏的情况,导致出错,这时候就可以考虑使用递归的方式进行,寻找递推关系。比如先获得数据规模是n-1的所有情况,那么求n就只需要在n-1的基础上做一些修改即可,这样就大大简化了问题,但是如果递归深度较大,可能会产生内存溢出的问题。

    对于枚举问题,递归是一个较容易想到的解题思路,适用于枚举数据量大、易出错、有递归关系的应用场景,先求数据规模小的所有情形,在此基础上经过一系列简单的操作,获得数据规模大的所有情形。

二、常见问题总结

1、有效的括号序列

需求:

    求n个括号组成的所有有效序列。比如n=2,有效序列有:(()),()()。

思路:

    这个一个枚举的题目,求n个括号的有效序列,如果直接列举,很容易出错,这种题目可以试图寻找递归关系,考虑递归的方式求解。

    不难发现,n个括号组成的有效序列,可以看成是在n-1个括号组成有效序列的基础上再添加一个括号,我们只需要获得n-1的所有序列,然后在每个序列的任意位置插入左括号,然后在左括号的右边任意位置插入右括号即可得到有效的序列。假设序列s是n-1个括号的有效序列,那么从s的0到s.length()-1,都可以插入左括号,比如插入的位置是i,那么从i+1到s.length()插入右括号即可得到一个有效序列。注意,添加完括号之后要记得删除,以便插入到新的位置。

代码:

import java.util.*;

class ValidParenthesesSequence{
	//求n个括号组成的所有有效序列,n>=0
	public List<String> getSeq(int n){
		List<String> res = new LinkedList<String>();

		//如果n==0,那么就没有这样的序列,直接返回res即可
		if(n == 0)
			return res;
		
		//如果n==1,那么只有一种情况,添加之后返回即可
		if(n == 1){
			res.add("()");
			return res;
		}

		//获取n-1个括号的有效序列
		List<String> pre = getSeq(n-1);
		
		//遍历每个字符串,进行括号的插入
		for(String str : pre){
			//方便处理字符串
			StringBuilder sb = new StringBuilder(str);

			for(int i = 0; i < sb.length(); i++){
				sb.insert(i, '(');//插入左括号

				for(int j = i+1; j < sb.length()+1; j++){
					sb.insert(j, ')');//插入右括号

					//sb就是有效的序列,如果res不包含这个序列,就添加
					if(!res.contains(sb.toString()))
						res.add(sb.toString());

					sb.deleteCharAt(j);//将插入的右括号删除,以便在新的位置上插入右括号
				}

				sb.deleteCharAt(i);//将插入的左括号删除,以便在新的位置上插入左括号
			}
		}

		return res;
	}

	public static void main(String[] args){
		Scanner scan = new Scanner(System.in);

		while(scan.hasNext()){
			int n = scan.nextInt();

			List<String> list = new ValidParenthesesSequence().getSeq(n);

			System.out.println(n+"个括号的有效序列 = "+list);
		}
	}
}

运行结果:

2、求和是m的所有组合情况

需求:

    用k个0-9的和表示m,返回所有的组合情况。比如k=2,m=4,那么所有的组合情况有:[0, 4] [1, 3] [2, 3]

思路:

    1、边界条件处理

        首先要考虑k个0-9的和是否能够表示m,k个0-9能够表示的数据范围是[0, 9*k],如果m超过这个范围,就无法表示。

    2、特殊值处理

        如果m==0或者m==9*k,那么只有一种表示情况,可以直接对这两个值进行处理。m等于0的时候,就是k个0,m等于9*k的时候,就是k个9。

    3、正常值处理

        如果0<m<9*k,直接求解肯定很复杂,并且没有一定的规律,对于这种枚举情况很多的需求,一般都可以使用递归求解,先求m-1的所有表示情况,然后对每个组合,选择其中一个小于9的数加一就可以得到一个新的组合,和是m。类似于求n个括号对的全部组合,可以先求n-1个括号对的全部组合,对每个组合,添加一个括号即可。

注意:

    在集合嵌套集合的时候,要格外注意,不可以使用add方法把用来遍历的变量添加到外层集合中。看以下实例:

List<List<Integer>> list1 = new LinkedList<List<Integer>>();
List<List<Integer>> list2 = new LinkedList<List<Integer>>();
		
for(List<Integer> list : list1){
	if(list ...){
	    //list2.add(list);这是错误的,因为list会变,如果直接添加list,那么list2中的值就是变化的,应该复制一份list添加
            
            //创建新的list,添加到list2中
            List<Integer> l = new LinkedList<Integer>();
	    for(Integer num : list)
		l.add(num);
	    
            list2.add(l);
        }
}

    在这个示例中,先遍历list1,把满足条件的元素放到list2中,其中list变量是用来遍历集合list1的,我们不能直接将其添加到list2中,即不能使用list2.add(list),因为会引起list2的变化。比如,某次遍历中,list满足条件,我们直接调用list2.add(list),那么下次循环中,list发生了变化,会导致list2中的元素发生变化,这并不是我们想要的结果,所以应该复制一份list,然后再添加。

代码:

import java.util.*;

class ComposeM{
	//求k个0-9的全部组合,使其和为m
	public List<List<Integer>> getCompose(int m, int k){
		List<List<Integer>> res = new LinkedList<List<Integer>>();

		//边界处理
		if(m < 0 || m > k*9)
			return res;

		//特殊值处理
		if(m == 0){
			List<Integer> list = new LinkedList<Integer>();

			//添加k个0
			for(int i = 0; i < k; i++)
				list.add(0);

			res.add(list);
		}
		else if(m == k*9){
			List<Integer> list = new LinkedList<Integer>();

			//添加k个9
			for(int i = 0; i < k; i++)
				list.add(9);

			res.add(list);
		}
		else{
			//如果0<m<9*k,可以先求m-1的所有表示情况,然后在每个组合中,将小于9的数字加一,就可以得到一个新的组合,和是m
			List<List<Integer>> prelist = getCompose(m-1, k);

			//遍历和为m-1的所有组合
			for(List<Integer> list : prelist){
				for(int i = 0; i < k; i++){
					int num = list.get(i);

					//如果num<9,那么加一,得到一个新的组合
					if(num < 9){
						list.set(i, num+1);
					}
					else{
						continue;//如果num==9,那么遍历下一个值
					}

					//将修改之后的list存到新的list集合中
					List<Integer> l = new LinkedList<Integer>();
					for(int j = 0; j < k; j++)
						l.add(list.get(j));

					Collections.sort(l);//对l进行排序,防止重复

					//如果res不包含l,就添加到其中
					//不能直接添加list,因为之后还会对list进行处理,会修改res的结果。
					//对于list嵌套list的情况,最好不要直接add遍历使用的变量list,而应该创建一个新的list,更新其值,然后add
					if(!res.contains(l))
						res.add(l);

					list.set(i, num);//将list的值复原
				}
			}
		}

		return res;
	}

	public static void main(String[] args){
		Scanner scan = new Scanner(System.in);//创建扫描器对象,从键盘读取输入

		while(scan.hasNext()){
			int m = scan.nextInt();
			int k = scan.nextInt();

			System.out.println("用"+k+"个0-9组合成"+m+"的组合为:"+new ComposeM().getCompose(m, k));
		}
	}
}

运行结果:


  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值