算法(6)-动态规划DP-最值

Abstract

base - 重复子问题、状态转移
// 509. 斐波那契数  -- 重复子问题 f(20) = f(19) + f(18) ,  f(19)拆解又需要重新解f(18), 用memo/dp数组来记住字问题的解,需要求解一个子问题时,先去看看适否以及有人解过了。
// 自顶向下(递归返回)、自低向上(循环迭代)、空间约简(只记录前两个状态)
int fib(int n)// 322. 零钱兑换 -- 状态转移-暴力递归(自顶向下), for循环(自底向上) 
// k=1, 2, 5 求amount=11, 可拆解为三个子问题取min, min(a=10, a=9,  a=6), 这三个方案+1硬币都能变成原文题的一个方案 (最优子结构 <- 子问题独立 <-硬币数量没有限制)
// dp(n) 凑出amount为n的最少硬币数
int coinChange(vector<int>& coins, int amount);

advanced - dp 数组初值设置
// 931. 下降路径最小和 -- dp数组-从上到下从左到右,dp[i][j]初值设置为不能取到的最小值10001;
// 最后一行选一个最小值
int minFallingPathSum(vector<vector<int>>& matrix);
// 300.最长递增子序列 - dp[i] 以nums[i]结尾的,拥有最大的长度 递增子序列 的长度

动态规划 - Dynamic Programming, 常见于最值问题解题。

核心是穷举:穷举所有子问题,由子问题 递推 原问题题。需要将原问题拆解为子问题,子问题需要满足相互独立,暴力递归所有子问题的解-> 求最优 ->递推 ->得到原问题得到解

聪明的穷举:动态规划存在重叠子问题,如果暴力求解效率会很低,所以 在穷举所有可能解的时候,可以使用DP table记录已求可能解,避免可能解重复计算。 f ( 19 ) = f ( 18 ) + f ( 17 ) f(19) = f(18) + f(17) f(19)=f(18)+f(17) 自底向上递推可以不用多次求 f ( 18 ) f(18) f(18)

DP三要素:1.状态转移:如何从大变小(由小的信息 + 有效判断条件)得出大的结论、2.边界条件、3.dp数组的定义和填充

典型题目:数组-子序列(连续/不连续)、字符串-子序列(不连续),子串(连续)

Tips:

  1. 最优子问题 应理解为 对所有子问题的解 求最值。要保证原问题的解 必须 包含在所有子问题中。各个班最高成绩 可以推 全校最高成绩; 各个班最大成绩差 不能推 全校最大成绩差(最大成绩差可会出现在不同的班级)
  2. 状态转移方程 是在穷举,DP table 是在聪明的穷举
  3. 子问题相互独立理解:每个科目考最高分,如果每个科目的成绩不相互独立,那其实每个科目都各自求一个最高分,最后无法由各个科目最高分得出总分,因为该状态不可达。
  4. 一维dp,dp[i] --以item[i]结尾的,有效的,最长XXX的长度

1. DP-Base: 重复子问题、状态转移

509.斐波那契数

509.斐波那契-带你了解重叠子问题, 其没有求最值,严格来说不是动态规划
509.斐波那契 -(通常用 F(n) 表示)形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: F ( 0 ) = 0 , F ( 1 ) = 1 F(0) = 0,F(1) = 1 F(0)=0F(1)=1 F ( n ) = F ( n − 1 ) + F ( n − 2 ) ,其中 n > 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1 F(n)=F(n1)+F(n2),其中n>1。给定 n ,请计算 F(n) 。

  • 自顶向下(递归返回)、自低向上(循环迭代)、空间约简(只记录前两个状态)
