经典递归问题与动态规划

参考链接:https://blog.csdn.net/summer070825/article/details/76572434
http://www.cnblogs.com/DarrenChan/p/8734203.html

一些经典递归问题

斐波那契数列问题

经典生兔子问题,不再赘述。
从该算法改进过程看递归问题的优化求解方式。
version 1:


long Fibonacci(int n){
	if(n == 0)
		return 0;
	else if(n == 1)
		return 1;
	else if(n > 1)
		return Fibonacci(n - 1) + Fibonacci(n - 2);
	else return -1;
		
}

最基本的递归问题最基本的写法,但其效率之低令人发指,复杂度为O(2^n);重复了大量的递归调用,很多结果计算了多次。因此为了避免重复计算,可以将计算过的值记录下来。
Version 2:


long tempResult[max]={0};
long FIbonacci2(int n){
	if(n == 0)
		return 0;
	else if(n == 1)
		return 1;
	else if(n > 1){
		if(tempResult[n] != 0)
			return tempResult[n];
		else{
			tempResult[n] = Fibonacci2(n - 1) + Fibonacci2(n - 2);
			return tempResult[n];
		}
	}
}

递归就会大量使用栈来存储调用信息和变量,要真正提高效率就得放弃递归。我们可以将从大到小分解问题的递归变为从小问题通往大问题的循环。



public class Fibonacci3 {
	public static long Fibonacci3(int n) {
		if (n < 0) return -1;

		long[] temp = new long[n+1];
		temp[0] = 0;
		if(n>0) temp[1] = 1;
		for(int i= 2;i <= n; i++) {
			temp[i] = temp[i-2] + temp[i-1];
		}
		
		return temp[n];
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		System.out.println(Fibonacci3(0));
	}

}

a,b,c=b,c,a+c
一次循环的O(n);再用分治策略优化还可以得到O(logn)复杂度的方法。

汉诺塔问题

架设3根柱子分别为A、B、C,圆盘数目为n。

1:如果A有一个圆盘,则直接移动至c。

2:如果A有2个圆盘,则A->B,A->C,B->C。

好了这个时候已经可以解决问题了,结束条件为 n==1;


void hano1(char A, char B, char C,int n){
	if( n == 1){
		print(A+"->"+C);
	}
	else{
		hanoi(A, C, B, n-1);
		print(A+"->"+C);
		hanoi(B, A, C, n-1);
	}
}

从递归到动态规划

对于可用动态规划求解的问题,一般有两个特征:①最优子结构;②重叠子问题

找零钱问题

有数组arr,arr中所有的值都为正数且不重复。每个值代表一种面值的货币,每种面值的货币可以使用任意张,再给定一个整数aim(小于等于1000)代表要找的钱数,求换钱有多少种方法。

给定数组arr及它的大小(小于等于50),同时给定一个整数aim,请返回有多少种方法可以凑成aim。

测试样例:
[1,2,4],3
返回:2
所有的动态规划题本质都是优化后的暴力求解,一般动态规划题是构造一个dp矩阵,第一行和第一列赋初值,然后根据递推关系,由一个个子问题求出整个问题,即把剩余位置的值填满,说白了就是空间换时间。因为暴力求解会有大量的重复计算,动态规划可以有效地避免重复计算。

比如找零钱问题,我们可以看成0个arr[0],让剩余的组成aim,1个arr[0],让剩余的组成aim - 1 * arr[0],2个arr[0],让剩余的组成aim - 2 * arr[0],以此类推。为什么会产生重复计算,是因为比方我用了1个10元,0个5元,然后让剩下的组成aim - 10和我用0个10元,2个5元,让剩下的组成aim - 10本质是一样的。
递归调用:



public class process1 {
	public static int process1(int[] arr, int index, int aim) {
		int res = 0;
		if(index == arr.length) 
			res = aim == 0 ? 1 : 0;
			else {
				for(int i = 0; i * arr[index] <= aim; i++) {
					res += process1(arr, index + 1, aim - i * arr[index]);
				}
			}
	return res;
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		int[] arr = {1,2,5};
		
		System.out.println(process1(arr, 0, 1000000));
	}

}

动态规划法:

