动态规划入门:从斐波那契到背包问题

什么是动态规划

🎯 动态规划学习路径

1
入门

斐波那契
爬楼梯

2
基础

01背包
最长子序列

3
进阶

区间DP
树形DP

4
高级

状压DP
数位DP

动态规划(Dynamic Programming,简称DP)是信息学竞赛中最重要的算法之一。它通过把复杂问题分解成相对简单的子问题,并保存子问题的解来避免重复计算,从而高效地解决问题。

动态规划的本质

  • 最优子结构:问题的最优解包含子问题的最优解
  • 重叠子问题:子问题会被多次求解
  • 无后效性:未来的决策不会影响已经做出的决策

动态规划vs其他算法

对比项 动态规划 贪心算法 分治算法 暴力枚举
时间复杂度 多项式级 线性或对数 对数级 指数级
空间复杂度 需要额外空间 常数空间 递归栈空间 较小
适用问题 最优化问题 局部最优 独立子问题 规模小
正确性 保证最优 不一定最优 保证正确 保证正确

从斐波那契数列开始

问题描述

斐波那契数列:F(0)=0, F(1)=1, F(n)=F(n-1)+F(n-2) (n≥2)

三种实现方法对比

方法1:暴力递归


int fib(int n) {
    if (n <= 1) return n;
    return fib(n-1) + fib(n-2);
}
// 时间复杂度:O(2^n)
// 空间复杂度:O(n)
// 问题:大量重复计算

方法2:记忆化搜索


int memo[100];
bool vis[100];

int fib(int n) {
    if (n <= 1) return n;
    if (vis[n]) return memo[n];
    vis[n] = true;
    memo[n] = fib(n-1) + fib(n-2);
    return memo[n];
}
// 时间复杂度:O(n)
// 空间复杂度:O(n)
// 优点:避免重复计算

方法3:动态规划


int fib(int n) {
    if (n <= 1) return n;
    int dp[n+1];
    dp[0] = 0;
    dp[1] = 1;
    for (int i = 2; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}
// 时间复杂度:O(n)
// 空间复杂度:O(n)

方法4:空间优化


int fib(int n) {
    if (n <= 1) return n;
    int a = 0, b = 1, c;
    for (int i = 2; i <= n; i++) {
        c = a + b;
        a = b;
        b = c;
    }
    return b;
}
// 时间复杂度:O(n)
// 空间复杂度:O(1)

动态规划的解题步骤

标准四步法

  1. 定义状态
    • 确定dp数组的含义
    • 明确下标表示什么
    • 数组值表示什么
  2. 状态转移方程
    • 找出状态之间的关系
    • 写出递推公式
    • 确保无后效性
  3. 初始化
    • 确定边界条件
    • 设置初始值
    • 避免越界
  4. 计算顺序
    • 确定遍历方向
    • 保证依赖关系
    • 返回目标值

经典问题1:爬楼梯

题目描述

你正在爬楼梯,需要n阶才能到达楼顶。每次可以爬1或2个台阶,有多少种不同的方法可以爬到楼顶?

解题思路

  • 状态定义:dp[i]表示爬到第i阶的方法数
  • 转移方程:dp[i] = dp[i-1] + dp[i-2]
  • 初始化:dp[0]=1, dp[1]=1

代码实现


int climbStairs(int n) {
    if (n <= 2) return n;
    vector<int> dp(n + 1);
    dp[1] = 1;
    dp[2] = 2;
    for (int i = 3; i <= n; i++) {
        dp[i] = dp[i-1] + dp[i-2];
    }
    return dp[n];
}

变式问题

变式 描述 转移方程
一次可爬1-3阶 每次可以爬1、2或3阶 dp[i]=dp[i-1]+dp[i-2]+dp[i-3]
有代价爬楼梯 每阶有不同代价 dp[i]=min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])
不能连续爬2阶 限制条件 需要二维状态

经典问题2:最长上升子序列(LIS)

题目描述

给定数组nums,找到最长严格递增子序列的长度。

方法1:动态规划 O(n²)


int lengthOfLIS(vector<int>& nums) {
    int n = nums.size();
    vector<int> dp(n, 1);  // dp[i]表示以nums[i]结尾的LIS长度
    
    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (nums[j] < nums[i]) {
                dp[i] = max(dp[i], dp[j] + 1);
            }
        }
    }
    
    return *max_element(dp.begin(), dp.end());
}

方法2:二分优化 O(nlogn)


int lengthOfLIS(vector<int>& nums) {
    vector<int> tails;  // tails[i]表示长度为i+1的LIS的最小尾元素
    
    for (int num : nums) {
        auto it = lower_bound(tails.begin(), tails.end(), num);
        if (it == tails.end()) {
            tails.push_back(num);
        } else {
            *it = num;
        }
    }
    
    return tails.size();
}

经典问题3:0-1背包问题

问题描述

有n个物品,第i个物品重量为w[i],价值为v[i]。背包容量为W,每个物品只能选一次,求最大价值。

状态定义与转移

  • 状态:dp[i][j]表示前i个物品,容量为j时的最大价值
  • 转移:dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i])

二维DP实现