class Solution {
    vector<int> _memo;
public:
	// 509. 斐波那契数  -- 重复子问题 f(20) = f(19) + f(18) ,  f(19)拆解又需要重新解f(18), 用memo/dp数组来记住字问题的解,需要求解一个子问题时,先去看看适否以及有人解过了。
	// - 自低向上(循环迭代) + 空间约简(只记录前两个状态)
    int fib(int n) {
        if (n < 2) {
            return n;
        }
        int fi_1 = 1;
        int fi_2 = 0;
        int res = 0;
        for (int i = 2; i < n+1; i++) {
            res = fi_1 + fi_2;
            fi_2 = fi_1;
            fi_1 = res;
        }
        return res;
    }
	// 自低向上(循环迭代) + DP tabel
    int fib_2(int n) {
        _memo.resize(n+1);
        if (n < 1) {
            return n;
        }
        _memo[1] = 1;
        for (int i = 2; i < n+1; i++) {
            _memo[i] = _memo[i-1] + _memo[i-2];
        }
        return _memo[n];
    }
    // 自顶向下(递归返回) + DP tabel
    int fib_1(int n) {
        _memo.resize(n+1);
        return helper_1(n);
    }
    int helper_1(int k) {
        if (k <= 1) {
            return k;
        }
        if (_memo[k] != 0) {
            return _memo[k];
        }
        _memo[k] = helper(k-1) + helper(k-2);
        return _memo[k];
    }
   // 自顶向下(递归返回) + 暴力穷举,子问题重复计算
   int fib_0(int n) {
        return helper(n);
    }
    int helper_0(int n) {
        if (n < 2) {
            return n;
        }
        return helper(n-1) + helper(n-2);
    }
};

322.零钱兑换

322.零钱兑换 – 给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1 。你可以认为每种硬币的数量是无限的。

class Solution {
public:
	//322. 零钱兑换 -- 状态转移-暴力递归(自顶向下), for循环(自底向上) 
	//k=1, 2, 5 求amount=11, 可拆解为三个子问题取min, min(a=10, a=9,  a=6), 这三个方案+1硬币都能变成原文题的一个方案 (最优子结构 <- 子问题独立 <-硬币数量没有限制)
	// dp(n) 凑出amount为n的最少硬币数
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount+1, amount+1);   // dp[i] amount = i 需要的最少硬币数, 很烦
        dp[0] = 0;  // base case 总是很头疼的。
        for (int i = 1; i <= amount; i++) {
            for (int coin : coins) {
                // cout << i << ", " << coin << "," << endl;
                if (i - coin >= 0 && dp[i - coin] != amount+1) {
                    // cout << i << ", " << coin << "," << dp[i-coin] << endl;
                    dp[i] = min(dp[i], dp[i-coin]+1);
                }
            }
        }
        return dp[amount] != amount+1 ? dp[amount]: -1;
    }
};

2. DP-Advanced: DP数组初值设置

931. 下降路径最小和 - 数

931.下降路径最小和 – 给你一个 n x n方形 整数数组 matrix ,请你找出并返回通过 matrix下降路径最小和下降路径 可以从第一行中的任何元素开始,并从每一行中选择一个元素。在下一行选择的元素和当前行所选元素最多相隔一列(即位于正下方或者沿对角线向左或者向右的第一个元素)。具体来说,位置 (row, col) 的下一个元素应当是 (row + 1, col - 1)(row + 1, col) 或者 (row + 1, col + 1)

class Solution {
public:
  	// 迭代求解,自底向上
    int minFallingPathSum(vector<vector<int>>& matrix) {
       int n = matrix.size();
        vector<vector<int>> dp(n, vector<int>(n, 0));
        copy(matrix[0].begin(), matrix[0].end(), dp[0].begin());  // base case
        for (int i = 1; i < n; i++) {
            for (int j = 0 ; j < n; j++) {
                int min_pre = 10001;  // 单元 100 * 行数100 累计最大值
                int offsets[3][2] = {{-1, -1,}, {-1, 0}, {-1, 1}};
                for (auto& offset:offsets) {
                    int pre_i = i + offset[0], pre_j = j + offset[1];
                    if (pre_i < 0 || pre_i >= n || pre_j < 0 || pre_j >= n) {
                        continue;
                    }
                    min_pre = min(min_pre, dp[pre_i][pre_j]);
                 }
                dp[i][j] = min_pre + matrix[i][j];  // 前路最短 + 走到[i][j]的增量
                // cout  << "i: " << i << ", j: "  << j << ", dp: " << dp[i][j] << endl;
            }
        }
        int res = 10001;
        for (int j = 0; j < n; j++) {
            res = min(res, dp[n-1][j]);
        }
        return res;
      
        // int res = 10001;
        // for (int j = 0; j < _n; j++) {
        //     res = min(res, helper(_m-1, j, matrix));
        // }
        // return res;
    }
		// 递归求解 自顶向下
    int helper(int row, int col, vector<vector<int>>& matrix) {
        // cout << row << ", " << col << endl;
        if (row < 0 || col < 0 || row > _m - 1 || col > _n -1 ) {
            // cout << "a: "<< row << ", " << col << endl;
            return 10001;    // 取不到的位置,返回无法娶到的值
        }
        if (row == 0) {
            // cout << "b: "<< row << ", " << col << endl;
            return matrix[row][col];
        }
        int tmp = 10001;
        int offsets[3][2] = {{-1, -1}, {-1, 0}, {-1, 1}};
        for (auto& offset : offsets) {
            int pre_row = row + offset[0];
            int pre_col = col + offset[1];
            tmp = min(tmp, helper(pre_row, pre_col, matrix) + matrix[row][col]);
            // cout << "c: "<< row << ", " << col << "," << tmp << endl;
        }
        return tmp;
    }
};