首先思考如何设计dp矩阵,这里我们把行设置成arr下标,代表的就是利用[0…i]区间内组成aim的值的方法数,列代表的是aim值,从0取到aim。
我们先给第一列赋值,因为aim是0,所以只有一种组合方式,就是每个价值的纸币都取0个,所以第一列全取1。
接下来看第一行,就是求arr[0]能够凑成的钱的方案,只要是其倍数的都能凑成,所以相应位置应该填写1。
最后我们确定其他位置,完全不用arr[i]货币,只用剩下的,则方法数dp[i - 1][j].
用arr[i],方法数是dp[i - 1][j - arr[i]]。
以此类推,是上面那一行,经过化简,可以简化成dp[i][j] = dp[i - 1][j] + dp[i][j - arr[i]]。这就是状态转移方程。


public class process2 {
	public static int process2(int[] arr, int aim) {
		//dp[i][j]表示数i用arr[j-1]之前的数划分的方法数(含arr[j-1])
		//如,若arr为[1,2,5,10],dp[10][2]表示 10用1,2划分的方法数
		int[][] dp = new int[aim+1][arr.length];
		for(int i = 0; i < dp[0].length; i++) {
			dp[0][i] = 1;			
		}
		for(int j = 1; j * arr[0] <= aim; j++) {
			dp[j * arr[0]][0] = 1;
		}
		for(int j = 1; j < dp[0].length; j++) {
			for(int i = 1; i < dp.length; i++) {
				dp[i][j] = dp[i][j-1];
				dp[i][j] += i - arr[j] >= 0 ? dp[i - arr[j]][j] : 0;
			}
		}
		return dp[aim][arr.length-1];
		
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		int[] arr = {1,2,5};
		
		System.out.println(process2(arr,1000000));
	}

}

整数划分问题

描述
将正整数n 表示成一系列正整数之和,n=n1+n2+…+nk, 其中n1>=n2>=…>=nk>=1 ,k>=1 。
正整数n 的这种表示称为正整数n 的划分。

输入
标准的输入包含若干组测试数据。每组测试数据是一行输入数据,包括两个整数N 和 K。
(0 < N <= 50, 0 < K <= N)

输出
对于每组测试数据,输出以下三行数据:
第一行: N划分成K个正整数之和的划分数目
第二行: N划分成若干个不同正整数之和的划分数目
第三行: N划分成若干个奇正整数之和的划分数目

分析
整数划分问题这几个变形确实很经典,需要一个个说明下:

N划分成若干个可相同正整数之和
划分分两种情况:

划分中每个数都小于m:则划分数为dp[n][m-1]。
划分中至少有一个数等于m:则从n中减去去m,然后从n-m中再划分,则划分数为dp[n-m][m]。
动态转移方程:dp[n][m]=dp[n][m-1]+dp[n-m][m]。

package test;

public class Divide {
	public static int divide_int(int num) {
		if (num <= 0) return 0;
		int[][] dp = new int[num+1][num+1];
		for(int i = 0; i < dp.length; i++) {
			dp[i][1] = 1;
		}
		for(int i = 1; i < dp.length; i++ )
			for(int j = 1; j < dp[0].length; j++)
			{
				if(i < j)
					dp[i][j] = dp[i][i];
				else if( i > j)
					dp[i][j] = dp[i-j][j] + dp[i][j-1]; 
				else 
					dp[i][j] = dp[i][j-1] + 1;
					
			}
		return dp[num][num];
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		System.out.println(divide_int(4));
	}

}

N划分成若干个不同正整数之和
划分分两种情况:

划分中每个数都小于m:则划分数为dp[n][m-1]。
划分中至少有一个数等于m:则从n中减去m,然后从n-m中再划分,且再划分的数中每个数要小于m, 则划分数为dp[n-m][m-1]。
动态转移方程:dp[n][m]=dp[n][m-1]+dp[n-m][m-1]。
在上面那个程序基础上修改动态转移方程即可。

N划分成K个正整数之和
设dp[n][k]表示数n划分成k个正整数之和时的划分数。
划分分两种情况:

划分中不包含1:则要求每个数都大于1,可以先拿出k个1分到每一份,之后在n-k中再划分k份,即dp[n-k][k]。
划分中包含1:则从n中减去1,然后从n-1中再划分k-1份, 则划分数为dp[n-1][k-1]。
动态转移方程:dp[n][k]=dp[n-k][k]+dp[n-1][k-1]。

package test;

public class Divide2 {
	public static int divide_int2(int num, int k) {
		if (num <= 0) return -1;
		if (k < 1) return -1;
		if (num < k) return 0;
		
		int[][] dp = new int[num+1][k+1];
		for(int i = 0; i < dp.length; i++) {
			dp[i][1] = 1;
		}
		for(int i = 1; i < dp.length; i++ )
			for(int j = 1; j < dp[0].length; j++)
			{
				if(i < j)
					dp[i][j] = 0;
				else if( i > j)
					dp[i][j] = dp[i-1][j-1] + dp[i-j][j]; 
				else 
					dp[i][j] = 1;
					
			}
		return dp[num][k];
	}
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		System.out.println(divide_int2(0,2));
	}

}

N划分成若干个奇正整数之和
设f[i][j]表示将数i分成j个正奇数,g[i][j]表示将数i分成j个正偶数。
首先如果先给j个划分每个分个1,因为奇数加1即为偶数,所以可得:
f[i-j][j] = g[i][j]。
划分分两种情况:

划分中不包含1:则要求每个数都大于1,可以先拿出k个1分到每一份,刚可将问题转换为”从i-j中划分j个偶数”,即g[i-j][j]。
划分中包含1:则从n中减去1,然后从n-1中再划分k-1份, 则划分数为dp[n-1][k-1]。
动态转移方程:f[i][j]=f[i-1][j-1]+g[i-j][j]。

package test;

public class Divide3 {
	public static int divide_int3(int num) {
		if (num <= 0) return -1;
		int sum = 0;
		
		int[][] f = new int[num+1][num+1];
		int[][] g = new int[num+1][num+1];
		//初始化
		for(int i = 1; i <= num; i++) {
			
		} 

		//动态规划
		for(int i = 1; i <= num; i++ ) {
			for(int j = 1; j <= i; j++)
			{	
				if(j == 1) {
					if(i%2 == 1)
						f[i][1] = 1;
					else 
						g[i][1] = 1;
				} 
				else {
					g[i][j] = f[i-j][j];
					f[i][j] = f[i-1][j-1] + g[i-j][j];
				}
				
			}
		}
		for (int i = 1; i <= num; i++) {
	        sum += f[num][i];
		}
	    return sum;
	
    }
	public static void main(String[] args) {
		// TODO 自动生成的方法存根
		System.out.println(divide_int3(5));
	}

}

最长递增子序列

这是一个经典的LIS(即最长上升子序列)问题,请设计一个尽量优的解法求出序列的最长上升子序列的长度。

给定一个序列A及它的长度n(长度小于等于500),请返回LIS的长度。

public static int[] getLIS(int[] A) {
        // write code here
        List<Integer> list = new ArrayList<>();
        
        int[] dp = new int[A.length];
        dp[0] = 1;
        
        for (int i = 1; i < dp.length; i++) {
            dp[i] = 1;
            for(int j = 0; j < i; j++){
                if(A[j] < A[i]){
                    dp[i] = Math.max(dp[i], dp[j] + 1);
                }
            }
        }
        
        int maxIndex = dp.length - 1;
        for (int i = dp.length - 2; i >= 0; i--) {
            if(dp[i] > dp[maxIndex]){
                maxIndex = i;    
            }
        }
        
        list.add(A[maxIndex]);
        for (int i = maxIndex - 1; i >= 0; i--) {
            if(A[maxIndex] > A[i] && dp[maxIndex] == dp[i] + 1){
                list.add(A[i]);
                maxIndex = i;
            }
        }
        
        int[] nums = new int[list.size()];
        for(int i = 0; i < nums.length; i++){
            nums[nums.length - 1 - i] = list.get(i);
        }
        return nums;
    }

其他一些经典问题参考http://www.cnblogs.com/DarrenChan/p/8734203.html

晚上对该帖子的算法进行实现并改进自己的这篇博客,自律给我自由。
2018/09/18 已实现整数划分前的所有算法

猜你喜欢

转载自blog.csdn.net/u013453787/article/details/82746118