动态规划的常用状态转移方程总结

本文总结了动态规划的常用状态转移方程,涵盖斐波那契数列、爬楼梯问题、背包问题、最长递增子序列、最大子数组和、最长公共子序列、编辑距离、打家劫舍和最大正方形等经典问题,给出各问题定义及动态规划方程,并介绍了相应的计算函数和步骤。

动态规划的常用状态转移方程总结

1.斐波那契数列

1. 斐波那契数列定义

斐波那契数列是一个经典的数学数列,其中每个数字是前两个数字的和。通常,斐波那契数列的前几个数字是:1, 1, 2, 3, 5, 8, 13, 21, …

2. 动态规划方程

动态规划的转移方程为 dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示第 i 个斐波那契数。

// 定义一个函数,接受斐波那契数列的序号作为参数
function fibonacci(n) {
    // 初始化一个数组dp,用于存储斐波那契数列的值
    const dp = [];

    // 初始条件:第0个和第1个斐波那契数都为1
    dp[0] = dp[1] = 1;

    // 通过动态规划的转移方程计算每个斐波那契数
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    // 返回第n个斐波那契数
    return dp[n];
}

// 测试例子
const n = 5;
const result = fibonacci(n);
console.log(`${n}个斐波那契数为:${result}`); 
//第5个斐波那契数为:8 数组[5]  1 1 2 3 5 8
  • fibonacci 函数接受一个参数 n,表示要计算第 n 个斐波那契数。
  • 初始化一个数组 dp,用于存储斐波那契数列的值。
  • 初始条件设置 dp[0]dp[1] 均为 1,作为数列的起始。
  • 通过循环计算每个斐波那契数,根据动态规划的转移方程 dp[i] = dp[i-1] + dp[i-2]
  • 最后返回第 n 个斐波那契数。

2.爬楼梯问题

1. 爬楼梯问题定义

爬楼梯问题是一个经典的动态规划问题,其目标是计算爬到第 n 级楼梯的方法数,每次可以爬 1 或 2 级楼梯。动态规划的转移方程为 dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。

2.动态规划方程

dp[i] = dp[i-1] + dp[i-2],其中 dp[i] 表示爬到第 i 级楼梯的方法数。

// 定义一个函数,接受楼梯的级数作为参数
function climbStairs(n) {
    // 初始化一个数组dp,用于存储到达每一级楼梯的方法数
    const dp = [];

    // 初始条件:爬到第0级和第1级楼梯的方法数都为1
    dp[0] = dp[1] = 1;

    // 通过动态规划的转移方程计算每一级楼梯的方法数
    for (let i = 2; i <= n; i++) {
        dp[i] = dp[i - 1] + dp[i - 2];
    }

    // 返回爬到第n级楼梯的方法数
    return dp[n];
}

// 测试例子
const n = 4;
const result = climbStairs(n);
console.log(`爬到第${n}级楼梯的方法数为:${result}`);  
//爬到第4级楼梯的方法数为:5

当 n=4 时,输出结果是 5,表示爬到第 4 级楼梯有 5 种不同的方式。这包括:
1+ 1+ 1+ 12+ 1+ 11+ 2+ 11+ 1+ 22+ 2
  • climbStairs 函数接受一个参数 n,表示楼梯的级数。
  • 初始化一个数组 dp,用于存储到达每一级楼梯的方法数。
  • 初始条件设置 dp[0]dp[1] 均为 1,作为楼梯的起始条件。
  • 通过循环计算每一级楼梯的方法数,根据动态规划的转移方程 dp[i] = dp[i-1] + dp[i-2]
  • 最后返回爬到第 n 级楼梯的方法数。

3.背包问题

1. 背包问题定义

背包问题是一个经典的动态规划问题,目标是在给定背包容量下,选择一定数量的物品放入背包,使得放入背包的物品总价值最大。这里采用的是 0/1 背包问题,即每个物品只能选择放入一次或不放入。

2. 动态规划方程

