动态规划其实是分治思想的延伸,通俗一点来说就是大事化小,小事化无的思想。
在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接 使用这些结果
2个本质:
(1)状态的定义
(2)状态转移方程的定义 (也就是该状态与前一个状态之间的关系)
3个特点:
(1)把原来的问题分解成了几个相似的子问题。
(2) 所有的子问题都只需要解决一次。
(3)储存子问题的解
4个要素:
(1)状态定义
(2)状态间的转移方程定义
(3)状态的初始化
(4)返回结果
状态定义的要求:定义的状态一定要形成递推关系。
话不多说,下面直接通过几个典型例题来认识动态规划问题
例题1:青蛙跳台阶
(1)一次可跳1,2,3级
我们的中间状态F[i]表示跳上第 i 级台阶的方法数,因为一次可跳1、2、3级,所以最后一步到达i 时有三种可能的情况:
a:从i-1 级台阶跳了一级到达第 i 级
b:从i-2 级台阶一次跳了两级到达第 i 级
c:从i-3 级台阶一次跳了三级到达第 i 级
这三种情况的和就是我们要求的跳上 i 级台阶的方法数,
即得到的状态方程为:F[i]=F[i-1]+F[i-2]+F[i-3]
因此问题就转换成跳到 i-1 级,i-2 级 ,i-3级的方法数,依次不断往下递归就可求出任意 第 i 阶的方法数。但递归总该有个出口,因此,我们还必须知道它的初始状态:F[1]=1; F[2]=2; F[3]=4
即:跳到第1级有一种方法,一次跳1级,跳到第二级有两种方法,1+1或2,跳到第3级有四种方法:1+1+1或1+2或2+1或3。
接下来我们来看一下具体的代码实现:
/**
* 任务:小孩上一个n阶的楼梯,一次可上1,2,3阶,总共有多少种上法;
* 思想:动态规划
* 状态:F[i]=F[i-1]+F[i-2]+F[i-3]
* 初始状态:F[1]=1;F[2]=2;F[3]=4;
* 返回值:F[n]
* 状态:运行成功
* */
public class ClimbStairs {
public static int howWays(int n){
if(n==0||n==1){
return 1;
}else if(n==2){
return 2;
}
int[] F=new int[n+1];
F[0]=1;F[1]=1;F[2]=2;
for(int i=3;i<=n;i++){
F[i]=F[i-1]+F[i-2]+F[i-3];
}
return F[n];
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
while(sc.hasNext()){
int n=sc.nextInt();
System.out.println(howWays(n));
}
}
}
(2)对上述问题进行扩展:一次可跳1,2,…n级
状态:跳上i级台阶的方法数 : F(i) = F(i-1)+F(i-2)+F(i-3)+…+F(1) = 2*F(i-1)
初始状态:F(1)=1
返回结果:F(n)
代码只需对上述代码稍作变形即可;
public class ClimbStairs {
public static int howWays(int n){
if(n==0||n==1){
return 1;
}
int[] F=new int[n+1];
F[0]=F[1]=1;
for(int i=2;i<=n;i++){
F[i]=F[i-1]*2 ;
}
return F[n];
}
public static void main(String[] args) {
Scanner sc=new Scanner(System.in);
while(sc.hasNext()){
int n=sc.nextInt();
System.out.println(howWays(n));
}
}
}
例题2:一组数组中连续子序列的最大和
状态:以第i项结尾的连续子序列的最大和F(i)=max ( F(i-1)+a[i] , a[i] )
初始状态:F(0)=a[0]
返回结果:max(F[i])
代码如下:
public class Solution {
public int FindGreatestSumOfSubArray(int[] array) {
int[] F=new int[array.length+1];
F[0]=array[0];
int max=F[0];
for(int i=1;i<array.length;i++){
F[i]=Max(F[i-1]+array[i],array[i]);
if(F[i]>max){
max=F[i];
}
}
return max;
}
public int Max(int a,int b){
return a>=b?a:b;
}
}
例题3:判断一个字符串能否被分割成1个或多个单词
状态F(i):判断前i个字符能否被分割
F(i): F(1)~F(j) && F(j+1)~F(i) 能被分割
初始状态:F(0)=true
返回结果:F(n)
import java.util.Set;
public class Solution {
private int getMinLength(Set<String> dict){
int min=Integer.MAX_VALUE;
for(String string:dict){
min=Math.min(min,string.length());
}
return min;
}
public boolean wordBreak(String s, Set<String> dict) {
int n=s.length();
boolean[] F=new boolean[n+1];
int minLength=getMinLength(dict);
F[0]=true;
for(int i=0;i<n;i++){
if(!F[i]){
continue;
}
for(int j=i+1;j<=n;j++){
if(n-i<minLength){
break;
}
if(!F[j]){
F[j]=F[i]&&dict.contains(s.substring(i,j));
}
}
}
return F[n];
}
}
例题4:给定一个三角矩阵,找出自顶向下的短路径和,每一步可以移动到下一行的相邻数字
如图:由1可到2和3,每一个点都可以由它正上方的点和左上角的点到达,比如4可由2到4,也可由3到4.
因此得出下列状态关系:
状态F(i,j):表示到达第i 行第 j 列的点的最小路径,同样以4为例,到达4有两条路径,要么从2到4,要么从3到4,要求到4的最短路径,便可将其转换成求到2和到3的最短路径,然后取它们的最小值再加上4节点的权值便可得到到4点的最短路径。
状态方程为:
F(i,j) = min( F(i-1,j),F(i-1, j-1) ) +a[i][j]
边界情况考虑如下:
F(i,0)=F(i-1,0)+a[i][0]
F(i,i)=F(i-1,i-1)+a[i][i]
初始状态:F(0,0)=a[0][0]
返回结果:min(F(n-1,j))
C++代码实现如下:
class Solution {
public:
int minimumTotal(vector<vector<int>> &triangle) {
if (triangle.empty()){
return 0;
} // F[i][j], F[0][0]初始化
vector<vector<int>> min_sum(triangle);
int line = triangle.size();
for (int i = 1; i < line; i++){
for (int j = 0; j <= i; j++){
// 处理左边界和右边界
if (j == 0){
min_sum[i][j] = min_sum[i - 1][j];
}else if (j == i){
min_sum[i][j] = min_sum[i - 1][j - 1];
}else{
min_sum[i][j] = min(min_sum[i - 1][j], min_sum[i - 1][j - 1]);
}
min_sum[i][j] = min_sum[i][j] + triangle[i][j];
}
}
int result = min_sum[line - 1][0];
for (int i = 1; i < line; i++){
result = min(min_sum[line - 1][i], result);
}
return result;
}
};
例题5:从左上角到右下角有多少条路径(与上题分析相同)
状态F(i,j):F(i,j)=F(i-1,j)+F(i,j-1)
初始状态:F(0,0)=1
返回结果:F(m-1)(n-1)
public class Solution {
public int uniquePaths(int m, int n) {
int[][] op=new int[m][n];
op[0][0]=1;
if(m==1||n==1){
return 1;
}
for(int j=0;j<n;j++){
op[0][j]=1;
}
for(int j=0;j<n;j++){
op[j][0]=1;
}
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
op[i][j]=op[i-1][j]+op[i][j-1];
}
}
return op[m-1][n-1];
}
}
例题6:背包问题(求价值最大)
有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.
问多能装入背包的总价值是多大?
分析:
状态F(i,j):代表前i个商品,包的重量为j 的最大值(w[i]:第i个商品的重量;v[i]:第i个商品的价值)
大的方面分两种情况讨论:
(1)当第 i 个商品装不下时:即 w[i]>j
此时 F(i,j) = F(i-1,j)
(2)当第 i 个商品能装下时:即 w[i]<=j
此时我们可以选择装或者不装,而装不装的条件取决于看是否装了以后能让价值变得更大,因此 F(i,j) = max( F(i-1,j), F(i-1,j-w[i])+v[i] )
max()中的第一个参数表示不装的情况,第二个参数表示装的情况,如果不装,那麽此时的最大价值就等于前一次的最大价值,也就是F(i-1,j);重点来分析第二个参数:如果要选择装入第 i 个商品,那么我们就需要给第 i 个商品从包中腾出他所需要的空间(当然此题对应的是重量),因此就需要从包的总承重里减去第i 个商品的重量,即 j-W[i],代表包剩余的空间的可承重量,既然第i个物品已经放进去了,我们当然还要把它的价值算进去,因此也有了后面的+v[i]操作,最后我们只需要从它装和不装两种情况里选出他的最大值作为我们当前的状态量即可。
初始状态:F(0,j)=0; F(i,0)=0
返回结果:F(n,m)
代码实现如下:
public static int backPackII(int m, int[] A, int[] V) {
if (A.length < 1 || V.length < 1 || m < 1) {
return 0;
}
int N = A.length + 1;
int M = m + 1;
int[][] result = new int[N][M];
for (int i = 1; i != N; ++i) {
for (int j = 1; j != M; ++j) {
if (A[i - 1] > j) {
result[i][j] = result[i - 1][j];
} else {
int newValue = result[i - 1][j - A[i - 1]] + V[i - 1];
result[i][j] = newValue>result[i - 1][j]?newValue:result[i-1][j];
}
}
}
return result[N - 1][m];
}
例题7:回文串(给定一个字符串,将其分割成回文串,返回最小分割次数)
状态:
子状态:到第1,2,3,…,n个字符需要的小分割数
F(i): 到第i个字符需要的最小分割数
状态递推: F(i) = min{F(i), 1 + F(j)},
这句是什么意思呢?我们知道,回文串就是一个字符串正着读和倒着读的顺序是一样的。那么将一个字符串分割为回文串其中有一种分法就是将这个字符串拆分为单个字符,那样每个字符就是一个回文串,例如:“abc"拆分为回文串是"a”,“b”,“c”,这样我们只需要进行两次拆分就能将"abc"拆分为回文串。也就是说,一个长度为n的字符串,我们要将其拆分成回文串有一种拆法就是将其通过n-1次拆分拆成n个单个字符的回文串,但问题是,这样的拆法未必就是拆分次数最少的拆法,因此另一种拆法就是先将它的前j个字符拆分成回文串,同时保证j+1~i 也是是回文串,这样,通过递归的思想我们就可以知道前j 个字符能拆分出的回文串的最小分割次数+1,就可以构成另一种拆分方法,最后,我们在这两种拆分方法中选择出拆分次数最少的那一个作为当前的最小分割次数,即:F(i) = min{F(i), 1 + F(j)}, 依次递推便可得到长度为n的字符串拆分成回文串的最小拆分次数。
注意:j<i && j+1到i是回文串
简单来说,1 + F(j) 表示如果从j+1到i判断为回文字符串,且已经知道从第1个字符 到第j个字符的小切割数,那么只需要再切一次,就可以保证 1–>j, j+1–>i都为回文串。
初始化: F(i) = i - 1 上式表示到第i个字符需要的大分割数 比如单个字符只需要切0次,因为单子符都为回文串 2个字符大需要1次,3个2次…
返回结果: F(n)
代码如下:
public class Solution {
public int minCut(String s) {
int[] F=new int[s.length()+1];
for(int i=0;i<F.length;i++){
F[i]=i-1;
}
if(s.length()==1){
return 0;
}
for(int i=1;i<F.length;i++){
for(int j=0;j<i;j++){
if(ishuiwen(s,j,i-1)){
F[i]=(F[i]<=F[j]+1?F[i]:F[j]+1);
}
}
}
return F[s.length()];
}
public boolean ishuiwen(String s,int i,int j){
while(i<j){
if(s.charAt(i)!=s.charAt(j)){
return false;
}
i++;
j--;
}
return true;
}
}