(一)递归与分治
分治的全称为“分而治之”,也就是说,分治法将原问题划分成若干个规模较小而结构与原问题相似或者相同的子问题,然后分别解决这些子问题,最后合并子问题的解,即可得到原问题的解。总结一下分治法的三个步骤:
①分解:将原问题分解为若干个相似或者相同的子问题。
②解决:递归解决所有的子问题。
③合并所有子问题的解得到原问题的解。
举个例子
/*递归求斐波那契*/
public static int Fibonacci(int n){
if(n==0)return 1;
if(n==1)return 1;
return Fibonacci(n-1)+Fibonacci(n+1);//计算Fibonacci(n)时,要计算Fibonacci(n-1)和Fibonacci(n-2),两个相加就是合并。
}
递归,一类反复调用自身的函数,但是每次都可以把问题规模缩小,因此递归可以很好的实现分治的思想。
递归的两个重要概念:(以递归版斐波那契为例子)
①递归边界
if(n==0)return 1;
if(n==1)return 1;
②递归式
return Fibonacci(n-1)+Fibonacci(n+1);
/*这种递归式放在尾部的我们称之为尾递归。*/
下面来看几道题目,了解一下基础的递归案例
P1044 [NOIP2003 普及组] 栈
题目内容
题目描述
宁宁考虑的是这样一个问题:一个操作数序列,1,2,…,n,栈 A 的深度大于 n。
现在可以进行两种操作,
将一个数,从操作数序列的头端移到栈的头端(对应数据结构栈的 push 操作)
将一个数,从栈的头端移到输出序列的尾端(对应数据结构栈的 pop 操作)
你的程序将对给定的 n,计算并输出由操作数序列 1,2,…,n 经过操作可能得到的输出序列的总数。输入格式
输入文件只含一个整数 n(1≤n≤18)。
输出格式
输出文件只有一行,即可能输出序列的总数目。
输入输出样例
输入 #1
3
输出 #1
5
题目分析:
我们如果要使用递归来做,就要先找到递归关系式和递归边界。
由于栈只有两种操作,pop()压栈和push()弹栈,如果栈为空,只能压栈push(),如果待入栈的序列为空,只有弹出pop()的操作,且只有一种输出序列。
所以有三种情况,递归函数要有两个参数,栈中元素i,待入栈元素n.
第一种情况:i=0,这时候只能入栈,i+1,n-1
第二种情况:n=0,这时候只能出栈,而且要不断把栈里面的元素弹出,只能有一种序列,直接返回1。
第三种情况:n!=0,i!=0这时候,可以出栈,也可以继续进栈,返回两个子问题进栈(i+1,n-1),出栈(i-1,n)
所以我们就找到了递归关系式,和递归边界。
递归边界:
if(n==0)return 1;
递归关系式:
if(i==0&&n!=0)return fun(i+1,n-1);
else{//其他情况
return fun(i+1,n-1)+fun(i-1,n);
}
参考代码:
import java.util.Scanner;
public class Main {
static long fun(int i,int n) {//递归写法
if (n==0) {
return 1;
}
if (i==0) {
return fun(i+1, n-1);
}
return fun(i+1, n-1)+fun(i-1, n);
}
public static void main(String[] args) {
Scanner scanner =new Scanner(System.in);
int n=scanner.nextInt();
System.out.println(fun(0,n));
}
}
但是!!!
测试情况却是:(超时)
这种情况出现的原因是出现了重复计算,我们通过分析程序的调用栈来说明这种递归方式如何产生出这种情况的。
上述程序调用栈如下图所示(以n==5为例):
可以见到,fun(1,3),fun(0,3)被重复算了一次,fun(1,2)被重复算了3次,如果n给的特别大,我们将重复计算很多内容,所以我们试想通过建立一个数组,将计算过的数据保存到一个数组中,当再次遇到这个数据时,不必再次计算,直接使用即可。我们称之为记忆化搜索 (空间换取时间!),这也时dp的主要思想。
改进后的代码:
import java.util.Scanner;
public class P1044 {
static long [][] data=new long[51][51];//[i][n],保存结果的数组
static long fun(int i,int n) {//递归写法
if (n==0) {
return 1;
}
if (i==0) {
if (data[i+1][n-1]==0) {//判断条件为,如果没有计算,就进行计算
data[i+1][n-1]=fun(i+1, n-1);
}
return data[i+1][n-1];
}
if (data[i+1][n-1]==0) {
data[i+1][n-1]=fun(i+1, n-1);
}
if (data[i-1][n]==0) {
data[i-1][n]=fun(i-1, n);
}
return data[i+1][n-1]+data[i-1][n];
}
public static void main(String[] args) {
for (int i = 0; i < 51; i++) {
data[i][0]=1;
}
Scanner scanner =new Scanner(System.in);
int n=scanner.nextInt();
System.out.println(fun(0,n));
}
}
这样我们就AC过了!(欧耶)
P1255 数楼梯
题目内容
题目描述
楼梯有 NN 阶,上楼可以一步上一阶,也可以一步上二阶。
编一个程序,计算共有多少种不同的走法。输入格式
一个数字,楼梯数。
输出格式
输出走的方式总数。
输入输出样例
输入
4
输出
5
说明/提示
对于 60% 的数据,N≤50;
对于 100% 的数据,N≤5000。
题目分析:
还是一样,记忆化搜索递归才能做出来,不然n=50就已经开始卡了。
找递归边界和递归关系式:
因为走楼梯可以走两步或者一步,用n表示剩余的楼梯,当前问题等于走一步fun(n-1)加上fun(n-2)这两个子问题的解。当剩余一步时,只能有一种情况,当剩余两步时,可以走1步后再走一步到,也可以直接走两步,有两种情况。
递归边界:
if(n==1)return 1;
if(n==2)return 2;
递归关系式
return fun(n-1)+fun(n-2);
最后别忘了记忆化搜索!!!(空间换取时间)
AC代码:
//写这道题时,long都爆炸了,用的是BigInteger。
import java.math.BigInteger;
import java.util.Scanner;
public class P1255{
static BigInteger [] data=new BigInteger[5010];
static BigInteger fun(int n) {
if (n==0) {
return BigInteger.valueOf(0);
}
if (n==1) {
return BigInteger.valueOf(1);
}
if (n==2) {
return BigInteger.valueOf(2);
}
if (data[n-1]==null) {//对象数组未被使用是null状态
data[n-1]=fun(n-1);
}
if (data[n-2]==null) {//对象数组未被使用是null状态
data[n-2]=fun(n-2);
}
return data[n-1].add(data[n-2]);
}
public static void main(String[] args) {
Scanner in=new Scanner(System.in);
int n=in.nextInt();
System.out.println(fun(n));
}
}
P1464 Function
题目内容
题目描述
对于一个递归函数w(a,b,c)
如果a≤0 or b≤0 orc≤0就返回值1.
如果a>20 or b>20 or >20就返回w(20,20,20)
如果a<b并且b<c 就返回w(a,b,c-1)+w(a,b-1,c-1)-w(a,b-1,c)
其它的情况就返回w(a-1,b,c)+w(a-1,b-1,c)+w(a-1,b,c-1)-w(a-1,b-1,c-1)
这是个简单的递归函数,但实现起来可能会有些问题。当a,b,c均为15时,调用的次数将非常的多。你要想个办法才行.
absi2011 : 比如 w(30,-1,0)w(30,−1,0)既满足条件1又满足条件2
这种时候我们就按最上面的条件来算
所以答案为1输入格式
会有若干行。
并以-1,-1,-1结束。
保证输入的数在[-9223372036854775808,9223372036854775807] (符合long大小)之间,并且是整数。输出格式
输出若干行,每一行格式:
w(a, b, c) = ans
注意空格。输入 #1
1 1 1
2 2 2
-1 -1 -1输出 #1
w(1, 1, 1) = 2
w(2, 2, 2) = 4说明/提示
记忆化搜索
题目分析:
和前两个类似,只是边界条件变多,思路一致,这里只给出AC代码,一定要自己尝试。
AC代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class P1464 {
static long[][][] res = new long[21][21][21];
static long w(long a, long b, long c) {
if (a <= 0 || b <= 0 || c <= 0) {
return res[0][0][0];
}
if (a > 20 || b > 20 || c > 20) {
return w(20, 20, 20);
}
if (a < b && b < c) {
if (res[(int) a][(int) b][(int) c-1]==0) {
res[(int) a][(int) b][(int) c-1]= w(a, b, c - 1);
}
if (res[(int) a][(int) b-1][(int) c-1]==0) {
res[(int) a][(int) b-1][(int) c-1]=w(a, b - 1, c - 1);
}
if (res[(int) a][(int) b-1][(int) c]==0) {
res[(int) a][(int) b-1][(int) c]=w(a, b - 1, c);
}
return res[(int) a][(int) b][(int) c-1]+res[(int) a][(int) b-1][(int) c-1]-res[(int) a][(int) b-1][(int) c];
}
if (res[(int) a-1][(int) b][(int) c]==0) {
res[(int) a-1][(int) b][(int) c]=w(a - 1, b, c);
}
if (res[(int) a-1][(int) b-1][(int) c]==0) {
res[(int) a-1][(int) b-1][(int) c]=w(a - 1, b-1, c);
}
if (res[(int) a-1][(int) b][(int) c-1]==0) {
res[(int) a-1][(int) b][(int) c-1]=w(a - 1, b, c-1);
}
if (res[(int) a-1][(int) b-1][(int) c-1]==0) {
res[(int) a-1][(int) b-1][(int) c-1]=w(a - 1, b-1, c-1);
}
return res[(int) a-1][(int) b][(int) c]+res[(int) a-1][(int) b-1][(int) c]+res[(int) a-1][(int) b][(int) c-1]-res[(int) a-1][(int) b-1][(int) c-1];
}
public static void main(String[] args) throws IOException {
res[0][0][0] = 1;
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] input = in.readLine().split(" ");
long a = Long.parseLong(input[0]), b = Long.parseLong(input[1]), c = Long.parseLong(input[2]);
while (a != -1 || b != -1 || c != -1) {
System.out.printf("w(%d, %d, %d) = ",a,b,c);
System.out.println(w(a, b, c));
input = in.readLine().split(" ");
a = Long.parseLong(input[0]);
b = Long.parseLong(input[1]);
c = Long.parseLong(input[2]);
}
}
}
(二)DFS与暴力枚举
如图,是一个迷宫,如果我们要从出发点A走到终点G,在没有任何指引的情况下,我们可能盲目的走,我们可以去采取图中看似很盲目而实际上却很有用的方法走出去。以当前位置为起点,遇到分叉,选择一个分叉走,如果遇到死路就退回来,选择另外一个分叉路口,如果岔路中存在新的分叉,仍然按照上面的方法枚举每一条岔路。这样,只要迷宫存在出口,就一定能走出去。当然,要考虑到如何退回到原来分叉的问题,我们只需要沿着墙的右端一直走就行。
深度优先搜索是一种可以枚举完所有完整路径以遍历所有情况的搜索方式!
下面是一个例子,让大家明白DFS思想。
题目:有n件物品,每件重W[i],价值为c[i]。现在要选出若干件物品放入一个容量为V的背包中,使得在背包物品重量和不超过容量V的前提下,让背包中的物品价值最大,求最大价值。(1<=n<=20)
分析:
在这个问题中,需要从n件物品中选出若干件,使得价值最大,对于每一件物品,有选与不选两种选择,这就是所谓的 “岔道口”。那么 “死胡同” 就是当前背包的重量已经超过了容量V,就要返回最近的岔路口。
参考代码:
import java.util.Scanner;
public class 背包问题引入 {
static int n=0,V=0,max=0;//物品件数 背包容量 最大价值
static int []w=new int[0];//物品重量
static int []c=new int[0];//物品价值
static int thisW=0;//背包实时重量。
static int thisC=0;//背包实时价值。
static void DFS(int num,int index) {//num 表示已经放了的物品数目。index 指示岔道的开始位置。
if (num==n) {//已经全部装入了,死胡同了,已经走不下去了,必须返回岔路
if (thisW<=V&&thisC>max) {
max=thisC;
}
return;
}
if (thisW>V) {//剪枝,当前已经超容量了,再加肯定都不行了
return;
}
if (thisC>max) {//更新最大价值。
max=thisC;
}
for (int i =index; i <n; i++) {//尝试遍历每一个岔道。
/*进入一个岔道*/
thisW+=w[i];
thisC+=c[i];
DFS(num+1,i+1);
/*返回岔路口*/
thisW-=w[i];
thisC-=c[i];
}
}
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
n=scanner.nextInt();
V=scanner.nextInt();
w=new int[n];
c=new int[n];
for (int i = 0; i < n; i++) {
w[i]=scanner.nextInt();
}
for (int i = 0; i <n; i++) {
c[i]=scanner.nextInt();
}
DFS(0,0);
System.out.println(max);
}
}
输入数据:
5 8
3 5 1 2 2
4 5 2 1 3
输出结果:
10
仔细体会这个过程!(尤其是剪枝的情况)
P2089 烤鸡
题目内容:
题目描述
猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 10 种配料(芥末、孜然等),每种配料可以放 1 到 3 克,任意烤鸡的美味程度为所有配料质量之和。
现在, Hanke 想要知道,如果给你一个美味程度 n ,请输出这 10 种配料的所有搭配方案。输入格式
一个正整数 nn,表示美味程度。
输出格式
第一行,方案总数。
第二行至结束,1010 个数,表示每种配料所放的质量,按字典序排列。
如没有符合要求的方法,就只要在第一行输出一个 00。
输入输出样例输入
11
输出
10
1 1 1 1 1 1 1 1 1 2
1 1 1 1 1 1 1 1 2 1
1 1 1 1 1 1 1 2 1 1
1 1 1 1 1 1 2 1 1 1
1 1 1 1 1 2 1 1 1 1
1 1 1 1 2 1 1 1 1 1
1 1 1 2 1 1 1 1 1 1
1 1 2 1 1 1 1 1 1 1
1 2 1 1 1 1 1 1 1 1
2 1 1 1 1 1 1 1 1 1说明/提示
对于 100% 的数据,n≤5000。
题目分析:
这道题用DFS,剪枝条件是是否超过美味程度,还有10个调料放完是否达到了美味程度。且要全部搜索完才打印结果,必须要先储存结果。
AC代码:
import java.util.ArrayList;
import java.util.Scanner;
import java.util.Stack;
public class P2089 {
static int sum=0;//当前的美味程度。
static Stack<Integer> stack =new Stack<>();存储当前各种调料的剂量分别是多少
static ArrayList<String> al=new ArrayList<>();//存储符合的结果的列表。
static int n=0;//要求达到的美味程度。
static void dfs(int index) {
if (index==10) {//10种调料都放完了。
if (sum<n) {//还未达到美味程度就退出
return;
}
//符合将结果加入列表中。
StringBuffer sb=new StringBuffer();
for (int e : stack) {
sb.append(e+" ");
}
al.add(sb.toString());
}
for (int i = 1; i <=3; i++) {
//每个岔路口都有三种选择,1-3.
if (sum+i>n) {//如果加的剂量超了,没必要继续走了,直接剪枝退出。
return;
}
/*走入一个岔道*/
sum+=i;
stack.add(i);
dfs(index+1);
/*回到岔路口*/
stack.pop();
sum-=i;
}
}
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
n=scanner.nextInt();
dfs(0);
//先打印方案数目,再打印具体方案。
System.out.println(al.size());
for (String s:al) {
System.out.println(s);
}
}
}
P1618 三连击(升级版)
题目内容:
题目描述
将 1, 2…,9 共 9 个数分成三组,分别组成三个三位数,且使这三个三位数的比例是 A:B:C,试求出所有满足条件的三个三位数,若无解,输出 No!!!。
输入格式
三个数,A,B,C。
输出格式
若干行,每行 3 个数字。按照每行第一个数字升序排列。输入
1 2 3
输出
192 384 576
219 438 657
273 546 819
327 654 981说明/提示
保证 A<B<C。
题目分析:
1-9分成3个组,相当于1-9的数组求排列数,1-3组成A,4-6组成B,7-9组成C,成比例计算的话相当于通分的计算规则Ak/Bk=A/B 等价于AkB=BkA,还因为1:2:3等价于2:4:6,要算出他们的gcd(最小公倍数)来先约去,防止数据溢出。
剪枝分析:
当已经排了6个数字时,我们就可以比较A:B是否符合,没必要等9个数字都排满再验证。当然9个数字也要进行一次验证,因为可以排到9个数字,所以A:B肯定没问题,比较A:C或者B:C就行了。
AC代码:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Stack;
public class P618 {
static int cnt = 0;//方案数
static int A = 0;
static int B = 0;
static int C = 0;
static Stack<Integer> num = new Stack<>();//当前排进去的数字。
static ArrayList<Boolean> hash = new ArrayList<>();//设立对于的哈希表,哈希表表示,数字是否被选入进去了
static void dfs(int index) {
if (index == 6) {//判断A:B是否符合
int a = num.get(0) * 100 + num.get(1) * 10 + num.get(2);
int b = num.get(3) * 100 + num.get(4) * 10 + num.get(5);
if (!Judge(a, b, A, B)) {
return;
}
}
if (index == 9) {//判断A:B:C是否符合
int a = num.get(0) * 100 + num.get(1) * 10 + num.get(2);
int b = num.get(6) * 100 + num.get(7) * 10 + num.get(8);
if (!Judge(a, b, A, C)) {
return;
}
/*符合就输出*/
cnt++;
System.out.println(a+" "
+(num.get(3) * 100 + num.get(4) * 10 + num.get(5))+" "
+b);
}
for (int i = 0; i < 9; i++) {
if (hash.get(i)) {//判断数字是否已经排进去了,如果已经排入,就跳过
continue;
}
/*进入一条分岔*/
hash.set(i, true);
num.add(i + 1);
dfs(index + 1);
/*回到分岔口*/
num.pop();
hash.set(i, false);
}
}
public static boolean Judge(int a, int b, int c, int d) {//判断两个数是否符合比例
return a * d == b * c;
}
static int gcd(int a,int b) {//计算最小公倍数
return b==0?a:gcd(b, a%b);
}
public static void main(String[] args) throws IOException {
for (int i = 0; i < 10; i++) {
hash.add(false);
}
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String[] strings = in.readLine().split(" ");
A = Integer.parseInt(strings[0]);
B = Integer.parseInt(strings[1]);
C = Integer.parseInt(strings[2]);
/*计算出最小公约数,并分别将其因子约去*/
int k=gcd(gcd(A, B),C);
A/=k;
B/=k;
C/=k;
/*深度搜索*/
dfs(0);
if (cnt==0) {
System.out.println("No!!!");
}
}
}
P1706 全排列问题
题目描述:
题目描述
输出自然数 1 到 n 所有不重复的排列,即 n 的全排列,要求所产生的任一数字序列中不允许出现重复的数字。
输入格式
一个整数 nn。
输出格式
由1∼n 组成的所有不重复的数字序列,每行一个序列。
每个数字保留 5 个场宽。
输入
3
输出
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
说明/提示
1≤n≤9
总结在最后,可以当模板题目来看。
AC代码:
import java.util.ArrayList;
import java.util.Scanner;
import java.util.Stack;
public class P1706 {
static int n=0;
static ArrayList<Integer> num=new ArrayList<>();//存储1-n个数字。
static ArrayList<Boolean> hash=new ArrayList<>();//存储当前数字是否被选了。
static Stack<Integer> res=new Stack<>();//当前序列
static void DFS(int index) {
if (index==n) {//这道题没有多余的剪枝情况
for (int i = 0; i < n; i++) {
System.out.printf("%5d ",res.get(i));
}
System.out.println();
}
for (int i = 0; i < n; i++) {
if (hash.get(i)==true) {
continue;
}
hash.set(i, true);
res.add(num.get(i));
DFS(index+1);
res.pop();
hash.set(i, false);
}
}
public static void main(String[] args) {
Scanner scanner=new Scanner(System.in);
n=scanner.nextInt();
for (int i = 1; i <=n; i++) {
num.add(i);
hash.add(false);
}
DFS(0);
}
}
P1157 组合的输出(这也是一个模板题目,没有分析)
题目描述:
import java.io.BufferedReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedList;
public class P1157 {
/*对应的变量和上一题基本类似*/
static ArrayList<Integer> al =new ArrayList<>();
static int r=0;
static ArrayList<Boolean> hash =new ArrayList<>();
static LinkedList<Integer> res =new LinkedList<>();
static void dfs(int index,int start) {
if (index==r) {
for (int i = 0; i < res.size(); i++) {
System.out.printf("%3d",res.get(i));
}
System.out.println();
}
for (int i=start; i < al.size(); i++) {
if (hash.get(i)==true) {
continue;
}
hash.set(i, true);
res.add(al.get(i));
dfs(index+1,i+1);
res.remove(res.size()-1);
hash.set(i, false);
}
}
public static void main(String[] args) throws IOException {
int n=0;
BufferedReader in =new BufferedReader(new java.io.InputStreamReader(System.in));
String[] input = in.readLine().split(" ");
n=Integer.parseInt(input[0]);
r=Integer.parseInt(input[1]);
for (int i = 0; i < n; i++) {
al.add(i+1);
hash.add(false);
}
dfs(0,0);
}
}
两个模板题目的总结:
不知道你注意了吗,一个是求全排列,有位置变换,一个是组合,不要求位置变换,所以两个题目的dfs有一点小小的区别。
①如果说是求全排列:
设立hash表,每个数字都可能是岔路,用hash表把死的路剪掉。
②如果是求组合
多设置一个参数用于传递岔路的开始位置,子递归的岔路要从自己后面一个开始。