动态规划

前提

我今天给大家讲一讲基础的算法,因为这块东西比较复杂,再加上本人其实也是一个小菜鸡,所以不敢涉及太多的理论知识,今天主要内容还是通过练习的方式让你们了解部分算法的大概思想。

递归

讲算法之前了,我们先来回顾一下递归的知识,以下是维基百科定义的递归

大部分的递归定义都由三个部分构成:基本情况的定义,递归法则和递归结束的情况,如果定义的对象是无限的,那么可以省略第三个部分。比如说,可以用递归定义的方式来定义如下的一个自然数集上的函数

f(0)=1
n>0,f(n)=n f(n-1).
举一个简单的例子,你和你的女朋友一起去看电影,你女朋友想知道你们坐的是第几排,但是电影院太黑了,没办法一排一排的数,那你就可以问你前面一个人,一排一排往前问(递归法则),直到前面没有人为止(终止条件)则递归的表达式为:
f(1) = 1
n>1,f(n) = f(n-1)+1;
递归代码如下:

//n>=1
if(n==1) return 1;
return  f(n-1)+1;

斐波那契数列

下面给大家出一道比较经典题,大家先思考一下:
大家应该都有了解过斐波那契数列把,它的定义如下:

  • F(0) = 0;
  • F(1) = 1;
  • F(n) = F(n-1)+F(n-2)

求f(10)的值;

if(n == 0) return 0;
if(n==1) return 1;
return  f(n-1)+f(n-2);

但是了,上面解题的思路是存在问题的在这里插入图片描述
我画个图帮助大家分析
你会发现图中存在大量的重复计算。大家下去不妨用计算器跑跑f(100)。看能得出什么结果。
既然存在大量的重复计算,那我们是否用一个一维数组能把这些计算过的值,保存起来啊,需要用它的时候直接拿出就行了。
甚至更简单的是,我如果从小往大计算,即f(2) = f(1)+f(0),f(3) = f(2)+f(1)…是否就避免了重复计算

if(n==0) return 0;
if(n==1) return 1;

int old = 0;	//f(0)
int last = 1;	//f(1)
int currentValue //当前值
for(int i = 2;i<=n;i++){
	currentValue = last+old;
	old = last;
	last=currentValue;
}

在这里我要说一下,计算机是一个没有感情的杀手,为什么这么说了,因为我相信大家的脑子可能会和我一样不自觉的跟着这个方程一层一层的递归下去,因为这符合我们平时的思维,但是人脑并不擅长递归,我这里要是给n=100了,你去递归试一试。所以递归最好的方法就是找到递归公式和终止条件。然后在翻译成代码,不要试图去人为递归。
但是计算机就不一样了,他就擅长我们不擅长的,相信大家都学过栈吧,先进后出,一层一层压下去,再把最上面的弹上来。你们有没有发现递归其实和栈是类似的。计算机把递归的问题一层一层的压人栈中,在一层一层的弹出来。
要小心的是,如果递归的深度大于虚拟机允许的深度,就会抛出StackOverFlow异常。

算法分类

回溯法

既然你们是做游戏开发的,不知道在坐的各位玩个steam上的一款游戏没有,游戏的名字叫《奇异人生》,在这款游戏里,女主角MAX拥有回到过去的超能力,每当她的闺蜜和她的小伙伴们有了生命危险,游戏都会给你开启一个回到过去的选项,在关键的岔路口,重新做选择。这个算法的思想如果体现在游戏里面的话,大概就是每一个岔路口,都做出一个看起来最优的选择,期望MAX的人生能达到最优。
那在这个游戏里面怎么才能在分叉路口做出最优的选择了,你们有撒好的提议?

比较有趣的是,回溯法没有你们想的那么高大上。有很多资深玩家(耐心玩家)就喜欢用回溯法解决这个问题。每个路口我都存个档,一个路口一个路口的实验,这个路口玩出的结果不让我满意,我就回退到上一个路口,直到玩出我满意的结果为止。

回溯法的理论大致就是这样,因此,回溯法非常适合用递归实现。为了让大家更明白,我找了一道题。大家思考一下。

迷宫问题

请设计一个函数,用来判断在一个矩阵中是否存在一条包含某字符串所有字符的路径。路径可以从矩阵中的任意一个格子开始,每一步可以在矩阵中向左,向右,向上,向下移动一个格子。如果一条路径经过了矩阵中的某一个格子,则该路径不能再进入该格子。 例如 a b c e s f c s a d e e 矩阵中包含一条字符串"bcced"的路径,但是矩阵中不包含"abcb"路径,因为字符串的第一个字符b占据了矩阵中的第一行第二个格子之后,路径不能再次进入该格子

首先我们找到递归的公式

f(i,j)=f(i-1,j)+f(i+1,j)+f(i,j+1)+f(i,j-1),终止条件是strs.length = startPos

代码实现如下:

package algorithm;

import java.util.Arrays;

/**
 * @description: 回溯法
 * @author: Ksssss(chenlin @ hoolai.com)
 * @time: 2019-12-22 18:01
 */

public class Solution {

    //1、第一点,也是重要的一点判断条件是否满足
    //2、需要一个数组来记录它的访问记录(路径不能重复进入)
    //3、设置一个hasPath,默认为 false
    //4、选择一点出发
    //5、判断字符串是否完全匹配。如匹配,标记更改为true,并返回
    //6、记录字符串匹配开始位置。判断是否满足条件,满足进入步骤7,不满足进入步骤10
    //7、访问点记录为true,匹配字符串下标++
    //8、进入步骤4并赋值给hasPath
    //9、判断hasPath,如果为false 访问点记录为false,匹配字符串下标--
    //10、返回hasPath

    public boolean hasPath(char[] matrix, int rowLen, int colLen, char[] str) {
        if (rowLen < 1 || colLen < 1 || matrix.length == 0 || str.length == 0) {
            return false;
        }
        
        boolean[] visited = new boolean[rowLen * colLen];
        int strPos = 0;
        for (int row = 0; row < rowLen; row++) {
            for (int col = 0; col < colLen; col++) {
                if (hasPathCore(matrix, row, rowLen, col, colLen, str, strPos, visited)) {
                    return true;
                }
            }
        }
        return false;
    }

    public boolean hasPathCore(char[] matrix, int row, int rowLen, int col, int colLen, char[] str, int strPos, boolean[] visited) {
        //指定位置
        if (strPos == str.length) {
            return true;
        }
        boolean hasPath = false;
        if (row >= 0 && col >= 0 && row < rowLen && col < colLen
                && str[strPos] == matrix[row * colLen + col] && !visited[row * colLen + col]) {
            visited[row * colLen + col] = true;
            strPos++;

            hasPath = hasPathCore(matrix, row + 1, rowLen, col, colLen, str, strPos, visited) ||
                    hasPathCore(matrix, row - 1, rowLen, col, colLen, str, strPos, visited) ||
                    hasPathCore(matrix, row, rowLen, col + 1, colLen, str, strPos, visited) ||
                    hasPathCore(matrix, row, rowLen, col - 1, colLen, str, strPos, visited);
                    
            if (!hasPath) {
                strPos--;
                visited[row * colLen + col] = false;
            }
        }
        return hasPath;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        System.out.println(solution.hasPath((
                "abce" +
                "sfcs" +
                "adee").toCharArray(), 3, 4, "bcced".toCharArray()));
    }
}

学会了这个。以后在和妹子一起玩迷宫的时候,妹子肯定会一脸崇拜的看着你的。

动态规划

先问一个问题啊,你们有女朋友吗?如果没有的花,那你们就要好好的听完这节课。有女朋友的人,就更要好好听这节课了。
我这儿出一个思考题啊,假设你有女朋友或者男朋友,然后了你女朋友在双11的时候在淘宝上领了一张满400减50的折扣,你该怎样让她从购物车里筛选出最接近400块钱的物品了?
很荣幸的告诉你,动态规划就是解决这个问题的。

动态规划解决的问题

动态规划通常是求一个问题的最优解(最大值或最小值),且每个问题都能分解成若干个子问题,子问题继续分解成若干个子问题。并且最优解依赖与每一个子问题的最优解。
由于子问题在分解过程中重复出现,为了避免重复,我们先计算小问题的最优解并保存(大部分都是把小问题的最优解存入一个一位数组或者二维数组里)下来。

01简单背包问题

我这里介绍一种比较基础的。我们有一个背包,背包总的承载重量是Wkg。现在我们有n个物品,每个物品的重量不等,并且不可分割。 我们现在期望选择几件物品,装载到背包中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大?

先来分析一下这道题,每个物品都有两种状态,我们可以选择装进去或者不装。我们这里先考虑使用回溯的方法,一个一个的去试,我们先推导出递归公式。对于n个物品来说,一定具有:

f(i,cw) = Max(f(i-1,cw),f(i-1,cw+Wi))  (i>0,i<=n,cw+Wi<w)
//i代表当前物品,cw代表当前背包容量,w代表总容量
	int maxCapacity = 0;	//结果放入maxCapacity中
    int[] items = {2,2,4,6,3};  //物品重量
    int bagCapacity = 9;    //背包容量
public void knapsack01Core(int startPos,int currentCapacity) {
        if (currentCapacity == bagCapacity || startPos == items.length) {
            if (currentCapacity > maxCapacity) {
                maxCapacity = currentCapacity;
            }
            return;
        }

        knapsack01Core(startPos + 1,currentCapacity);//选择不装第i个物品
        if (startPos >= 0 && startPos < items.length && bagCapacity >= 0 && currentCapacity < bagCapacity) {//选择装第i个物品
            if (items[startPos] + currentCapacity <= bagCapacity) {
                currentCapacity = items[startPos] + currentCapacity;
                knapsack01Core(startPos + 1,currentCapacity);
            }
        }
    }

因为每一个物品都有选择或者不选择这两种情况,所以n个物品的时间复杂度就是2的n次方。这是非常恐怖的,比如说如果我女朋友购物车里面有200件物品,那如果你给他来个回溯法。可能双十一早已过去。千万不要抱有过去就不花钱的想法,说不定到时候花的更多,还要被你女朋友可能会觉得你是一个智障程序员。那么回溯法行不通。我们来尝试下动态规划,我们先判断它是否满足动态规划的几个条件。

  • 是求最优解吗?
  • 可以拆分成若干个小问题吗?
  • 小问题之间是否存在重复计算?

第一二两个问题都容易明白,第三个问题我画个图来分析
我们假设背包的最大承载重量是9。我们有5个不同的物品,每个物品的重量分别是2,2,4,6,3。 其中f(i,j) ,i代表的是第几个物品,j代表的是背包当前容量 从图中可以看出来 f(2,2),f(3,2)等都存在重复计算,符合动态规划的要求,我们可以进行动态规划来求解。
key

动态规划版 简单0-1背包

我们把求解过程分为n个阶段,每个阶段会决策一个物品是否放到背包中。每个物品决策(放入或者不放入背包)完之后,背包中的物品的重量会有多种情况,也就是说,会达到多种不同的状态,对应到递归树中,就是有很多不同的节点。
我们把每一层重复的状态(节点)合并,只记录不同的状态,然后基于上一层的状态集合,来推导下一层的状态集合。我们可以通过合并每一层重复的状态,这样就保证每一层不同状态的个数都不会超过w个(w表示背包的承载重量),也就是例子中的9。于是,我们就成功避免了每层状态个数的指数级增长。
给大家把图画出来分析,(行代表物品的序号,列代表重量,存的值代表了状态) ,items={2,2,4,6,4}:
在这里插入图片描述
放入第二个物品
在这里插入图片描述
放入第三个物品
在这里插入图片描述
放入第4个物品
在这里插入图片描述
放入第五个物品
在这里插入图片描述

//动态规划

    //f(i,cw)=Max(f(i-1,cw),f(i-1,cw-Wi))
public int knapsack01Core_3(int[] items, int bagCapacity) {
        boolean[][] states = new boolean[items.length][bagCapacity + 1];
        states[0][0] = true;
        if (bagCapacity > items[0]) {
            states[0][items[0]] = true;
        }
        for (int i = 1; i < items.length; i++) {
            //不放入背包
            for (int j = 0; j <= bagCapacity; j++) {
                if (states[i - 1][j]) {
                    states[i][j] = true;
                }
            }
            //放入背包
            for (int j = 0; j <= bagCapacity - items[i]; j++) {
                if (states[i - 1][j]) {
                    states[i][j + items[i]] = true;
                }
            }
        }

        for (int i = bagCapacity; i >= 0; i--) {
            if (states[items.length - 1][i]) {
                return i;
            }
        }
        return 0;
    }

那么它的时间复杂度主要就是消耗在两层for循环那儿,就O(mn),相比指数型的是小了很多了,但是了它需要额外的空间,空间复杂度也是O(mn),所以动态规划,也是以空间换时间的算法。

还没有结束,我们还可以继续优化,仔细分析上面的二维矩阵,我们发现下一行包含了上一行的所有状态,所以最后一行包含了所有物品放入和不放入的状态,最终我们使用的也只有最后一行。那我们能否把矩阵压缩成一行了。答案是可以的。
实现代码如下。

public int knapsack01Core_1(int[] items, int bagCapacity) {
        boolean[] states = new boolean[bagCapacity + 1];
        //特殊处理
        states[0] = true;
        for (int i = 0; i < items.length; i++) {
            //
            for (int j = bagCapacity - items[i]; j >= 0; j--) {
                if (states[j]) {
                    states[j + items[i]] = true;
                }
            }
        }

        for (int i = bagCapacity; i >= 0; i--) {
            if (states[i]) {
                return i;
            }
        }
        return 0;
    }
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值