一、问题概述
枚举就是列举出所有的情况。数据量较小时,比较容易列举,但是随着数据量增大,如果依然一一列举,可能会出现遗漏的情况,导致出错,这时候就可以考虑使用递归的方式进行,寻找递推关系。比如先获得数据规模是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));
}
}
}
运行结果: