总结与归纳之DP

前言

对于 \(\text{DP}\) 这种算法或者思想,在 \(\text{OI}\) 界可以算是最为常见且实用的工具了。本人对于 \(\text{DP}\) 的理解在不断的打磨下,基本成形。由于不同的 \(\text{DP}\) 有不同的分析思路,下面进行归纳。

总论

总的有个大纲吧~

何时用

目前没有什么特征说明绝对只能用 \(\text{DP}\),但大多数时候,有以下特征:

  1. 求最值问题
  2. 求方案数问题
  3. 求可行性问题(ex:求概率或期望)
  4. 可从大问题(规模)分解为小的问题,小的问题可以推出大的问题。

最后一条是关键,如果不满足,则一定不是 \(\text{DP}\)

何从想

第一步便是要找到描述问题的状态,找的方法因题而异,之后讨论。

第二步便是找到转移方程,可理解为,状态 \(S=f(S_1,S_2,\dots,S_k)\)。这个 \(S_k\)\(S\) 分解的若干个小问题,多元函数 \(f\) 就是状态转移方程,也是 \(\text{DP}\) 的骨架。

第三步便是根据无后效性确定枚举顺序,无后效性的意思是:某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响,这决定了 \(\text{DP}\) 的正确性(这一步并不绝对,有时候可以用记忆化搜索避免这个问题,有时候可以用高斯消元)。

第四步便是分析边界情况,如果一个状态无法再拆分了,那么理论上讲可以快速求解,找到边界即可 \(\text{DP}\)

为何用

算法竞赛本质上是要求选手利用已有资源解决问题,已有资源便是时间和空间。所以优化问题,便是节约已有资源,因为我们知道几乎所有题都可以搜索实现,或者更无脑的暴力。\(\text{DP}\) 的巧妙之处在于能把相关的一切状态求出,解决最终的问题,每个状态遍历一次,因此时间复杂度 \(O(状态规模 \times 转移规模)\),但是搜索的效率普遍是 \(O(问题规模^{决策规模})\),指数增长是绝对慢与线性增长的,因此,\(\text{DP}\) 要优于暴力,优秀的算法不用才怪。

分讨类论(doge)

注意,\(\text{OI}\) 并不是只要归类就能打遍天下无敌手,题目分析能力往往比归纳重要的多,这便是经验的意义。

线性结构 \(\text{DP}\)

并非状态转移方程线性,而是问题结构线性,序列问题和矩阵问题等,这里讨论的是这类 \(\text{dp}\) 的基础方法。

题目特征:

事实上,只要是序列问题和矩阵问题都可以思考一下。

状态:

原则1:问什么设什么(题目有啥状态就设啥状态)。
原则2:状态规模在可处理范围内(不绝对,参考矩阵快速幂)
原则3:决策和状态绝对相关。即决策影响状态的改变,变了啥,状态里就有啥,除此之外的均为冗余状态。

目前没做过脱离三原则的线性结构 \(\text{DP}\)

转移方程:

基础模型如下:

\[f(g(dp_{j_k},w(j_k,i))) \to dp_i \]

解释,\(g\) 是关于 \(dp_{j}\)\(w(j,i)\) 的一个式子,也可以只跟 \(dp_{j}\) 有关。\(f\) 是对这些 \(g\) 值的合并,可以是求和,求积,求极值,求异或等等。\(j\) 是可以通过一种决策,转移到 \(i\) 的决策集合,\(w(j,i)\) 是决策的代价。

目前,所有(几乎吧~) \(\text{DP}\) 都是这个式子的变形或者加维,不过线性结构 \(\text{DP}\) 更为明显。

区间 \(\text{DP}\)

线性结构 \(dp\) 的另一种思考方式,将区间的左右端点作为状态,区间作为阶段,把小区间的答案合并成大区间的答案。

题目特征:

区间合并问题,区间删除问题,以及有可能通过转换变成这两类的问题。

状态:

原则1:左端点坐标,右端点坐标,这是核心。
原则2:缺啥补啥。(有时候状态转移方程推不出来,就要加一些附加状态,一般是左右端点的附加属性)

目前没做过脱离双原则的区间 \(\text{DP}\)

转移方程:

基础模型如下:

\[f(g(dp_{i,k},dp_{k+1|2,j},w(i,j)))\to dp_{i,j}(i<=k<j) \]

不过多解释了,注意的是该式子有一个极其常见的变形,那就是通过 \(dp_{i+1,j-1},dp_{i,j-1},dp_{i+1,j}\) 合并得到 \(dp_{i,j}\)。这种转移方式一定要看是否满足题意,是否可以推出转移式。

