什么是动态规划:
动态规划(英语:Dynamic programming,简称 DP),是一种在数学、管理科学、计算机科学、经济学和生物信息学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题。 ------维基百科
以上定义来自维基百科,看定义感觉还是有点抽象。简单来说,动态规划其实就是,给定一个问题,我们把它拆成一个个子问题,直到子问题可以直接解决。然后呢,把子问题答案保存起来,以减少重复计算。再根据子问题答案反推,得出原问题解的一种方法。
一般这些子问题很相似,可以通过函数关系式递推出来。然后呢,动态规划就致力于解决每个子问题一次,减少重复计算,比如斐波那契数列就可以看做入门级的经典动态规划问题。
动态规划的三个步骤就是:
1.定义状态,
2.列出递推公式,
3.找出边界条件。
下面,我们来看两道题。
问题一:
给定一个字符串s和一个字符串t,计算在s的子序列中t出现的个数。
分析:
字符串的一个子序列是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置 所组成的新字符串。 (例如,"BCD"是"ABCDE"的一个子序列,而"BDC"不是) 这题说的是s的子序列中出现t的个数,翻译一下就是字符串s的所有子序列中,和字符串t完全一样的有多少个。我们定义dp[i][j]表示t的前i个字符可以由s的前j个字符组成的个数 (也可以说是字符串s的前j个字符组成的子序列中,和字符串t的前i个字符组成的字符串一样的有多少个)。 那么最终我们只需要求出dp[tLength][ sLength]即可(其中tLength和sLength分 别表示字符串t和s的长度) 如果字符串t的第i个字符和字符串s的第j个字符不一样,也就是说字符串s的第j个字符 不能匹配字符串t的第i个字符。那么我们只能计算字符串s的前j-1个字符构成的子序列中包含字符串t的前i个字符组成的字符串的个数。 动态规划的三个步骤就是: 1.定义状态, 2.列出递推公式, 3.找出边界条件。 前面两步我们都完成了,我们来看最后一个。 因为空字符串" "是所有字符串的子集,所以当字符串t为空的时候,dp[0][j]=1;
来看一下代码:
import java.util.Scanner;
public class Demo01 {
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
System.out.println("请输入一个字符串:");
String s=sc.next();
System.out.println("请再输入一个字符串:");
String t=sc.next();
int index=numDistinct(s,t);
System.out.println(index);
}
public static int numDistinct(String s, String t) {
//sLength和tLength分别是两个字符串的长度
int sLength = s.length();
int tLength = t.length();
int[][] dp = new int[tLength + 1][sLength + 1];
//base case 边界条件
for (int j = 0; j <= sLength; j++) {
dp[0][j] = 1;
}
for (int i = 1; i <= tLength; i++) {
for (int j = 1; j <= sLength; j++) {
//下面是递推公式
if (t.charAt(i - 1) == s.charAt(j - 1)) {
//如果字符串t的第i个字符和s的第j个字符一样,
// 那么有两种选择
dp[i][j] = dp[i - 1][j - 1] + dp[i][j - 1];
} else {
//如果字符串t的第i个字符和s的第j个字符不一样,我们只能用字符串s的前j-1个字符来计算他包含的数量
dp[i][j] = dp[i][j - 1];
}
}
}
return dp[tLength][sLength];
}
}
运行结果如图:
问题二:
给你一个只包含正整数的非空数组nums。请你判断是否可以将这个数组分割成两 个子集,使得两个子集的元素和相等。 分析: 这题判断把数组分成两份,这两份的元素和是否相等。 首先我们需要计算数组中所 有元素的和sum,然后判断sum是否是偶数: 如果不是偶数,说明不可能分割成完全相等的两份,直接返回false。 如果是偶数,我们只需要判断是否存在一些元素的和等于sum/2,如果等于sum/2,那么剩下的肯定也等于sum/2,说明我们可以把数组分为元素和相等的两部分。
看一下代码 :
import java.util.Arrays;
public class Demo02 {
public static void main(String[] args) {
// int[] nums={1,2,3,4,5,5};
int[] nums={1,2,3,4,5,6,7,8,9};
boolean b=canPartition(nums);
System.out.println("原数组是:"+Arrays.toString(nums));
if(b){
System.out.println("原数组可以分割成两个元素和相同的子集");
}else {
System.out.println("原数组不可以分割成两个元素和相同的子集");
}
}
public static boolean canPartition(int[] nums) {
//计算数组中所有数字的和
int sum = 0;
for (int n : nums)
sum += n;
//如果sum是奇数,直接返回false
if ((sum & 1) == 1)
return false;
int len = sum >> 1;
//这里bits的长度是len+1,因为我们只需要计算
//低位就行了,没必要计算所有的
byte[] bits = new byte[len + 1];
bits[0] = 1;
for (int i = 0; i < nums.length; i++) {
int num = nums[i];
int size = len - num;
for (int j = size; j >= 0; j--) {
bits[j + num] |= bits[j];
}
//判断中位数如果是1,说明可以分成两种相等的
//子集,直接返回true,不需要再计算了
if ((bits[len] & 1) != 0)
return true; }
return false; }
}
在主方法里我定义了两个数组,是为了测试能否成功判断,所以,以下分别为是/不是的两个运行结果。
{1,2,3,4,5,5}可以分成两个元素和相同的子集
{1,2,3,4,5,6,7,8,9}则不能
运行结果如下:
数组 {1,2,3,4,5,5}运行结果:
{1,2,3,4,5,6,7,8,9}运行结果: