【LeetCode系列】动态规划算法

前言

在知乎上看到有一篇对动态规划进行介绍的文章,觉得介绍的十分的好,这里贴出来和大家一起分享: 什么是动态规划?动态规划的意义是什么? 其实在三年前也写了一篇用动态规划求解的问题: 开心的小明——动态规划, 但是昨天在求解Leetcode的问题 : Longest Substring Without Repeating Characters 时又有动态规划的解法,看了知乎这篇文章之后觉得又加深了我的理解。

动态规划

  • 动态规划的本质,是对问题状态的定义和状态转移方程的定义。

引自维基百科

dynamic programming is a method for solving a complex problem by breaking it down into a collection of simpler subproblems.

动态规划是通过拆分问题,定义问题状态和状态之间的关系,使得问题能够以递推(或者说分治)的方式去解决。
本题下的其他答案,大多都是在说递推的求解方法,但如何拆分问题,才是动态规划的核心。
而拆分问题,靠的就是状态的定义和状态转移方程的定义。

  • 什么是状态的定义?

首先想说大家千万不要被下面的数学式吓到,这里只涉及到了函数相关的知识。
我们先来看一个动态规划的教学必备题:

给定一个数列,长度为N,
求这个数列的最长上升(递增)子数列(LIS)的长度.
以
1 7 2 8 3 4
为例。
这个数列的最长递增子数列是 1 2 3 4,长度为4;
次长的长度为3, 包括 1 7 8; 1 2 3 等.

要解决这个问题,我们首先要定义这个问题和这个问题的子问题。
有人可能会问了,题目都已经在这了,我们还需定义这个问题吗?需要,原因就是这个问题在字面上看,找不出子问题,而没有子问题,这个题目就没办法解决。

所以我们来重新定义这个问题:

给定一个数列,长度为N,
设F_{k}为:以数列中第k项结尾的最长递增子序列的长度.
求F_{1}..F_{N} 中的最大值.

显然,这个新问题与原问题等价。
而对于F_{k}来讲,F_{1} .. F_{k-1}都是F_{k}的子问题:因为以第k项结尾的最长递增子序列(下称LIS),包含着以第1..k-1中某项结尾的LIS。

上述的新问题F_{k}也可以叫做状态,定义中的“F_{k}为数列中第k项结尾的LIS的长度”,就叫做对状态的定义。

  • 什么是状态转移方程?

上述状态定义好之后,状态和状态之间的关系式,就叫做状态转移方程。
比如,对于LIS问题,我们的第一种定义的状态方程为:

F_{1} = 1 (根据状态定义导出边界情况)
F_{k}=max(F_{i}+1 | A_{k}>A_{i}, i\in (1..k-1)) (k>1)

这里可以看出,这里的状态转移方程,就是定义了问题和子问题之间的关系。

  • 动态规划迷思

a. “缓存”,“重叠子问题”,“记忆化”:这三个名词,都是在阐述递推式求解的技巧。以Fibonacci数列为例,计算第100项的时候,需要计算第99项和98项;在计算第101项的时候,需要第100项和第99项,这时候你还需要重新计算第99项吗?不需要,你只需要在第一次计算的时候把它记下来就可以了。上述的需要再次计算的“第99项”,就叫“重叠子问题”。如果没有计算过,就按照递推式计算,如果计算过,直接使用,就像“缓存”一样,这种方法,叫做“记忆化”,这是递推式求解的技巧。这种技巧,通俗的说叫“花费空间来节省时间”。都不是动态规划的本质,不是动态规划的核心。

b. “递归”:递归是递推式求解的方法,连技巧都算不上。

c. “无后效性”,“最优子结构”:上述的状态转移方程中,等式右边不会用到下标大于左边i或者k的值,这是”无后效性”的通俗上的数学定义,符合这种定义的状态定义,我们可以说它具有“最优子结构”的性质,在动态规划中我们要做的,就是找到这种“最优子结构”。

需要注意的是,一个问题可能有多种不同的状态定义和状态转移方程定义,存在一个有后效性的定义,不代表该问题不适用动态规划。这也是其他几个答案中出现的逻辑误区:动态规划方法要寻找符合“最优子结构“的状态和状态转移方程的定义,在找到之后,这个问题就可以以“记忆化地求解递推式”的方法来解决。而寻找到的定义,才是动态规划的本质。

Leetcode解法

参考上述解法, 我假设F(K)为到字符串第K个字母所具备的最长子串,那么导出状态方程为

F(1) = 1;
F(k)= F(k-1)+1 (如果第K个字母不在F(K-1)对应的最长子串中)
F(K)= 1(如果第K个字母在F(K-1)对应的最长子串中)

得到了如下的递归解法:

public class Solution {
    List<String> results = new ArrayList<String>();
    public int lengthOfLongestSubstring(String s) {
        int max = 0;
        if(s.length() == 0){
            return 0;
        }else{
        maxLengthbeforeK(s, s.length());
        for(String str : results){
            if(max < str.length()){
                max = str.length();
            }
        }
        return max;
        }
    }

    private String maxLengthbeforeK(String s, int k){
        String result = new String();
        if(k ==1) {
            result = String.valueOf(s.charAt(0));
            results.add(0, result);
        }else{
        result = maxLengthbeforeK(s, k-1);
        if(result.contains(String.valueOf(s.charAt(k-1)))){
            result = String.valueOf(s.charAt(k-1));
        }else{
            result += String.valueOf(s.charAt(k-1)); 
        }
            results.add(k-1, result);
        }
        return result;
    }
}

但是对于递归解法,有测试例无法通过,显示栈溢出,只得采用其他的解法。

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        int ans = 0;
        for (int i = 0; i < n; i++)
            for (int j = i + 1; j <= n; j++)
                if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
        return ans;
    }

    public boolean allUnique(String s, int start, int end) {
        Set<Character> set = new HashSet<>();
        for (int i = start; i < end; i++) {
            Character ch = s.charAt(i);
            if (set.contains(ch)) return false;
            set.add(ch);
        }
        return true;
    }
}

而通过上述解题我们发现使用动态规划的核心还是在数学建模,如何构建状态和状态迁移方程式关键,那么在小明的背包这个问题中是如何建模的呢?

这里写图片描述

这里C[i, w] 表示在钱为W时,第i个值所导致的最大的价值和。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值