动态规划的转移方程为 dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]),其中 dp[i][j] 表示在前 i 个物品中选择总重量不超过 j 的最大价值,weight[i] 表示第 i 个物品的重量,value[i] 表示第 i 个物品的价值。

// 定义一个函数,接受物品的重量数组和价值数组以及背包的容量作为参数
function knapsackProblem(weights, values, capacity) {
    const n = weights.length; // 物品的数量

    // 初始化一个二维数组dp,表示在前i个物品中选择总重量不超过j的最大价值
    const dp = new Array(n + 1).fill(0).map(() => new Array(capacity + 1).fill(0));

    // 动态规划,计算每个子问题的最优解
    for (let i = 1; i <= n; i++) {
        for (let j = 1; j <= capacity; j++) {
            // 当前物品的重量
            const currentWeight = weights[i - 1];
            // 当前物品的价值
            const currentValue = values[i - 1];

            // 如果当前物品的重量小于等于背包的容量
            if (currentWeight <= j) {
                // 选择当前物品或不选择当前物品,取较大的价值
                dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - currentWeight] + currentValue);
            } else {
                // 如果当前物品的重量大于背包的容量,则不能选择当前物品
                dp[i][j] = dp[i - 1][j];
            }
        }
    }

    // 返回在前n个物品中选择总重量不超过capacity的最大价值
    return dp[n][capacity];
}

// 测试例子
const weights = [2, 1, 3];
const values = [4, 2, 3];
const capacity = 4;
const result = knapsackProblem(weights, values, capacity);
console.log(`在给定容量为${capacity}的背包中,能够获得的最大价值为:${result}`);  
//在给定容量为4的背包中,能够获得的最大价值为:6
  • knapsackProblem 函数接受三个参数:物品的重量数组 weights、价值数组 values,以及背包的容量 capacity
  • 初始化一个二维数组 dp,用于存储在前 i 个物品中选择总重量不超过 j 的最大价值。
  • 通过嵌套循环计算每个子问题的最优解,根据动态规划的转移方程更新 dp 数组。
  • 最后返回在前 n 个物品中选择总重量不超过 capacity 的最大价值。

4.最长递增子序列

1. 最长递增子序列定义

最长递增子序列是指在一个数组中找到一个子序列,使得这个子序列中的元素是按照升序排列的,并且在所有符合条件的子序列中它是最长的。

2. 动态规划方程

动态规划的转移方程为 dp[i] = max(dp[j] + 1, dp[i]),其中 dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度,j0i-1 的索引,且 nums[i] > nums[j]

// 定义一个函数,接受数组作为参数
function longestIncreasingSubsequence(nums) {
    const n = nums.length; // 数组的长度

    // 初始化一个数组dp,表示以第i个元素结尾的最长递增子序列的长度
    const dp = new Array(n).fill(1);

    // 动态规划,计算每个子问题的最优解
    for (let i = 1; i < n; i++) {
        for (let j = 0; j < i; j++) {
            // 如果当前元素大于前面某个元素,则更新最长递增子序列的长度
            if (nums[i] > nums[j]) {
                dp[i] = Math.max(dp[j] + 1, dp[i]);
            }
        }
    }

    // 返回最长递增子序列的最大长度
    return Math.max(...dp);
}

// 测试例子
const nums = [10, 22, 9, 33, 21, 50, 41, 60, 80];
const result = longestIncreasingSubsequence(nums);
console.log(`最长递增子序列的长度为:${result}`);  
//最长递增子序列的长度为:6
  • longestIncreasingSubsequence 函数接受一个数组 nums 作为参数。
  • 初始化一个数组 dp,用于存储以每个元素结尾的最长递增子序列的长度,初始值为1。
  • 动态规划通过两层循环计算每个子问题的最优解。
  • 如果当前元素 nums[i] 大于前面某个元素 nums[j],则更新最长递增子序列的长度 dp[i] 为当前的最大值。
  • 最后返回最长递增子序列的最大长度,使用 Math.max(...dp) 获取数组中的最大值。

5.最大子数组和

1. 最大子数组和定义

最大子数组和问题是一个经典的动态规划问题,其目标是找到一个具有最大和的连续子数组。

2. 动态规划方程

动态规划的转移方程为 dp[i] = max(nums[i], nums[i] + dp[i-1]),其中 dp[i] 表示以第 i 个元素结尾的最大子数组和。

// 定义一个函数,接受数组作为参数
function maxSubArray(nums) {
    const n = nums.length; // 数组的长度

    // 初始化一个数组dp,表示以第i个元素结尾的最大子数组和
    const dp = new Array(n);

    // 初始条件:第0个元素结尾的最大子数组和即为第0个元素本身
    dp[0] = nums[0];

    // 动态规划,计算每个子问题的最优解
    for (let i = 1; i < n; i++) {
        // 当前元素单独构成子数组,或者与前面的子数组连接
        dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
    }

    // 返回以每个元素结尾的最大子数组和中的最大值
    return Math.max(...dp);
}

// 测试例子
const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
const result = maxSubArray(nums);
console.log(`最大子数组和为:${result}`);  
//最大子数组和为:6
  • maxSubArray 函数接受一个数组 nums 作为参数。
  • 初始化一个数组 dp,用于存储以每个元素结尾的最大子数组和。
  • 初始条件设置 dp[0] 为第0个元素本身。
  • 动态规划通过循环计算每个子问题的最优解,即以当前元素结尾的最大子数组和。
  • 转移方程 dp[i] = max(nums[i], nums[i] + dp[i-1]) 表示当前元素单独构成子数组,或者与前面的子数组连接,取两者中的较大值。
  • 最后返回以每个元素结尾的最大子数组和中的最大值,使用 Math.max(...dp) 获取数组中的最大值。

6.最长公共子序列

1. 最长公共子序列定义

最长公共子序列是指在两个字符串中找到一个最长的子序列,使得它在两个字符串中都出现,但不要求连续。

2. 动态规划方程

动态规划的转移方程为:

  • 如果 str1[i] 等于 str2[j],则 dp[i][j] = dp[i-1][j-1] + 1
  • 否则,dp[i][j] = max(dp[i-1][j], dp[i][j-1]),其中 dp[i][j] 表示 str1 的前 i 个字符和 str2 的前 j 个字符的最长公共子序列的长度。
// 定义一个函数,接受两个字符串作为参数
function longestCommonSubsequence(str1, str2) {
    const m = str1.length; // 字符串1的长度
    const n = str2.length; // 字符串2的长度

    // 初始化一个二维数组dp,表示str1的前i个字符和str2的前j个字符的最长公共子序列的长度
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));

    // 动态规划,计算每个子问题的最优解
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            // 如果当前字符相等,则取左上方的值加1
            if (str1[i - 1] === str2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1] + 1;
            } else {
                // 如果当前字符不相等,则取左方和上方的最大值
                dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
            }
        }
    }

    // 返回str1的前m个字符和str2的前n个字符的最长公共子序列的长度
    return dp[m][n];
}

// 测试例子
const str1 = "abcde";
const str2 = "ace";
const result = longestCommonSubsequence(str1, str2);
console.log(`最长公共子序列的长度为:${result}`); 
//最长公共子序列的长度为:3
  • longestCommonSubsequence 函数接受两个字符串 str1str2 作为参数。
  • 初始化一个二维数组 dp,用于存储str1的前i个字符和str2的前j个字符的最长公共子序列的长度。
  • 动态规划通过两层循环计算每个子问题的最优解。
  • 转移方程中,如果当前字符相等,则取左上方的值加1;如果当前字符不相等,则取左方和上方的最大值。
  • 最后返回 str1 的前 m 个字符和 str2 的前 n 个字符的最长公共子序列的长度,即 dp[m][n]

7.编辑距离

1. 编辑距离定义

编辑距离是衡量两个字符串相似程度的一种指标,表示将一个字符串转换成另一个字符串所需的最少操作次数。典型的操作包括插入一个字符、删除一个字符、替换一个字符。

2. 动态规划方程

动态规划的转移方程为:

  • 如果 word1[i] 等于 word2[j],则 dp[i][j] = dp[i-1][j-1]
  • 否则,dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1,其中 dp[i][j] 表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。
// 定义一个函数,接受两个单词作为参数
function minDistance(word1, word2) {
    const m = word1.length; // 单词1的长度
    const n = word2.length; // 单词2的长度

    // 初始化一个二维数组dp,表示将word1的前i个字符转换为word2的前j个字符所需的最少操作次数
    const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));

    // 初始化边界条件
    for (let i = 0; i <= m; i++) {
        dp[i][0] = i;
    }

    for (let j = 0; j <= n; j++) {
        dp[0][j] = j;
    }

    // 动态规划,计算每个子问题的最优解
    for (let i = 1; i <= m; i++) {
        for (let j = 1; j <= n; j++) {
            // 如果当前字符相等,则取左上方的值
            if (word1[i - 1] === word2[j - 1]) {
                dp[i][j] = dp[i - 1][j - 1];
            } else {
                // 如果当前字符不相等,则取左方、上方和左上方的最小值加1
                dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
            }
        }
    }

    // 返回将word1的前m个字符转换为word2的前n个字符所需的最少操作次数
    return dp[m][n];
}

// 测试例子
const word1 = "horse";
const word2 = "ros";
const result = minDistance(word1, word2);
console.log(`将"${word1}"转换为"${word2}"所需的最少操作次数为:${result}`); 
//将"horse"转换为"ros"所需的最少操作次数为:3
  • minDistance 函数接受两个单词 word1word2 作为参数。
  • 初始化一个二维数组 dp,用于表示将 word1 的前 i 个字符转换为 word2 的前 j 个字符所需的最少操作次数。
  • 初始化边界条件,即当一个字符串为空时,转换所需的操作次数为字符串的长度。
  • 动态规划通过两层循环计算每个子问题的最优解。
  • 转移方程中,如果当前字符相等,则取左上方的值;如果当前字符不相等,则取左方、上方和左上方的最小值加1。
  • 最后返回将 word1 的前 m 个字符转换为 word2 的前 n 个字符所需的最少操作次数,即 dp[m][n]

8.打家劫舍

1. 打家劫舍问题定义

打家劫舍问题是一个典型的动态规划问题,目标是在一排房屋中选择一些房屋进行偷窃,但不能偷相邻的房屋,求能够获得的最大金额。

2. 动态规划方程

动态规划的转移方程为 dp[i] = max(dp[i-1], dp[i-2] + nums[i]),其中 dp[i] 表示前 i 个房屋能够获得的最大金额,nums[i] 表示第 i 个房屋中的金额。

// 定义一个函数,接受一个包含每个房屋金额的数组作为参数
function rob(nums) {
    const n = nums.length; // 房屋的数量

    if (n === 0) {
        return 0; // 没有房屋,则返回0
    } else if (n === 1) {
        return nums[0]; // 只有一个房屋,则返回该房屋的金额
    }

    // 初始化一个数组dp,表示前i个房屋能够获得的最大金额
    const dp = new Array(n);

    // 前两个房屋的最大金额为较大的那个
    dp[0] = nums[0];
    dp[1] = Math.max(nums[0], nums[1]);

    // 动态规划,计算每个子问题的最优解
    for (let i = 2; i < n; i++) {
        // 在偷第i个房屋和不偷第i个房屋之间取较大值
        dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
    }

    // 返回前n个房屋能够获得的最大金额
    return dp[n - 1];
}

// 测试例子
const nums = [2, 7, 9, 3, 1];
const result = rob(nums);
console.log(`在给定房屋中能够获得的最大金额为:${result}`);
//在给定房屋中能够获得的最大金额为:12
  • rob 函数接受一个包含每个房屋金额的数组 nums 作为参数。
  • 根据房屋的数量 n 分情况处理:如果没有房屋,则返回0;如果只有一个房屋,则返回该房屋的金额。
  • 初始化一个数组 dp,表示前 i 个房屋能够获得的最大金额。
  • 前两个房屋的最大金额为较大的那个,即 dp[0] = nums[0]dp[1] = Math.max(nums[0], nums[1])
  • 动态规划通过循环计算每个子问题的最优解,转移方程为 dp[i] = max(dp[i-1], dp[i-2] + nums[i]),表示在偷第 i 个房屋和不偷第 i 个房屋之间取较大值。
  • 最后返回前 n 个房屋能够获得的最大金额,即 dp[n - 1]

9.最大正方形

1. 最大正方形定义

最大正方形是指在一个二维矩阵中,由’1’组成的最大正方形的边长。

2. 动态规划方程

动态规划的转移方程为:

  • 如果 matrix[i][j] 等于 1,则 dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
  • 否则,dp[i][j] = 0,其中 dp[i][j] 表示以 matrix[i][j] 为右下角的最大正方形的边长。
// 定义一个函数,接受一个包含0和1的二维矩阵作为参数
function maximalSquare(matrix) {
    const m = matrix.length; // 矩阵的行数
    const n = matrix[0].length; // 矩阵的列数

    // 初始化一个二维数组dp,表示以matrix[i][j]为右下角的最大正方形的边长
    const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));

    // 初始化结果,表示最大正方形的边长
    let maxSquare = 0;

    // 动态规划,计算每个子问题的最优解
    for (let i = 0; i < m; i++) {
        for (let j = 0; j < n; j++) {
            // 如果当前元素为1,则更新dp[i][j]为左、上、左上三个方向的最小值加1
            if (matrix[i][j] === '1') {
                dp[i][j] = Math.min(
                    i > 0 ? dp[i - 1][j] : 0,
                    j > 0 ? dp[i][j - 1] : 0,
                    i > 0 && j > 0 ? dp[i - 1][j - 1] : 0
                ) + 1;

                // 更新最大正方形的边长
                maxSquare = Math.max(maxSquare, dp[i][j]);
            }
        }
    }

    // 返回最大正方形的边长的平方
    return maxSquare * maxSquare;
}

// 测试例子
const matrix = [
    ['1', '0', '1', '0', '0'],
    ['1', '0', '1', '1', '1'],
    ['1', '1', '1', '1', '1'],
    ['1', '0', '0', '1', '0']
];
const result = maximalSquare(matrix);
console.log(`最大正方形的面积为:${result}`);   
//最大正方形的面积为:4
  • maximalSquare 函数接受一个包含0和1的二维矩阵 matrix 作为参数。
  • 初始化一个二维数组 dp,用于存储以 matrix[i][j] 为右下角的最大正方形的边长。
  • 初始化结果 maxSquare,表示最大正方形的边长。
  • 动态规划通过两层循环计算每个子问题的最优解。
  • 转移方程中,如果当前元素为1,则更新 dp[i][j] 为左、上、左上三个方向的最小值加1;否则, dp[i][j] 为0。
  • 在动态规划过程中,不断更新最大正方形的边长。
  • 最后返回最大正方形的边长的平方,即 maxSquare * maxSquare