树形 \(\text{DP}\)

当问题跑到树上时,或者可以分解成树的问题(基环树)。树形 \(dp\) 是显然的,而且它非常符合 \(dp\) 的思想,子树就对应了子问题。

题目特征

一种是直接给出树结构(或基环树),一种是告诉你状态之间的限制关系,通过数学推导得出是树结构(或基环树)。

状态

原则1:结点编号一定有。因为在访问子节点时,访问的是编号。
原则2:问什么设什么原则(常见的设计:该结点是否选择,以该节点为根的子树有几个点被选择)。

目前没做过脱离双原则的树形 \(\text{DP}\)

转移方程:

基础模型如下:

\[f(g(dp_{v_k},w(v_k,u))) \to dp_u \]

\(v_k\)\(u\) 的子节点。
一般来说,这个方程很好列,但是树形 \(\text{DP}\) 还能做到更多。

典型题型:树形背包
当状态有在子树里选若干个结点时,树形背包孕育而生,方程如下:

\[f(g(dp_{v_k,k},dp_{u,j-k},w(v_k,u))) \to dp_{u,j} \]

记住,\(j\) 要倒序枚举。
变形1:换根 \(\text{DP}\)
当根节点的变化会影响一些值,那么选一个根就很重要了,难道要枚举根?二次扫描法,又称换根 \(\text{DP}\),能很好的解决问题。
第一步:树形 \(\text{DP}\) 求出指定某个结点为根的 \(\text{DP}\) 值。\((v \to u)\)
第二步:第二次树形 \(\text{DP}\),求出使得 \(\text{DP}\) 值最大的根,方程一般通过换根后的变化列出。\((u \to fa)\)
变形2:基环树 \(\text{DP}\)
基环树也不是不可解决,多了一条边,就删吗,枚举环上的边,删掉求 \(\text{DP}\),这是破环。还有一种是统计环上结点为根的子树+环上情况,这是拆环。但因为我自己很少做这种题,所以暂且写到这。

状态压缩 \(\text{DP}\)

这种题套路的很,所谓状态压缩,是把一个大小为 \(N\) 的集合且集合内元素最大值为 \(K\) 看做一个 \(N\)\(K\) 进制数,将这个数作为状态,这样可以记录下来已有的状态。

数据范围暗示一切~。

状态:

原则1:数据范围定压缩对象。如果 \(N^K<=1e8\) 显然就压缩作为状态。
原则2:问什么设什么原则。
原则3:原则1不明显时,可以考虑决策会影响哪段数。

目前没做过脱离三原则的状压 \(\text{DP}\)

转移方程:

基础模型如下:

\[f(g(dp_{j},w(j,i)) \times check(i,j) \times pd(j)) \to dp_i \]

\(check\) 函数是判断 \(i\) 是否可以被 \(j\) 转移,\(pd\) 函数是判断状态原本是否合法,为啥我之前不单独写出来,之前的 \(\text{DP}\) 枚举点集很简单,基本上循环枚举就好了,所以就咕着不写。

注意 \(pd\) 函数可以预处理,不过预处理出合法关系没啥用,我们要预处理出合法点集,这是常见优化。

数位 \(\text{DP}\)

这更套路。一般来说,如果问题是 \([L,R]\) 中满足某种条件的数的个数(或其他,不过显然可以用前缀和思想求出 \([1,i]\) 的答案),或者第几大满足某种条件的数。你会说,这能 \(\text{DP}\)?还真能,但是只是用来辅助求解。

状态:

原则1:必须要有数位长度,当前考虑到第几位。
原则2:问什么设什么原则
假原则:学过的都知道可以用记忆化把代码写的漂亮一点,精髓在于在 \(\text{dfs}\) 里设置参数 \(\text{lim}\)\(\text{lead}\)。表示前几位是否到达上界,以及前导零。

目前没做过脱离假三原则的数位 \(\text{DP}\)

转移方程?

没错,是个问号!这玩意有个代码模板!

