什么是动态规划?
1. 书上的解释:
任何数学递推公式都可以直接转换成递归算法,但是基本实现使编译器常常不能正确对待递归算法,结果导致低效的程序。当怀疑很可能是这种情况时,我们必须再给编译器提供一些帮助,降递归算法重新写成非递归算法,让后者(非递归算法)把那些子问题的答案系统地记录在一个表内。利用这种方法的一种技巧叫做动态规划
2. 为什么要用到动态规划?
在这里不得不卖弄一下自己,这学期学校开设了 “马原” 的课程,而我也只记住了 黑格尔 的一句:“存在即合理”,想想在这用也挺合适的
大家可能都听过斐波那契数列数列吧,可能也有部分人求过斐波那契数列的某一项的值,最简单的方法可能就是用递归实现 求解斐波那契数列的某一项 ,但是这种方法 它不仅违反了递归四条基本法则的第四条:合成效益法则,同时它的时间复杂度也极大,用递归实现斐波那契数的冗余计算的增长也是爆炸性的,如下图:
此时 动态规划 就站出来了,它会将已经计算好的值存在一个表里,下次使用就可以直接拿出来而不是再计算一次,当然这种只是解决了时间复杂度的问题,没有解决空间复杂度的问题
再优化的话,就不应该用表来存取值,而是用临时变量来存取已经求出来的值,这样不仅优化了时间复杂度,也优化了空间复杂度
最终优化后的求斐波那契数的代码如下:
public static int fibonacci(int n){
//若 n 为 1 时,直接返回边界值
if(n <= 1)
return 1;
//定义 3 个临时变量存储当前斐波那契数和前两项斐波那契数
int last = 1;
int nextToLast = 1;
int answer = 1;
//当 n>=2 时才会进入该循环
for( int i = 2; i <= n; i++){
//求出当前斐波那契数
answer = last + nextToLast;
//重新给临时变量赋值,准备下一次循环
nextToLast = last;
last = answer;
}
return answer;
}
当然这种 动态规划 只是一维的,比较简单
漫画形象解释动态规划
别猜了,这当然不是我这种小菜鸡整理的
- 这里附上大佬漫画解释的链接:感兴趣,你就点,准没错!
动态规划的实例操作
前几天在 LeetCode官网 刷题就遇到了一个动态规划的题(求最长回文子串),幸幸苦苦撸几小时代码,缺栽在了最后一个时间复杂度的测试上,那时的心情…我T(O)M(CAT)。
我的解题思路:暴力枚举法,for循环就完事了,以下是我的代码:
import java.util.Scanner;
public class Solution {
static boolean flag=true;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (flag){
boolean flag2=true;
System.out.println("请输入一段字符串,我将会返给您一段最长回文子串...");
String s = sc.nextLine();
System.out.println(longestPalindrome(s));
System.out.println("是否继续测试?请输入 y or n");
while (flag2){
String s1 = sc.nextLine();
if(s1.equals("n")||s1.equals("N")){
flag=false;
flag2=false;
}else if(s1.equals("y")||s1.equals("Y")){
flag2=false;
}else {
System.out.println("输入格式不对,请重新输入 y or n");
}
}
}
}
//求最长回文子串的方法
public static String longestPalindrome(String s) {
//解决空串问题
String result="";
//长度由大及小,可以在第一次就找到最长回文子串
for (int i = s.length(); i > 0; i--) {
//若该字串没有回文子串,则返回第一个字母
if(i==1){
result=s.substring(0,1);
break;
}
//循环改变最长回文字串的起始位置,循环次数=字符串长度-最长回文字串的长度
for (int j = 0; j <= s.length()-i; j++) {
//循环比较 字符串以中心对称的字母是否相同 循环次数=字符串长度/2
for (int k = 0; k < i/2; k++) {
//判断以中心对称的字母是否相同
Boolean flag = s.substring(j+k, j+k+1).equals(s.substring((i-1)+j-k, i+j-k));
//如果相同,结束这次循环
if(!flag){
break;
}
//如果索引到了字符串的中间索引,则说明这个字符串以中心对称的字母全部相同
if(k==(i-2)/2){
//进到这个判断语句则说明这个字符串就是我们想要的结果了
result=s.substring(j,j+i);
}
}
}
//若进到下面的判断语句则说明:这个字符串是空串,结束循环并直接return空串
if(!(result.equals(""))){
break;
}
}
//暴力枚举过后,返回得到的结果
return result;
}
}
该程序的时间复杂度是:O(n^3),挂掉也是理所应当的,菜鸡的心酸
--------------------------------------------------------------------------------------------------------------------------------------------------------
借鉴其他大佬的动态规划及思想深入理解动态规划,我只是跟着他的思想再理解一次动态规划,如下:
(一)状态
- f[i][j]表示s的第 i 个字符到第 j 个字符组成的子串,是否为回文串
(二)转移方程
- 如果s的第 i 个字符和第 j 个字符相同的话,且 i + 1, 到 j - 1 的子串也是回文串的话,f[i][j] 也为回文串
- f[i][j] = f[i + 1][j - 1] and s[i] == s[j]
- 稍微要注意的是,如果 i == j 或者 i + 1 == j 的时候,也就是单个字符的子串和两个相邻字符的子串,就不需要f[i + 1][j - 1]了
- 如果s的第 i 个字符和第 j 个字符不同的话,f[i][j] 不是回文串
- 然后注意遍历顺序,i 从最后一个字符开始往前遍历,j 从 i 开始往后遍历,这样可以保证每个子问题都已经算好了
(三) 理解代码
注释是我根据自己的理解加的,理解不透彻之处,望见谅…
import java.util.Scanner;
public class Test {
static boolean flag=true;
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (flag){
boolean flag2=true;
System.out.println("请输入一段字符串,我将会返给您一段最长回文子串...");
String s = sc.nextLine();
System.out.println(longestPalindrome(s));
System.out.println("是否继续测试?请输入 y or n");
while (flag2){
String s1 = sc.nextLine();
if(s1.equals("n")||s1.equals("N")){
flag=false;
flag2=false;
}else if(s1.equals("y")||s1.equals("Y")){
flag2=false;
}else {
System.out.println("输入格式不对,请重新输入 y or n");
}
}
}
}
public static String longestPalindrome(String s) {
//解决空串问题
String result = "";
int len = s.length();
//模拟一个二维的表,第一个[]内的数字是字符串的起始索引,第二个[]内的数字是字符串的终止索引,
//指的是从起始索引到终止索引的字符串是否回文,若回文则将默认值false改为true
boolean[][] flags = new boolean[len][len];
for (int i = len - 1; i >= 0; i--) {
for (int j = i; j < len; j++) {
//如果s的第 i 个字符和第 j 个字符不同的话,f[i][j] 不是回文串,就不用进到判断语句改二维数组的值
if(s.charAt(i) == s.charAt(j) && (j - i <= 1 || flags[i + 1][j - 1])) {
//若进到这个判断语句,则从i索引开始,j索引结束的字符串必回文,改它对应二维数组那一项的值
flags[i][j] = true;
//如果这个回文字符串的长度大于此前回文字符串的长度,则覆盖此前的result值
if(j - i >= result.length()){
result = s.substring(i, j + 1);
}
}
}
}
//返回结果
return result;
}
}