前言
说到动态规划首先不得不提及数学归纳法,与多米诺骨牌类似,只要证明如下两个条件成立:
1、证明第一张骨牌会倒;
2、证明只要任意一张骨牌倒下,那么与其相邻的下一张骨牌也会倒下。
那么便可以证明一下结论:所有的骨牌都会倒下。
动态规化继承了数学归纳的思想,利用动态规划解决算法问题时也需要分两步:
1、寻找算法求解的初始状态和初始值对应的结果;
2、寻找在给定范围内下一个状态与上一个状态的转移关系;
下面通过几个特殊且典型的动态规划算法题感受动态规划求解的过程。
比特位计数
在众多简单的动态规划算法中,比特位计数这道题还是有那么一丝思考的价值。首先看一下这道题的题目描述:
给你一个整数n,对于0<=i<=n中的每个i,计算其二进制表示中1的个数,返回一个长度为n+1的数组ans作为答案。
示例:
输入:n = 2 输出:[0,1,1] 解释: 0 --> 0 1 --> 1 2 --> 10
这道题乍一看需要将每一个输入的整数转化为对应的二进制流,然后统计其中包含“1”的个数。虽然这样的求解思路没有问题,也比较容易实现,但是非常耗费时间。然而,我们只要仔细思考一下二进制数在整数域中的变化规律,我们就很容易发现:
1、初始状态下:0--->0;1--->1;
2、对于大于1的整数n,当n 为奇数时,即是在上一个整数包含“1”个数的基础上+1,当n为偶数时,“1”的个数是n/2时“1”的个数,比如8--->1000,4--->100;6--->110;3--->11;
所以该题的求解如下(JAVA语言实现):
class Solution {
public int[] countBits(int n) {
if(n==0){
return new int[]{0};
}else if(n==1){
return new int[]{0,1};
}else{
int[] dp=new int[n+1];
dp[0]=0;
dp[1]=1;
for(int i=2;i<=n;i++){
if(i%2==1){
dp[i]=dp[i-1]+1;
}else{
dp[i]=dp[i>>1];
}
}
return dp;
}
}
}
括号生成
这道题我初次想到使用回溯法来求解,直到我看到另外一种求解思路---动态规划,我便大声惊呼:世间还真有如此清奇的脑回路。首先我们来看一下这道题的描述:
数字n代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且有效的括号组合。
比如:
输入:n = 3 输出:["((()))","(()())","(())()","()(())","()()()"]
如何使用动态规划呢,首先我们得明确一点,每次转态的转移就是增加一个括号,那么问题就在于如何去增加这个括号? 这里我就不做过多描述了,直接给出答案:将每一个整数对应的所有括号组合都记录下来,这样便得到了如下形式的中间结果:
n=0=>{""};
n=1=>{"()"};
n=2=>{"(())","()()");
n=3=>{"((()))","(()())","(())()","()(())","()()()"}
...
那么要得到n+1括号的组合情况,就可以返回中间结果中遍历所有i+j=n的情况得到“str1”与“str2”,然后添加括号"(str1)+str2"得到结果。代码如下(JAVA语言):
class Solution {
public List<String> generateParenthesis(int n) {
List<List<String>> rt =new ArrayList<>();
List<String> list1=new ArrayList<>();
list1.add("");
List<String> list2=new ArrayList<>();
list2.add("()");
rt.add(list1);
rt.add(list2);
for(int i=2;i<=n;i++){
List<String> list=new ArrayList<>();
for(int j=0;j<i;j++){
List<String> li1=rt.get(j);
List<String> li2=rt.get(i-j-1);
for(String e1:li1){
for(String e2:li2){
String e3='('+e1+')'+e2;
list.add(e3);
}
}
}
rt.add(list);
}
return rt.get(n);
}
}
正则表达式
动态规划最难的一步就是怎样取寻找递推关系式,拥有一个寻找递推关系式的思路,动态规划的题基本就不在话下。这里举一个需要分类讨论的题,如何分类分几类将直接影响着最后的求解效果。对于正则表达式这道题的描述如下:
给你一个字符串s和一个字符规律p,请你来实现一个支持‘.’和'*‘的正则表达式匹配。
- ‘.’匹配任意单个字符;
- ‘*’匹配零个或多个前面的那一个元素。
所谓匹配,是要涵盖整个字符串s的,而不是部分字符串。
对于匹配本身来说‘.’是需要单独考虑的,代码实现如下:
public boolean matchs(String s,String p,int i,int j){
if(i==0){
return false;
}
if(p.charAt(j-1)=='.'){
return true;
}
return p.charAt(j-1)==s.charAt(i-1);
}
然后就是‘*’符号,可以匹配零个或多个。所以必有dp[i][j]=dp[i][j-2];另外如果i位置与j-1位置相匹配,那么此时dp[i][j]=dp[i-1][j]||dp[i][j],即‘*’是一个“可有可无的字符”。整个代码如下:
class Solution {
public boolean isMatch(String s, String p) {
int lens=s.length();
int lenp=p.length();
boolean[][] dp=new boolean[lens+1][lenp+1];
dp[0][0]=true;
for(int i=0;i<=lens;i++){
for(int j=1;j<=lenp;j++){
if(p.charAt(j-1)=='*'){
dp[i][j]=dp[i][j-2];
if(matchs(s,p,i,j-1)){
dp[i][j]=dp[i-1][j]||dp[i][j];
}
}else{
if(matchs(s,p,i,j)){
dp[i][j]=dp[i-1][j-1];
}
}
}
}
return dp[lens][lenp];
}
public boolean matchs(String s,String p,int i,int j){
if(i==0){
return false;
}
if(p.charAt(j-1)=='.'){
return true;
}
return p.charAt(j-1)==s.charAt(i-1);
}
}
另外一些经典的动态规划题目如下: