前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、动态规划 (感谢程序员小灰)

动态规划宗旨:大事化小,小事化了

1、爬楼梯

有一座高度是10级台阶的楼梯,从下往上走,每跨一步只能向上1级或者2级台阶。要求用程序来求出一共有多少种走法。

1.1、递归算法

这个问题其实我们仔细思考,到最后的第10层楼梯,事实上只有2种走法,就是从9级上10层(一步一层)、8级上10层(一步2层)。 所以应该是F(10) = F(9) + F(8) 的结果。这是不是马上就可以得出结果呢 。通过下面的代码执行后有89种方法

@Test
public void method(){
    System.out.println(f(10));//89
}

public int f(int n){
    //第1级台阶有1中走法
    if (n == 1){
        return 1 ;
    }

    //第2级台阶有两种走法
    if (n == 2){
        return 2;
    }

    //其他台阶的走法是 如下
    return f(n-1) + f(n-2);
}

1.2、优化递归算法-添加备忘录

优化该算法:上面的时间复杂度我们先求一下 ,要算出F(n),我们先必须知道F(n-1)和F(n-2),要算F(n-1),就必须算F(n-2) 和 F(n-3)的值,依次类推 ,我们可以得到下面的图

1579414627491

所以时间复杂度 应该是2^n - 1 约等于2^n , 这个时候我们看到上面重复计算的还是挺多的,比如下面相同颜色的都被重新计算了。 我们可以保存一下已经计算过的东西,如果计算过,就不让它重新计算了,

1579414787858

这个时候,我加一个备忘录算法,保存一下当时的计算状态 ,集合map是一个备忘录。当每次需要计算F(N)的时候,会首先从map中寻找匹配元素。如果map中存在,就直接返回结果,如果map中不存在,就计算出结果,存入备忘录中。,调整代码后如下

@Test
public void method2() {
    Map<Integer, Integer> map = new HashMap<>();
    System.out.println(f2(10, map));
}

public int f2(Integer n, Map<Integer, Integer> map) {
    //第1级台阶有1中走法
    if (n == 1) {
        return 1;
    }

    //第2级台阶有两种走法
    if (n == 2) {
        return 2;
    }

    Integer value = map.get(n);
    if (value == null) {
        //其他台阶的走法是 如下
        value = f2(n - 1, map) + f2(n - 2, map);
        map.put(n, value);
        return value;
    }
    return value;
}

这个算法时间时间复杂度的话,F1到F(n),除了n=1,n=2,基本上每次都会进入map中,也就是说最后的map中一共存放了n-2个结果,那么时间复杂度就是n

1.3、动态规划求解

1.3.1、动态规划3个重要概念

最优子结构、边界、状态转移公式

F(10) = F(9) + F(8)最优子结构

当只有1级台阶或者2级台阶的时候,我们可以得到结果了,而且无需简化了再。我们称之为边界

F(n) = F(n-1)+F(n-2):这个是阶段与阶段之间的状态转移方程这个是动态规划的核心

从下到上推广

**其实递归算法到了这里基本上时间复杂度不能再优化了,但是空间复杂度我们还可以优化,现在是map存储的数据,数据量其实是比较多的, **

递归算法是从上到下进行递归的,那我们可不可以从下到上进行推导呢 ,就是说从1级台阶开始从下到上推 F(1) = 1,F(2) = 2,F(3) = F(1) +F(2),F(4) = F(3) +F(2), 等等等

1579415544392

1579415539295

1579415534035

这样下来,其实只要我们保留之前两个状态就可以一直推导到第10个了,这才是真正的动态实现哦,

代码解析:程序从 i=3 开始迭代,一直到 i=n 结束。每一次迭代,都会计算出多一级台阶的走法数量。迭代过程中只需保留两个临时变量a和b,分别代表了上一次和上上次迭代的结果。 为了便于理解,我引入了temp变量。temp代表了当前迭代的结果值。

@Test
public void method3() {
    System.out.println(f3(10));
}
public int f3(int n) {
    //第1级台阶有1中走法
    if (n == 1) {
        return 1;
    }
    //第2级台阶有两种走法
    if (n == 2) {
        return 2;
    }

    //从3级台阶开始,每次都等于前两级,相加,a是倒数第2级,b是倒数第1级
    int a = 1;
    int b = 2;
    int temp = 0;
    for (int i = 3; i <= n; i++) {
        temp = a + b;
        a = b;
        b = temp;
    }
    return temp;
}

这个算法的时间复杂度是O(n),但是空间复杂度值引入了3个变量,空间复杂度竟然只有1,简直牛掰了,但是这才只是比较简单的动态规划,还不算真正理解动态规划

2、国王和金矿

问题:有一个国家发现了5座金矿,每座金矿的黄金储量不同,需要参与挖掘的工人数也不同。参与挖矿工人的总数是10人。每座金矿要么全挖,要么不挖,不能派出一半人挖取一半金矿。要求用程序求解出,要想得到尽可能多的黄金,应该选择挖取哪几座金矿?

1)挖每一座金矿需要的人数是固定的,多一个人少一个人都不行。国王知道每个金矿各需要多少人手.

2)每一座金矿所挖出来的金子数是固定的.

3)开采一座金矿的人完成开采工作后,他们不会再次去开采其它金矿,因此一个人最多只能使用一次.

金矿编号 黄金储量 需要人数
1 400 5
2 500 5
3 200 3
4 300 4
5 350 3

2.1、确定最优子结构

不考虑人数够不够用,也不考虑到底挖不挖,第5个金矿存在挖与不挖的问题 。当然我们希望是全挖的,但是工人数有限制,所以可能存在不挖的情况 。这样就会出现2中最优子结构 ,最优子结构有两种,

一种是5金矿10工人,

一种是4金矿10工人。

如果选挖第五个金矿,那么前4个金矿所分配的工人就是【10 - 第五个金矿所需人数】

1579419903487

所以最优选择是:(前4个金矿10工人的挖金数量 )和 (前4个金矿7个工人+第4个金矿的挖金数量 )中的最大值

1579420169261

2.2、确定边界

为了便于描述,我们将金矿数量设置为N,工人数设为W,金矿的黄金量设为G[],金矿的工人用量设为数组P[]

那么5座金矿的最优选择是F(5,10) = MAX(F(4,10), F(4,10 - P[4]) + G[4])

F(n,w) 的解释为n个金矿,w个人的黄金量 (n个金矿不一定全部都挖,w个人也不一定全用)

现在我们确定下最终的边界吧,也就是最后剩下1个金矿的情况,这时存在挖与不挖的可能,因为人数可能已经分完了,如果人数分完了,那第一个金矿就不能挖了。

当n = 1 ;  w < P[0];        F(n, w) = 0    
当n = 1 ;  w >= P[0];       F(n. w) = G[0] ;        

//通过上面推导出来的 n-1为当前的金矿
当n > 1 ;  w <  P[n - 1];  F(n, w) = F(n - 1, w)   
当n > 1 ;  w >= P[n - 1];  F(n, w) = max(F(n-1,w), F(n-1,w-p[n-1])+g[n-1]) ;

2.3、方案实现

2.3.1、递归实现


    @Test
    public void diGuimethod() {
        int g[] = new int[]{400, 500, 200, 300, 350};//黄金量
        int p[] = new int[]{5, 5, 3, 4, 3};//人数
        int maxGold = diguiMaxGold(5, 10, g, p);
        System.out.println(maxGold);
    }

    /**
     * 递归算法
     */
    public int diguiMaxGold(int n, int w, int[] g, int[] p) {
        //n 等于 1 的情况
        if (n == 1 && w < p[0]) {
            return 0;
        } else if (n == 1) {
            return g[0];
        }

        //n 大于 1 的情况
        if (w < p[n - 1]) {
            return diguiMaxGold(n - 1, w, g, p);
        }
        //挖4座金矿
        int a = diguiMaxGold(n - 1, w, g, p);
        //挖5座金矿
        int b = diguiMaxGold(n - 1, w - p[n - 1], g, p) + g[n-1];
        return Math.max(a, b);

    }

2.3.2、动态规划

2.3.2.1、规划过程

表格第一列表示给定1-5座金矿的情况,也就是n的值,表格第一行给定的工人数也就是w的值

1579501567093

2.3.2.1.1、第1座金矿结果

第一座金矿需要的人数是5人,所以当人数小于5的时候,不会挖矿,挖金量为0

1579501556664

2.3.2.1.2、第2座金矿结果

当n > 1 ; w < P[n - 1]; F(n, w) = F(n - 1, w) ;
当n > 1 ; w >= P[n - 1]; F(n, w) = max(F(n-1,w), F(n-1,w-p[n-1])+g[n-1]) ;

**第二座金矿需要的工人数也是5人 **

1、当人数小于5的时候,不会挖矿,挖金量为0

2、当人数在5到9人的区间,人数不足以挖取两个金矿,只能挖一个矿

3、当人数等于 碰巧1号金矿也能挖取,所以我们必须同时考虑

1579422762013

1579422852220

1579423292938

2.3.2.1.3、第3座金矿结果

n > 1 F(n , w) = max(F(n-1, w), F(n-1,w-p[n-1] + g[n-1]));

1579423552114

2.3.2.1.4、第4座金矿结果

n > 1 F(n , w) = max(F(n-1, w), F(n-1,w-p[n-1] + g[n-1]));

1579423659605

2.3.2.1.5、第5座金矿结果

n > 1 F(n , w) = max(F(n-1, w), F(n-1,w-p[n-1] + g[n-1]));

1579423683726

2.3.2.2.、规律总结

这个时候,我们发现,除了第一个格子之外,每个格子都是前一行的一个格子或者两个格子推出来的

比如3金矿8工人的结果,第三个金矿需要3个工人,挖的金矿数是200, 就来自2金矿5工人和2金矿8工人 Max(500,500+200) = 700

1579423899239

比如5金矿和10工人 来自4金矿7工人和4金矿10工人Max(900,500+350) = 900

1579424009460

2.3.2.3、动态规范代码

在实现程序的时候,我们也可以像这样从左至右,从上到下一格一格推导出最终结果。

但是我们并不需要存储整个表格,只需要存储前面一行的结果,就可以推导出新的一行。我们来实现一下代码。



    @Test
    public void method() {
        int g[] = new int[]{400, 500, 200, 300, 350};//黄金量
        int p[] = new int[]{5, 5, 3, 4, 3};//人数
        int maxGold = getMostGold(5, 10, g, p);
        System.out.println(maxGold);
    }

    /**
     * @param n 第几个金矿
     * @param w 总共有几个人
     * @param g 数组,存放每个金矿的黄金数
     * @param p 数组,存放每个金矿需要的工人数
     * @return
     */
    public static int getMostGold(int n, int w, int[] g, int[] p) {

        //存放上一行的结果
        int[] preResults = new int[w +1];
        //存放当前行的结果
        int[] results = new int[w + 1];

        //填充边界格子的值
        //保存的是第一行的数据
        for (int i = 1; i <= w; i++) {
            if (i < p[0]) {
                preResults[i] = 0;
            } else {
                preResults[i] = g[0];
            }
        }

        //填充其余格子的值,从上一行推出下一行,外层循环是金矿数量,内层循环是工人数
        //内层循环工人数从第二行数据开始,所以i = 1
          for (int i = 1; i < n; i++) {
            for (int j = 1; j <= w; j++) {
                //p[i] 当前金矿的人数 g[i] 当前金矿的数量
                //  如果当前人数小于金矿,则最多挖的金矿数量等于相同人数 的上一层金矿的数量
                if (j < p[i]){
                    results[j] = preResults[j];
                }else {
                    int  a = preResults[j];
                    int  b = preResults[j - p[i]] + g[i];
                    results[j] =  Math.max(a, b) ;
                }
            }

            for (int j = 1; j <= w; j++) {
                preResults[j] = results[j];
            }
        }
        return results[w];
    }

3、交叉队列

问题:给出三个队列 s1,s2,s3 ,判断 s3 是否是由 s1 和 s2 交叉得来。 如:s1 为 aabcc , s2 为 dbbca。 当 s3 为 aadbbcbcac 时,返回 true(即将 s1 拆成三部分: aa,bc,c 分别插入 s2 对应位置) 否则返回 false。

3.1、解题思路

1、动态规划求解,构造一个长为 len2=s2.size()+1,宽为 len1=s1.size()+1 的 dp[][] 二维数组;

2、设有 ij,其中 i 表示字符串 s1的第 i 个字符, j 表示字符串 s2的第 j 个字符, t=i+j表示 s3的第 t 个字符;

3、dp[i][j] 如果为 1 ,表示 s1[i] 等于 s3[t] dp[i−1][j] 等于 1、 或者 s2[j] 等于 ` s3[t] dp[i][j−1] `;

4、简单的说 dp[i][j] 为 1 , 就表示这个点可达,以 dp[0][0] 为起点, dp[len1][len2] 为终点,dp数组中值为 1 , 的点为路径,向下走表示取 s1 的字符,向右走表示取 s2 的字符。这样就将抽象的字符组合转化成了更好理解的二维数组来表示;

5、最优子结构即为: s1,s2 i,j 点字符之前的字符能否交叉组合成字符串 s3 的前 i+j个字符,转换到二维数组即为, i,j 点左侧点和上方的点是否可达。

6、可以根据例1画出二维表格,便于分析:

s2/s1 * d b b c a
* 1 0 0 0 0 0
a 1 0 0 0 0 0
a 1 1 1 1 1 0
b 0 1 1 0 1 0
c 0 0 1 1 1 1
c 0 0 0 1 0 1

3.2、代码实现

初始化的数组,数据都为0

1、先将第一行和第一列的数据初始化

2、然后从 i=1,j=1,开始扩散,首先想明白的是,如果dp[i][j] ,j相同 dp[i -1][j]= 0 ,那么 dp[i][j]= 0,所以千万注意下面 | 或的用法

@Test
public void test(){
    System.out.println(method());
}

public String method() {
    String line = "aabcc,dbbca,aadbbcbcac";
    String[] arr1 = line.split(",");
    int len1 = arr1[0].length();
    int len2 = arr1[1].length();
    int len3 = arr1[2].length();

    //判断基本的关系
    if (len3 != len1 + len2) {
        return false + "";
    }
    if (len1 == 0) {
        return (arr1[1] == arr1[2]) + "";
    }
    if (len2 == 0) {
        return (arr1[0] == arr1[2]) + "";
    }


    //开始真正代码

    int[][] dp = new int[len1 + 1][len2 + 1];
    //第一个点必然可达
    dp[0][0] = 1;
    // 初始化数组第1列, 如果第第一列有一个为0,那么肯定不可达, 后续的都为0,此处代码甚好
    for (int i = 1; i <= len1; i++) {
        if (arr1[0].charAt(i - 1) == arr1[2].charAt(i - 1)) {
            dp[i][0] = dp[i - 1][0];
        }
    }
    //初始化数组第1hang
    for (int i = 1; i <= len2; i++) {
        if (arr1[1].charAt(i - 1) == arr1[2].charAt(i - 1)) {
            dp[0][i] = dp[0][i - 1];
        }
    }



    // dp
    for (int i = 1; i < len1 + 1; i++) {
        for (int j = 1; j < len2 + 1; j++) {
            int t = i + j;
           //下面这两个判断,相当于我们已经默认了,第一个数据是arr1[1] 的第一个子母,而这个子母在上面已经初始化了
            if (arr1[0].charAt(i - 1) == arr1[2].charAt(t - 1)) {
                dp[i][j] = dp[i - 1][j] | dp[i][j];
            }
            if (arr1[1].charAt(j - 1) == arr1[2].charAt(t - 1)) {
                dp[i][j] = dp[i][j - 1] | dp[i][j];
            }
        }
    }
    // 返回处理后的结果
    if (dp[len1][len2] == 1) {
        return true + "";
    }
    return false + "";
}

ContactAuthor