int knapsack01(int n, int W, vector<int>& w, vector<int>& v) {
    vector<vector<int>> dp(n+1, vector<int>(W+1, 0));
    
    for (int i = 1; i <= n; i++) {
        for (int j = 0; j <= W; j++) {
            dp[i][j] = dp[i-1][j];  // 不选第i个物品
            if (j >= w[i]) {
                dp[i][j] = max(dp[i][j], dp[i-1][j-w[i]] + v[i]);
            }
        }
    }
    
    return dp[n][W];
}

一维优化(滚动数组)


int knapsack01(int n, int W, vector<int>& w, vector<int>& v) {
    vector<int> dp(W+1, 0);
    
    for (int i = 1; i <= n; i++) {
        for (int j = W; j >= w[i]; j--) {  // 注意:逆序遍历
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    }
    
    return dp[W];
}

背包问题家族

各类背包问题对比

类型 特点 转移方程 遍历顺序
0-1背包 每个物品只能选一次 dp[j]=max(dp[j],dp[j-w]+v) 逆序
完全背包 每个物品可选无限次 dp[j]=max(dp[j],dp[j-w]+v) 正序
多重背包 每个物品有限个 需要拆分或二进制优化 逆序
分组背包 物品分组,每组选一个 三重循环 逆序

完全背包代码


int completeKnapsack(int n, int W, vector<int>& w, vector<int>& v) {
    vector<int> dp(W+1, 0);
    
    for (int i = 1; i <= n; i++) {
        for (int j = w[i]; j <= W; j++) {  // 正序遍历
            dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
        }
    }
    
    return dp[W];
}

区间DP

石子合并问题

n堆石子排成一排,每次只能合并相邻两堆,代价为两堆石子数量之和,求最小代价。


int stoneGame(vector<int>& stones) {
    int n = stones.size();
    vector<int> sum(n+1, 0);  // 前缀和
    for (int i = 0; i < n; i++) {
        sum[i+1] = sum[i] + stones[i];
    }
    
    vector<vector<int>> dp(n, vector<int>(n, 0));
    
    for (int len = 2; len <= n; len++) {  // 区间长度
        for (int i = 0; i <= n - len; i++) {
            int j = i + len - 1;
            dp[i][j] = INT_MAX;
            for (int k = i; k < j; k++) {
                dp[i][j] = min(dp[i][j], 
                              dp[i][k] + dp[k+1][j] + sum[j+1] - sum[i]);
            }
        }
    }
    
    return dp[0][n-1];
}

状态压缩DP

旅行商问题(TSP)

给定n个城市的距离矩阵,从0号城市出发,访问所有城市恰好一次后回到起点,求最短路径。


int tsp(vector<vector<int>>& dist) {
    int n = dist.size();
    int ALL = (1 << n) - 1;
    vector<vector<int>> dp(1 << n, vector<int>(n, INT_MAX/2));
    
    dp[1][0] = 0;  // 从0出发
    
    for (int mask = 1; mask <= ALL; mask++) {
        for (int i = 0; i < n; i++) {
            if (!(mask & (1 << i))) continue;
            for (int j = 0; j < n; j++) {
                if (i == j || !(mask & (1 << j))) continue;
                dp[mask][i] = min(dp[mask][i], 
                                  dp[mask^(1<<i)][j] + dist[j][i]);
            }
        }
    }
    
    int ans = INT_MAX;
    for (int i = 1; i < n; i++) {
        ans = min(ans, dp[ALL][i] + dist[i][0]);
    }
    
    return ans;
}

DP优化技巧

1. 滚动数组优化

当dp[i]只依赖dp[i-1]时,可以用两个数组交替使用:


vector<int> prev(m), curr(m);
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        curr[j] = /* 根据prev计算 */;
    }
    swap(prev, curr);
}

2. 斜率优化

适用于形如:dp[i] = min{dp[j] + cost(j, i)}的转移方程

3. 单调队列优化

当转移有区间限制时,维护单调队列降低复杂度

4. 四边形不等式优化

适用于区间DP的优化

学习建议与练习题推荐

学习路线

  1. 入门阶段
    • 斐波那契、爬楼梯
    • 简单线性DP
    • 练习20-30题
  2. 基础阶段
    • 背包问题全家族
    • LIS、LCS问题
    • 练习50题
  3. 进阶阶段
    • 区间DP
    • 树形DP
    • 状态压缩DP
  4. 高级阶段
    • 数位DP
    • 概率DP
    • DP优化技巧

经典练习题

难度 题目 知识点
简单 最大子数组和 线性DP
简单 打家劫舍 线性DP
中等 最长公共子序列 二维DP
中等 编辑距离 二维DP
中等 零钱兑换 完全背包
困难 正则表达式匹配 字符串DP
困难 戳气球 区间DP

总结

动态规划是信息学竞赛的核心算法,掌握好DP对提升竞赛水平至关重要。学习DP的关键点:

  1. 理解本质:最优子结构和重叠子问题
  2. 掌握方法:状态定义、转移方程、初始化、计算顺序
  3. 大量练习:从简单到复杂,循序渐进
  4. 总结归纳:形成自己的DP模板库
  5. 灵活应用:识别问题类型,选择合适方法

记住,DP不是背公式,而是一种思维方式。通过不断练习和思考,你一定能掌握这个强大的算法工具!

Views: 0

相关文章

答复

您的邮箱地址不会被公开。 必填项已用 * 标注