#define int long long
int a[N];
int dp[N][state];//不同题目状态不同
int dfs(int pos,/*state变量*/,bool lead/*前导零*/,bool limit/*数位上界变量*/) {//不是每个题都要判断前导零 
  //递归边界,既然是按位枚举,最低位是0,那么pos==-1说明这个数我枚举完了
  if(pos==-1) return 1;/*这里一般返回1,表示你枚举的这个数是合法的,那么这里就需要你在枚举时必须每一位都要满足题目条件,也就是说当前枚举到pos位,一定要保证前面已经枚举的数位是合法的。不过具体题目不同或者写法不同的话不一定要返回1 */
  //第二个就是记忆化(在此前可能不同题目还能有一些剪枝)
  if(!limit&&!lead&&dp[pos][state]!=-1) return dp[pos][state];
  /*常规写法都是在没有限制的条件记忆化,这里与下面记录状态是对应,具体为什么是有条件的记忆化后面会讲*/
  int up=limit?a[pos]:9;//根据limit判断枚举的上界up;这个的例子前面用213讲过了
  int ans=0;
  //开始计数
  for(int i=0;i<=up;i++) {//枚举,然后把不同情况的个数加到ans就可以了
    if() ...
    else if()...
    ans+=dfs(pos-1,/*状态转移*/,lead&&i==0,limit&&i==a[pos]) //最后两个变量传参都是这样写的
    /*这里还算比较灵活,不过做几个题就觉得这里也是套路了
    大概就是说,我当前数位枚举的数是i,然后根据题目的约束条件分类讨论
    去计算不同情况下的个数,还有要根据state变量来保证i的合法性,比如题目
    要求数位上不能有62连续出现,那么就是state就是要保存前一位pre,然后分类,
    前一位如果是6那么这意味就不能是2,这里一定要保存枚举的这个数是合法*/
  }
  //计算完,记录状态
  if(!limit&&!lead) dp[pos][state]=ans;
  /*这里对应上面的记忆化,在一定条件下时记录,保证一致性,当然如果约束条件不需要考虑lead,这里就是lead就完全不用考虑了*/
  return ans;
}
int solve(int x) {
  int pos=0;
  while(x) {//把数位都分解出来
    a[pos++]=x%10;//个人老是喜欢编号为[0,pos),看不惯的就按自己习惯来,反正注意数位边界就行
    x/=10;
  }
  return dfs(pos-1/*从最高位开始枚举*/,/*一系列状态 */,true,true);//刚开始最高位都是有限制并且有前导零的,显然比最高位还要高的一位视为0嘛
}
int L,R;
signed main() {
  memset(dp,-1,sizeof(dp));
  //求区间[L,R]内符合的个数
  while(~scanf("%lld%lld",&L,&R)) {
    printf("%lld\n",solve(R)-solve(L-1));
  }
}

不要质疑,转载大佬的,但是我一般都用这个。

概率or期望 \(\text{DP}\)

当问题编程求某事件发生的概率,或是某事的期望,并且具有一定规模,就可以来个 \(\text{DP}\)

状态?

没错,这又是个问号意味着,这和前几个 \(\text{DP}\) 在状态设计上大同小异。

转移方程?

问号来袭,大同小异。

特别之处!

为啥把它拎出来单独说呢?因为它们有些特别的处理方式。
关于概率 \(\text{DP}\)
\(\text{DP}\) 值不再大幅度改变,比如变化量已经小于 \(10^{-8}\),那么即可停止。
关于期望 \(\text{DP}\)
(补充:\(Expected-value=\sum\limits_{All-the-situation} Final—result_k \times Probability_k\))
显然,这是对未来的 \(\text{DP}\),所有是倒序,即从最终局面往回推,当前状态表示初始局面。
这有些特别,所以咱浅浅的谈一谈。


其实还有些特别的 \(\text{DP}\) 这里随便说说。

计数 \(\text{DP}\),这类 \(\text{DP}\) 有时候很好想状态,但是转移式推不出来,因为涉及到组合式和子问题独立啦,要不重不漏的统计,是组合数学最重要的思路,所以数学要够硬。还有些时候要二项式反演了,钦定的方式求解,其实也是组合数学,广义的容斥吗~,偏题了,希望我以后总结这个东西。

\(\text{DAG}\) 上的 \(\text{DP}\),有向无环图满足了无后效性,可以 \(\text{DP}\),具体题型基本上只会考察最长/短链,注意到题目可能会暗示二元关系,从而让你建出 \(\text{DAG}\) 模型解题,状态很好设计,就是结点之间通过二元关系转移。

其他,都是优化~

接下来要进入优化啦~

数据结构优化 \(\text{DP}\)

不全面,刷过的题少,所以就浅浅的谈一下。

前缀和优化

并非给定数组的前缀和优化计算,而是,上一轮 \(\text{DP}\) 数组的前缀和优化转移。

如果转移式可以写成:

\[dp_{i,j}=\sum\limits_{k=X(i,j)}^{Y(i,j)}{f(dp_{i-1,k},k)} \]