300. 最长递增子序列 - 长度

300.最长递增子序 - [longest-increasing-subsequence] 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。子序列 是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4
class Solution {
public:
      // 数组无序,nums[i] = 8, 可以接在nums[i-1] = 2后,也能接在nums[i-2] = 6 后面,明显 nums[i-2] = 6 能够构成的递增子序列潜力大一些,因为无序,所以我们要遍历一下 nums[i] 前面的所有潜在候选对象。
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 1);
        int res = 1;
        for (int i = 1; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if(nums[i] > nums[j]) {  // nume[i]接 在每个现成的递增子序列后,从有望构成新递增子序列里,找出最大的
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            res = max(res, dp[i]);
        }
        return res;
    }
};

53. 最大子数组和 - 数

53.最大子数组和 - 给你一个整数数组 nums ,请你找出一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。子数组 是数组中的一个连续部分。

dp[i] = max(nums[i], nums[i] + dp[i-1]), nums[i]

  1. 自成一派:说明nums[i] > nums[i] + dp[i-1] , 即 dp[i-1] < 0, nums[i-1]成份被舍弃了,这些成分只有副作用,不要也罢,nums[i]开启新征程,往下去找一找
  2. 建立连结:说明nums[i] < nums[i] + dp[i-1], 即nums[i]<0,
    如果nums[i]不太小(nums[i] + dp[i-1]>0), 还是有希望numi[i+1]能够打平numi[i]带来的副作用,
    如果nums[i] 太小了(nums[i] + dp[i-1]>0), 在nume[i+1] 决策时,会直接把numi[i]成分舍弃。
class Solution {
public:

    int maxSubArray(vector<int>& nums) {
        int n = nums.size();
        vector<int> dp(n, 0);   // dp[i] 以 dp[i] 结尾的连续子数组的最大和
        dp[0] = nums[0];
        int res = dp[0];
        for (int i = 1; i < n; i++) {
            if (nums[i] + dp[i-1] > nums[i]) {
                dp[i] = dp[i-1] + nums[i];
            } else {
                dp[i] = nums[i];
            }
            // cout << dp[i-1] << ", ";
            res = max(res, dp[i]);
        }
        // cout << dp[n-1] << endl;
        return res;
    }
};

1143. 最长公共子序列 - 长度

1143.最长公共子序列 – 给定两个字符串 text1 和 text2,返回这两个字符串的最长 公共子序列 的长度。如果不存在 公共子序列 ,返回 0 。一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的 公共子序列 是这两个字符串所共同拥有的子序列。

(两个字符串,二维dp 才是标配嘛)
dp[i][j] 表示s1[0]-s1[i] 于s2[0]-s2[j] 的最长公共子序序列,更新形式于512题类似,只不过初值和方向不大一样,初值为第0行第0列均为0,方向由上到下,由左到右。

class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int m = text1.size(), n = text2.size();
        vector<vector<int>> dp(m+1, vector<int>(n+1, 0)); // dp[i][j] t1[0-i] t2[0-j] 公共子序列的长度;
        for (int i = 1; i < m + 1; i++) {
            for (int j = 1; j < n + 1; j++) {
                if (text1[i-1] == text2[j-1]) {  // 对角线传输,s_1[i-1] s_2[j-1]同为公共子序列一部分
                    dp[i][j] = dp[i-1][j-1] + 1;
                } else {                         // 边传输,s_1[i-1] s_2[j-1]其一 or none 为公共子序列的一部分
                                                 // dp[i-1][j] dp[i][j-1] 其一是通过对角线操作来的话,那么s_1[i-1] s_2[j-1] 其一是公共子序列的一部分
                    dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
                }
            }
        }
        return dp[m][n];
    }
};

