动态规划

旷日持久的战役。

动态规划原理

动态规划的原理包含最优子结构无后效性以及子问题重叠

下面是 AI 对动态规划原理举出的典型例子:

  1. 最优子结构
    一个问题的最优解包含其子问题的最优解。也就是说,我们可以通过组合子问题的最优解来构造原问题的最优解。这是动态规划能够“分而治之”的基础。

  2. 无后效性(未来不会影响过去)
    也称为“马尔可夫性质”。一旦某个阶段的状态确定,后续决策的演变就只与这个状态有关,而与之前如何到达这个状态的路径无关。这意味着未来的决策不会影响过去的状态。

  3. 子问题重叠
    在求解过程中,不同的决策序列可能会反复遇到相同的子问题。动态规划通过存储这些子问题的解(通常用数组或表格),使得每个子问题只计算一次,从而大幅提升效率。

:对子问题重叠的正确理解:不是将状态集合划分成可交集合,而是子问题可以用于后续的多次贡献计算。

例1:P1216 [IOI 1994 / USACO1.5] 数字三角形 Number Triangles

首先分析为什么一定要用动态规划。

  • 贪心是错误的,因为贪心的眼光是短浅的,只适配于局部最优解能推出全局最优解的情况。这个手玩样例就能发现。

这个题满足无后效性、子问题重叠与最优子结构。所以可以进行 dp。

  • dp 因素:行列。

\(dp[i][j]\) 表示考虑到第 \(i\)\(j\) 列的走过路径的最大权值和。

写出转移式:

\[dp[i][j]= \max \begin{cases} dp[i-1][j]+a[i][j] \\ dp[i-1][j-1]+a[i][j] & \text{if } j-1 \ge 0 \end{cases}\]

code:

#include<bits/stdc++.h>
#define ll long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
using namespace std;

const int N=1010;
int n;
int dp[N][N],a[N][N];

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    rep(i,1,n){
        rep(j,1,i)cin>>a[i][j];
    }
    dp[1][1]=a[1][1];
    rep(i,2,n){
        rep(j,1,i){
            if(j-1>=1){
                dp[i][j]=max(dp[i][j],dp[i-1][j-1]+a[i][j]);
            }
            dp[i][j]=max(dp[i][j],dp[i-1][j]+a[i][j]);
        }
    }
    int ans=0;
    rep(i,1,n){
        ans=max(ans,dp[n][i]);
    }
    cout<<ans<<'\n';
    return 0;
}

例2:\(O(n^2)\) 求解 LIS

LIS:最长上升子序列。

  • 首先,无后效性是好说明的。
  • 最优子结构也很好说明。我想得到 \([1,i]\) 的最长上升子序列,必定依赖某一段前缀 \([1,j]\)(有可能为空)加上末尾的一个数的最优解来更新。
  • 子问题重叠:考虑从一个 \(i\) 开始贡献给它后面的 \(j\)。它是能够多次进行贡献的。

所以考虑进行 dp。贪心是错的,因为我如果向后先找到一个最近的比当前数大的数,放到子序列末尾,很可能因为找到一个极大值而丢弃了后面更优的解。

  • dp 依赖的维度:序列的下标和权值数组。

\(dp[i]\) 表示强制以 \(a[i]\) 结尾的最长上升子序列的长度。

有转移式

\[dp[i]=(\max_{1 \le j < i \ \wedge a[j]<a[i]} dp[j] )+1 \]

例3:二分优化 LIS

此处有经典 trick:把答案作为 dp 存储的维度。

  • 额外记一个辅助数组 \(g\),其中 \(g[i]\) 表示长度为 \(i\) 的上升子序列末尾的可能最小值。可以看到,加粗部分就是要求的答案。

那么考察这个 \(g\) 的单调性,采取反证法。假设 \(g[i]>g[i+1]\),那么我只要把 \(i+1\) 子序列删去头部,即可使 \(g[i] = g[i+1]\),矛盾!所以说 \(g[i] \le g[i+1]\)。所以 \(g\) 单调不降。

  • 那么每次二分 \(g\),根据最优子结构(动态规划原理)可知对于当前 \(i\),我们需要找到一个 \(g[j]\) 满足 \(g[j] < a[i]\),此时令 \(dp[i] \leftarrow j+1\)。同时更新辅助数组 \(g\),即 \(g[dp[i]]= \min \ (g[dp[i]],a[i])\)

闫氏 DP 分析法

由 yxc 提出的,他用画图辅助 DP 的建模过程,收到网友的推崇。

详细阅读:https://www.cnblogs.com/IzayoiMiku/p/13635809.html

摘录两句名言:

\(\textcolor{blue}{所有的 DP 问题,本质上都是有限集中的优化问题--yxc}\)

\(\textcolor{red}{所有的 DP 优化,都是对代码的恒等变形--yxc}\)

要点总结

  • 确定状态集合。又称“化零为整”。
  • 列出 DP 属性:如 max/min/count/sum
  • 划分 DP 阶段:又称“化整为零”,即把状态集合划分为若干个子问题状态,此处用到了动态规划原理中的子问题重叠
  • 列 DP 转移方程:DP 的核心。
  • corner case(边界条件):要格外小心。

例4:\(O(n^2)\) 求最长公共子序列

最长公共子序列目前最优的解法是用 bitset,但这里仅介绍如何用 \(O(n^2)\) 求解最长公共子序列。

先来三步走。

  • 子问题重叠。没有问题。
  • 无后效性,可以对后面进行贡献,没有问题。
  • 最优子结构,也没有问题。

综上,可以 DP。


再用 trick 3 闫氏 DP 分析法。

  • 确定状态集合,\(dp[i][j]\) 表示 \(a[1...i]\)\(b[1...j]\) 最长公共子序列的集合。
  • 列出 DP 属性:Max。
  • 划分 DP 阶段:

  • 列状态转移方程:

\[dp[i][j]=\max \begin{cases} dp[i-1][j-1]+1 & \text{if} \ a[i]=b[j] \\ \max(dp[i-1][j],dp[i][j-1]) & \text{if} \ a[i] \neq b[j] \end{cases}\]

动态规划 trick

  1. 把答案作为 dp 维度。应用:二分优化 LIS。
  2. 反证法证明单调性。应用:二分优化 LIS。
  3. 闫氏 DP 分析法。这种方法使用门槛非常低,有画图就可以。

参考资源

posted @ 2026-04-03 19:26  lbh666  阅读(9)  评论(0)    收藏  举报