那么,在求解 \(dp_{i,j}\) 时候,可以看做是求序列 \({a_k=f(dp_{i-1,k},k)}\) 在某一个区间的和。显而易见,可以在第 \(i-1\) 轮时做一个前缀和,这样转移的复杂度就成 \(O(1)\) 的了。

前缀积也可以~。

\(\text{BIT}\) 优化

首先,二维偏序是 \(\text{BIT}\) 优化的核心。
平面上有若干个点,求对于每个点,有几个其他的点横坐标和纵坐标都小于这个点的横坐标和纵坐标。
方法:开一个 \(\text{BIT}\),将这些点的横坐标从小到大排序,遇到一个点,用树状数组统计 \((1 \dots a_y-1)\) 的和,然后用树状数组,在下标为 \(a_y\) 的位置加一。
因此我们可以总结出一般的可优化转移式。

\[dp_{i}=\sum\limits_{j<i}dp_j (w(j)<w(i)) \]

为啥可优?把 \((i,w(i))\) 看做平面上的点,然后加一变成加 \(dp_i\) 这就是个裸题啦。

当然,如果转移式中出现了小于当前下标的最大转移值,也可以用 \(\text{BIT}\) 求极值。(事实上,可以线段树~)

平衡树优化

线段树优化

线性 \(dp\) 的常见优化。回顾一下转移式:

\[f(g(dp_{j_k},w(j_k,i))) \to dp_i \]

在这个转移式中,\(w\) 函数有时候是很费时间的,但如果,每次求解 \(w\) 的过程中存在重复求解或者说存在更快的方式维护 \(w\) 的变化,那么大概率是线段树(如果操作简单的话,可以用 \(\text{BIT}\) 水过去)。具体方法是:

用线段树维护 \(a_i=g(dp_{j_k},w(j_k,i))\),有时候也要维护 \(a_i=dp_{j_k}\)

思考从 \(w(j,i)\)\(w(j,i+1)\) 的过程中,所有 \(j_k\) 会对 \(a\) 数组产生什么影响。

用线段树维护这一过程,支持上述的影响所需要的的操作,以及 \(f\) 的操作。这样的话复杂度可以将求解 \(w\) 的复杂度降为 \(\log\) 级别的。

单调队列优化 \(\text{DP}\)

“当一个选手比你小还比你强,你就打不过他了”是这个算法的核心。我们在遍历可以转移的决策集合中往往只会在意可以取得最优解的决策,那么是否存在在某一次转移后,永远不可能成为最优决策点的决策呢?没错,针对此,我们可以用单调队列优化。具体的,一般形式的可优化转移式如下(以最小值为例):

\[dp_{i}=\min_{j \in [l(i),r(i)]}{(dp_j+w(j))}+c(i) \]

其中 \(l(i),r(i)\) 单调不降。仔细看这个求极值的式子,是否很像滑动窗口呢!然后就很入门了。这样做的好处是均摊掉了找决策点的复杂度,砍掉了一个多项式量级。

拓展:单调队列优化多重背包。来个直观的:已知多重背包转移方程如下:

\[dp_{i,j}=\max_{0 \le k \le \min(c_i,j/w_i)}{(dp_{i-1,j-k \times w_i}+k \times v_i)} \]

请将它化简为可单调队列优化转移式。

解:令 \(j=q \times w_i +r(0 \le r <w_i)\),则

\[dp_{i,q \times w_i +r}=\max_{0 \le k \le \min(c_i,q)}{(dp_{i-1,r+(q-k) \times w_i}+k \times v_i)} \]

\(j'=q \times w_i +r,k'=r+(q-k) \times w_i\),则

\[dp_{i,j'}=\max_{j'-\min(w_i \times c_i,j'-r) \le k' \le j'}{(dp_{i-1,k'}-\frac{k'-r}{w_i} \times v_i)}+\frac{j'-r}{w_i} \times v_i \]

第一层循环枚举 \(i\),第二层循环枚举 \(r\),上面的式子便符合可单调队列优化转移式。

当然了,具体实现没这么恶心。

决策单调性优化 \(\text{DP}\)

四边形不等式/分治优化

斜率优化

二分栈/二分队列

\(\text{wqs}\) 二分

矩阵快速幂优化 \(\text{DP}\)

动态 \(\text{DP}\)

插头 \(\text{DP}\)

\(\text{SOS}\) \(\text{DP}\)

长链剖分 \(\text{DP}\)

虚树 \(\text{DP}\)

整体 \(\text{DP}\)

posted @ 2023-01-27 13:59  2021hych  阅读(98)  评论(0)    收藏  举报