// 最长公共子序 - 回溯法去找是谁

def longestCommonSubsequence(self, text1, text2):
    l1, l2 = len(text1), len(text2)
    dp = [[0] * (l2 + 1) for _ in range(l1 + 1)]
    for i in range(1, l1 + 1):
        for j in range(1, l2 + 1):
            if text1[i-1] == text2[j-1]:
                dp[i][j] = dp[i-1][j-1] + 1
            else:
                dp[i][j] = max(dp[i-1][j], dp[i][j-1])
    # return dp[-1][-1]
    #  回溯 只有加
    res_str=[""]*dp[-1][-1]
    i,j=l1,l2
    while(i>0):
        if dp[i][j]>dp[i-1][j]:                   # 比上面的大,不是来上面
            if dp[i][j]>dp[i][j-1]:				  # 	比左边的大,不是来自左边
                res_str[dp[i][j]-1]=text1[i-1]    # 		来自对角线操作
            else:                                 # 	没有左边大,来自左边 横坐标操作
                i+=1							  # 		i 需要先+1,最后的-1会抵消
            j-=1								  # 	没有左边大,来自左边,纵坐标操作
        i-=1
    print(res_str)

—20240118—

3.

32. 最长有效括号–子串

题目:给定一个只包含 ‘(’ 和 ‘)’ 的字符串s,找出最长的包含有效括号的子串的长度。

