动态规划入门

洛谷上原文:动态规划

动态规划(简称“DP”)是 1957 年 理查德·贝尔曼(Richard Bellman)在 Dynamic Programming 一书中提出的一种表格处理方法。它将原问题分解为若干子问题,自底向上先求解最小的子问题,并把结果存储在表格中,在求解大的子问题时直接从表格中查询小的子问题的解,以避免重复计算,从而提高效率。

具备的要素

动态规划算法常用来求解最优化问题,尤其是带有多步决策的最优化问题。

能用动态规划解决的问题具备以下 3 个要素:

  • 最优子结构: 如果问题的最优解所包含的子问题的解也是最优的,就称该问题具有最优子结构。也就是说一个问题的最优解只取决于其子问题的最优解。

  • 无后效性: 将原问题分解为若干子问题,每个子问题的求解过程作为一个阶段,当前阶段的求解只与之前阶段有关,与之后阶段无关。即某阶段的状态一旦确定,就不受这个状态后续决策的影响。

  • 重叠子问题: 求解过程中每次产生的子问题并不总是新问题,会有大量子问题重复。在遇到重复的子问题时,只需在表格中查询,无需再次求解。这个性质不是使用动态规划解决问题的必要条件,但凸显了动态规划的一大优势。

解题步骤

解DP题目的一般模式如下:

  • 划分阶段: 按照问题特征,将问题划分为若干阶段。注意每个阶段是无后效性的。

  • 状态表示: 将问题发展到各个阶段时所处于的各种情况用不同的阶段进行表示。比如:dp[i]代表了……

  • 决策与动态转移方程: 在对问题的处理中做出的每种选择性的行动成为“决策”。而根据上一阶段的状态和决策来导出本阶段的状态就是状态转移。

  • 边界条件: 需要一个递推的终止条件或边界条件。

  • 答案:问题的求解目标。

例子实操

01背包问题

题目地址--P1048 [NOIP 2005 普及组] 采药

题目摘自洛谷 P1048。

1.题目大意

山上有 M 株草药,辰辰有 T 的时间去采药。

每株草药有 Wi 的价值,但采某株草药就会花费 Vi 的时间。

求在规定时间内,最多可以采到的草药最大价值。

2.状态表示(初步)

我们先将题目中的值进行表示。

题目中提到两个重要的值:即 V 和 W 。我们该如何对其进行表示?首先我们先定义一个二维数组来存储状态(即上文中的“表格”)。

设数组 dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值。

由此,显而易见地,最终答案为 dp[M][T] ,也就是“考虑前 M 株草药,时间不超过 T ,能获得的最大价值”。

3.决策与动态转移方程

对于数组 dp ,我们不难发现有两种选择方式:

  • 选择第 i 个物品;

  • 不选第 i 个物品。

其中,“不选第 i 个物品”时,用 dp 数组的第一维表示为 dp[i−1][j] 。也就是根据上一个状态(未选择第 i 个物品)决定,这样的情况便能形成最优解。

先对 dp 数组进行初始化。根据上文定义 (dp[i][j] 为,考虑前 i 株草药,时间不超过 j 时能获得的最大价值),我们就可以把 dp[0][j] 和 dp[i][0] 初始为0,接着进行状态转移。

已知“不选第 i 个物品”的状态转移方程为dp[i][j]=dp[i-1][j],不妨推出“选第 i 个物品”的转移方程:

max(dp[i-1][j],dp[i-1][j-v[i]+w[i])

直接枚举 i 和 j 即可。

4.参考代码

代码仅供参考,AC记录

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int T, M;
    cin >> T >> M;
    
    int v[105], w[105];
    for (int i = 1; i <= M; ++i) {
        cin >> v[i] >> w[i];
    }
    
    int dp[105][1005] = {0};
    
    for (int i = 1; i <= M; ++i) {
        for (int j = 0; j <= T; ++j) {
            if (j >= v[i]) {
                dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]);
            } else {
                dp[i][j] = dp[i-1][j];
            }
        }
    }
    
    cout << dp[M][T] << endl;
    return 0;
}

5.考虑优化

考虑将二维的 dp 数组优化成一维。

由于 dp[i][j] 只依赖于 dp[i−1][...] ,我们不妨把第一维优化掉。

j的遍历顺序要是反向,以避免内容的覆盖。

6.优化后代码

优化后AC记录

#include <iostream>
#include <algorithm>
using namespace std;

int main() {
    int T, M;
    cin >> T >> M;
    
    int v[105], w[105];
    for (int i = 1; i <= M; ++i) {
        cin >> v[i] >> w[i];
    }
    
    int dp[1005] = {0};
    
    for (int i = 1; i <= M; ++i) {
        for (int j = T; j >= v[i]; --j) { //逆序遍历避免覆盖
            dp[j] = max(dp[j], dp[j-v[i]] + w[i]);
        }
    }
    
    cout << dp[T] << endl;
    return 0;
}

最长上升子序列

题目地址--B3637 最长上升子序列

最长上升子序列(LIS)适用一维的动态规划来解决。

“最长上升子序列”是对于一个给定的长度为 n 的序列,求其单调递增的最长自序列的长度。

最长上升子序列是指,从原序列中按顺序取出一些数字排在一起,这些数字是逐渐增大的。

注:“子序列”可以是非连续的,但是顺序要与原序相同

1.状态设计

状态:dp[i] 表示以 a[i] 结尾的最长上升子序列的长度。

2.状态转移

dp[i]=1≤j<ia[j]<a[i]​max​(dp[j])+1

条件:a[j] < a[i](保证上升)。

操作:在所有满足条件的 j 中,找到最大的 dp[j],然后加 1(因为 a[i] 可以接在 a[j] 后面)。

边界:如果没有任何 j 满足 a[j] < a[i],则 dp[i] = 1a[i] 自身构成一个长度为 1 的上升子序列)。

3.参考核心代码

这里只展示核心代码。

dp[0]=0;
ans=0;
for(int i=1;i<n;i++) {
	dp[i]=1;
	for(int j=1;j<=i-1;j++) {
		if(a[j]<a[i])
			dp[i]=max(dp[i],dp[j]+1);
	}
	if(dp[i]>ans)
		ans=dp[i];
}

4.说明

说明:时间复杂度为 O(n2) ,遇到大的数据可能是会爆掉的。

你可以试着用 贪心、二分查找 等办法进行优化。由于本篇讲述的是 动态规划,所以不展示优化的代码。

或许你可以优化到 O(nlogn) 的级别。

推荐题目

不保证按照难度排序。

P1048 [NOIP 2005 普及组] 采药

B3637 最长上升子序列

P1216 [IOI 1994] 数字三角形 Number Triangles

P1002 [NOIP 2002 普及组] 过河卒

P1020 [NOIP 1999 提高组] 导弹拦截

P1091 [NOIP 2004 提高组] 合唱队形

P1880 [NOI1995] 石子合并

P1063 [NOIP 2006 提高组] 能量项链

P1060 [NOIP 2006 普及组] 开心的金明

P1064 [NOIP 2006 提高组] 金明的预算方案

P7074 [CSP-J2020] 方格取数

P8816 [CSP-J 2022] 上升点列

推荐学习资料

oi-wiki--《动态规划基础》

oi-wiki--《背包DP》

oi-wiki--《树形DP》

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值