<think> 我们正在讨论动态规划状态转移方程的推导方法。动态规划的核心在于定义状态和状态转移方程。根据之前的引用,动态规划常用于解决具有最优子结构和重叠子问题的问题。推导状态转移方程通常包含以下步骤: 1. **定义状态**:明确问题的状态,通常用数组(一维、二维等)表示。状态应包含问题的子问题解。 2. **确定状态转移**:找出状态之间的关系,即如何从已知状态推导出未知状态。 3. **初始化**:确定初始状态的值。 4. **确定边界条件**:处理特殊情况。 5. **确定输出**:最终要得到的解对应哪个状态。 结合引用[1]中的例子: $$ dp_{i,0} = \max(dp_{i-1,0}, dp_{i-1,1} + w_i) $$ $$ dp_{i,1} = \max(dp_{i-1,0} - w_i, dp_{i-1,1}) $$ 这个例子中,状态被定义为$dp_{i,0}$和$dp_{i,1}$,分别表示在第i个时刻处于状态0或1时的最优值。状态转移方程则描述了如何从i-1时刻的状态推出i时刻的状态。 ### 快速掌握状态转移方程推导的方法 #### 步骤1: 明确状态定义 - 状态通常表示某个子问题的解。例如: - 在背包问题中,$dp[i][j]$表示前i个物品放入容量为j的背包的最大价值。 - 在最长公共子序列(LCS)问题中,$dp[i][j]$表示字符串A的前i个字符和字符串B的前j个字符的最长公共子序列长度。 #### 步骤2: 寻找状态转移关系 - **分析子问题之间的关系**:考虑当前状态如何由之前的状态推导得到。通常需要枚举最后一步的选择。 - 例如,在背包问题中,对于第i个物品,有两种选择:放入或不放入。状态转移方程为: $$ dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w_i] + v_i) $$ - 在LCS问题中: - 如果$A[i] == B[j]$,则$dp[i][j] = dp[i-1][j-1] + 1$ - 否则,$dp[i][j] = \max(dp[i-1][j], dp[i][j-1])$ #### 步骤3: 初始化和边界条件 - 初始化:通常$dp[0][0]$或第一行/列需要初始赋值。 - 边界条件:注意数组索引的边界情况,避免越界。 #### 步骤4: 确定输出 - 最终结果通常存储在$dp$数组的某个位置,例如$dp[n][m]$。 #### 技巧总结 1. **画状态转移图**:将状态视为节点,状态转移视为边,形成有向图(引用[1]中提到用带权有向图描述状态转移)。 2. **分类讨论**:考虑当前步骤的所有可能选择,分别计算并取最优。 3. **从简单情况入手**:先考虑小规模问题,再推广到一般情况。 4. **使用记忆化搜索**:如果直接思考递推关系困难,可以先用递归+记忆化方式编写,再转化为递推。 ### 示例:剪绳子问题(引用[3]) 问题:长度为$n$的绳子剪成$m$段($m>1$),求各段长度乘积的最大值。 **状态定义**:$dp[i]$表示长度为$i$的绳子剪断后的最大乘积。 **状态转移**: - 对于长度$i$,我们可以将其剪成两段:$j$和$i-j$($1 \leq j < i$)。 - 那么,$dp[i] = \max_{j}\{ j \times (i-j), j \times dp[i-j] \}$ - 注意:这里$j$段可以不剪(即$j$和$i-j$两段),也可以继续剪(即$j$段保留,$i-j$段递归剪)。 **初始化**:$dp[1]=1$(因为至少剪一刀,所以实际计算中$dp[1]$不会被用到,但$dp[2]=1$)。 最终结果:$dp[n]$。 ### 实战训练建议 1. **经典模型**:熟练掌握背包问题、最长上升子序列、编辑距离等经典模型的状态转移方程。 2. **多做题**:多做动态规划题目,总结状态定义和转移方程的特点[^2]。 3. **注意优化**:有时状态转移方程需要优化(如单调队列、斜率优化),但初赛阶段通常考察基础。 §§相关问题§§ 1. 动态规划中如何避免重复计算子问题? 2. 背包问题有哪些常见变种及其状态转移方程? 3. 如何判断一个问题是否可以使用动态规划解决? </think> ### 快速掌握动态规划状态转移方程推导方法 动态规划的核心在于**定义状态**和**建立状态转移关系**。以下是系统化的推导方法(结合真题示例): --- #### 一、四大核心推导步骤 1. **状态定义** 明确`dp[i][j]`的物理含义,通常表示: - 子序列长度(如LCS问题) - 选择决策状态(如背包问题) - 时空位置(如网格路径问题) *示例:* 引用[1]中定义$dp_{i,0}$和$dp_{i,1}$表示第$i$步两种决策状态[^1] 2. **分解子问题** 将原问题拆解为规模更小的子问题: $$ \text{原问题} = \text{子问题} \oplus \text{当前决策} $$ 常用拆分维度: - 序列长度($dp[i]$依赖$dp[i-1]$) - 选择范围(背包容量) - 状态转移(如状态机模型) 3. **建立转移方程** 基于决策选择建立数学关系: $$ dp[i] = \mathop{\text{opt}}\limits_{\text{决策}}\left\{ f(dp[k]), \text{代价} \right\} \quad (k < i) $$ *关键技巧:* - 最值选择:$\max$/$\min$(如引用[1]中的$\max$操作[^1]) - 累加关系:$dp[i] = \sum dp[j]$(如计数问题) - 状态转移图:用带权有向边表示决策路径[^1] 4. **边界与优化** - 边界:初始化$dp[0]$、$dp[1]$等基础状态 - 优化:空间压缩(滚动数组)、剪枝(冗余状态剔除) --- #### 二、经典模型转移方程模板 | 问题类型 | 状态定义 | 转移方程 | 真题示例 | |-------------------|-----------------------|--------------------------------------------------------------------------|---------------------------| | **01背包** | $dp[i][j]$: 前$i$件物品容量$j$的最大价值 | $dp[i][j] = \max(dp[i-1][j], dp[i-1][j-w_i] + v_i)$ | 资源分配问题 | | **最长上升子序列**| $dp[i]$: 以$a_i$结尾的LIS长度 | $dp[i] = \max_{j<i \land a_j<a_i}(dp[j]) + 1$ | 序列极值问题 | | **状态机DP** | $dp[i][0/1]$: 第$i$步的状态值 | $dp_{i,0} = \max(dp_{i-1,0}, dp_{i-1,1} + w_i)$<br>$dp_{i,1} = \max(dp_{i-1,0}-w_i, dp_{i-1,1})$ [^1] | 引用[1]的加权决策模型 | | **区间DP** | $dp[l][r]$: 区间$[l,r]$的最优解 | $dp[l][r] = \min_{k=l}^{r-1}(dp[l][k] + dp[k+1][r] + cost)$ | 矩阵链乘法/石子合并 | --- #### 三、实战推导技巧 1. **逆向思维法** 从终点状态反向推导(如Dijkstra算法的DP思想): ```python # 最短路径转移示例 dp[end] = 0 for node in reverse_topological_order: dp[node] = min(dp[neighbor] + edge_weight for neighbor in neighbors) ``` 2. **决策分类法** 枚举当前步骤所有选择(如背包问题选/不选): $$ dp[i][j] = \begin{cases} \text{选择第}i\text{项的方案} \\ \text{不选第}i\text{项的方案} \end{cases} $$ 3. **数学归纳法** 验证转移方程的正确性: - $n=1$时成立(边界验证) - 假设$n=k$时成立 ⇒ 证明$n=k+1$时成立 4. **降维优化** 当$dp[i]$仅依赖$dp[i-1]$时压缩空间: ```cpp // 斐波那契数列空间优化 int dp0 = 0, dp1 = 1; for (int i = 2; i <= n; i++) { int cur = dp0 + dp1; dp0 = dp1; dp1 = cur; } ``` --- #### 四、真题训练要点 1. **识别DP特征** - 问题含"最大/最小"最优解要求 - 决策过程可分阶段(如序列问题) - 子问题重叠(如递归树有重复计算) 2. **规避常见错误** - 状态定义不全(漏关键维度) - 转移条件遗漏(如未处理边界) - 复杂度估算错误(需用$O(n^2)$但设计成$O(2^n)$) 3. **特殊题型处理** - 环形问题:破环成链(复制数组) - 高维DP:状态压缩(位运算) - 数学结合:如引用[3]的取余优化[^3] ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

狐说狐有理

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值