套用:最长回文子串 + 改造 NO!!!!
dp[i][j] 更新
dp[i+1][j-1] == True and dp[i] == “(” and dp[j] == “)”
dp[i][j-1] == Flase and dp[i] == “(” and dp[j] ==“)”
状态转移不像 最长回文子串dp[i+1][j-1] -> dp[i][j] 中间子串仅需考虑是/否 回文,s[i] s[j] 单独加一个都不会影响中间的回文状态
dp[i+1][j-1] = False and dp[i] == “(” and dp[j] == “)” 但是dp[i][j] 的状态更新不一致
“( ( )”
“( )( )”

# 1.s[i]==")" and s[i-1]=="("    dp[i] = dp[i-2]+2
# 2.s[i]==")" and s[i-1]==")"    dp[i] = dp[i-1] + dp[i-dp[i-1]-2]+2 下标的合理性
# ((xx)) dp[i-1] 也有效的情况下
    def longestParentheses(self, s: str) -> int:
        """
        @brief 当s[i] == ")", 考虑所有会新增闭合情况
                dp[i] = dp[i-1] + 2            <=   s[i] == ")" and s[i-1] == "(" 
                dp[i] = dp[k-1] + dp[i-1] + 2  <=   s[i] == ")" and s[k] == "(" and s[i-1] == ")"
                    s[i] == ")"   可能新增闭合括号, 附加s[k] == "(" 可以形成有效括号
                    s[i-1] == ")" 可能是有效括号的一部分
                    如果 dp[i-1]表示的有效括号,不包含 s[k] "(", 就明确会新增闭合括号
        @note 注意边界条件
        """

        n = len(s)
        dp = [0] * n
        res = 0
        for i in range(1, n):
            if s[i] == ")" and s[i-1] == "(":
                dp[i] = dp[i-2] + 2 
            elif s[i] == ")" and s[i-1] == ")":
                k = i - 1 - dp[i-1]
                if k >= 0 and s[k] == "(":
                    dp[i] = dp[i-1] + 2
                    if k - 1 >= 0:
                        dp[i] += dp[k - 1]
            res = max(res, dp[i])
        return res

2.维度d


32. 最长回文子串–是什么

状态转移:dp[i][j] = s[i]==s[j] and dp[i+1][j-1]
边界条件:主对角线,副对角线
dp数组含义:dp[i][j] 表示s[i]-s[j]子串是否是回文子串
dp数组更新方向:状态转移决定了dp初始化元素(对角线)和更新的方向(由下向上,由对角线往右)

(独自一个人也,也可以二维dp, 强!二维dp 遍历所有子串,子串左右扩展)

def longestPalindrome(self, s):
    n = len(s)
    if n < 2:          # 空字符和单个字符直接输出
        return s
    dp = [[False] * n for _ in range(n)]
    for i in range(n):
        dp[i][i] = True
    res_str, res_len = s[0], 1    # 两个字符的的时候,不相等时结果为单个字符
    for i in range(n-2,-1,-1):
        for j in range(i+1, n):
            if j == i+1:          # 次对角线上的元素单独判断。
                if s[i] == s[j]:
                    dp[i][j] = True
                    if j-i+1 > res_len:
                        res_str = s[i:j+1]
                        res_len = j - i + 1
            else:
                if s[i] == s[j] and dp[i+1][j-1]:
                    dp[i][j] = True
                    if j-i+1 > res_len:
                        res_str = s[i:j+1]
                        res_len = j - i + 1
    return res_str  

中心扩展法:

class Solution {
public:
    string palindrome(string s, int left, int right) {
        int n = s.length();
        while(left >= 0 && right < n && s[left] == s[right]) {
            left -= 1;
            right += 1;
        }
        // std::cout << s << "," << left << "," << right << "," << endl;
        return s.substr(left+1,  right - 1 - left);     // substr 位置和长度
    }

    string longestPalindrome(string s) {
        int n = s.length();
        string res = "";
        for (int i = 0; i < n; i++) {
            // std::cout << "i1-" << i << endl;
            string s1 = palindrome(s, i, i);
            // std::cout << "i2-" << i << endl;
            string s2 = palindrome(s, i, i+1);
            // std::cout << "i3-" << i << "," << s1 << "," << s2 << endl;
            res = res.length() > s1.length() ? res : s1;
            res = res.length() > s2.length() ? res : s2;
        }
        return res;
    }
};

512. 最长回文子序列–长度

和上题的基本思路一样,不过dp数组表示的含义变为
dp[i][j] 表示s[i]-s[j]子串中回文序列的长度

# if s[i]==s[j]:dp[i][j] = dp[i+1][j-i]+2,
# if s[i]!=s[j]:dp[i][j] = max(dp[i+1][j],dp[i][j-1])
def longestPalindromeSubseq(self, s):
    n = len(s)
    if n < 2:
        return n
    dp = [[0] * n for _ in range(n)]
    for i in range(n):
        dp[i][i] = 1
    for i in range(n-2, -1, -1):
        for j in range(i+1,n):
            if s[j] == s[i]:
                dp[i][j] = dp[i+1][j-1] + 2
            else:
                dp[i][j] = max(dp[i+1][j], dp[i][j-1])
    return dp[0][n-1]

128. 最长连续序列–长度

给定一个未排序的整数数组,找出最长连续序列的长度。要求算法的时间复杂度为 O(n)。

数组本身无序,连续序列有序,只要能找到连续序列的开头,就能确定序列长度,迭代更新最长长度就可以了。

def longestConsecutive(self, nums):
    if not nums:
        return 0
    res = 1
    nums_set = set(nums)
    for val in nums_set:
        if val - 1 not in nums_set:     # val 为连续序列的开头
            count = 1
            num = val
            while(num + 1 in nums_set):
                count += 1
                num = num + 1
            res = max(res, count)
    return res

14. 最长公共前缀-字符串

暴力法:纵向扫描
先验:最长公共前缀不回比最短的字符串长,所以先求出最短的长度。

def longestCommonPrefix(self, strs):
    """
    :type strs: List[str]
    :rtype: str
    """
    n = len(strs)
    if n == 0:
        return ""
    min_l = float("INF")
    for string in strs:
        min_l = min(len(string), min_l)
    i = 0
    con_pre = ""
    while(i < min_l):
        con_pre = strs[0][:i+1]
        for string in strs:
            if string[:i+1] != con_pre:
                return con_pre[:i]
        i += 1
    return con_pre

剑指offer-48 最长不含重复字符串的子字符串-长度

请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3

移动窗口,保证窗口内的字符无重复,如果有重复就缩小窗口。

def lengthOfLongestSubstring(self, s):
    """
    :type s: str
    :rtype: int
    """
    win = {}
    left, right = 0, 0
    n = len(s)
    res = 0
    while (right < n):
        c1 = s[right]
        if win.get(c1):
            win[c1] += 1
        else:
            win[c1]  = 1
        right += 1
        while(win[c1]>1):
            c2 = s[left]
            win[c2] -=1
            left += 1
        res  = max(res, right - left)
    return res
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值