总结与归纳之DP
前言
对于
总论
总的有个大纲吧~
何时用
目前没有什么特征说明绝对只能用
- 求最值问题
- 求方案数问题
- 求可行性问题(ex:求概率或期望)
- 可从大问题(规模)分解为小的问题,小的问题可以推出大的问题。
最后一条是关键,如果不满足,则一定不是
何从想
第一步便是要找到描述问题的状态,找的方法因题而异,之后讨论。
第二步便是找到转移方程,可理解为,状态
第三步便是根据无后效性确定枚举顺序,无后效性的意思是:某阶段的状态一旦确定,则此后过程的演变不再受此前各种状态及决策的影响,这决定了
第四步便是分析边界情况,如果一个状态无法再拆分了,那么理论上讲可以快速求解,找到边界即可
为何用
算法竞赛本质上是要求选手利用已有资源解决问题,已有资源便是时间和空间。所以优化问题,便是节约已有资源,因为我们知道几乎所有题都可以搜索实现,或者更无脑的暴力。
分讨类论(doge)
注意,
线性结构
并非状态转移方程线性,而是问题结构线性,序列问题和矩阵问题等,这里讨论的是这类
题目特征:
事实上,只要是序列问题和矩阵问题都可以思考一下。
状态:
原则1:问什么设什么(题目有啥状态就设啥状态)。
原则2:状态规模在可处理范围内(不绝对,参考矩阵快速幂)
原则3:决策和状态绝对相关。即决策影响状态的改变,变了啥,状态里就有啥,除此之外的均为冗余状态。
目前没做过脱离三原则的线性结构
转移方程:
基础模型如下:
解释,
目前,所有(几乎吧~)
区间
线性结构
题目特征:
区间合并问题,区间删除问题,以及有可能通过转换变成这两类的问题。
状态:
原则1:左端点坐标,右端点坐标,这是核心。
原则2:缺啥补啥。(有时候状态转移方程推不出来,就要加一些附加状态,一般是左右端点的附加属性)
目前没做过脱离双原则的区间
转移方程:
基础模型如下:
不过多解释了,注意的是该式子有一个极其常见的变形,那就是通过
树形
当问题跑到树上时,或者可以分解成树的问题(基环树)。树形
题目特征
一种是直接给出树结构(或基环树),一种是告诉你状态之间的限制关系,通过数学推导得出是树结构(或基环树)。
状态
原则1:结点编号一定有。因为在访问子节点时,访问的是编号。
原则2:问什么设什么原则(常见的设计:该结点是否选择,以该节点为根的子树有几个点被选择)。
目前没做过脱离双原则的树形
转移方程:
基础模型如下:
一般来说,这个方程很好列,但是树形
典型题型:树形背包
当状态有在子树里选若干个结点时,树形背包孕育而生,方程如下:
记住,
变形1:换根
当根节点的变化会影响一些值,那么选一个根就很重要了,难道要枚举根?二次扫描法,又称换根
第一步:树形
第二步:第二次树形
变形2:基环树
基环树也不是不可解决,多了一条边,就删吗,枚举环上的边,删掉求
状态压缩
这种题套路的很,所谓状态压缩,是把一个大小为
数据范围暗示一切~。
状态:
原则1:数据范围定压缩对象。如果
原则2:问什么设什么原则。
原则3:原则1不明显时,可以考虑决策会影响哪段数。
目前没做过脱离三原则的状压
转移方程:
基础模型如下:
注意
数位
这更套路。一般来说,如果问题是
状态:
原则1:必须要有数位长度,当前考虑到第几位。
原则2:问什么设什么原则
假原则:学过的都知道可以用记忆化把代码写的漂亮一点,精髓在于在
目前没做过脱离假三原则的数位
转移方程?
没错,是个问号!这玩意有个代码模板!
#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期望
当问题编程求某事件发生的概率,或是某事的期望,并且具有一定规模,就可以来个
状态?
没错,这又是个问号意味着,这和前几个
转移方程?
问号来袭,大同小异。
特别之处!
为啥把它拎出来单独说呢?因为它们有些特别的处理方式。
关于概率
当
关于期望
(补充:
显然,这是对未来的
这有些特别,所以咱浅浅的谈一谈。
其实还有些特别的
计数
其他,都是优化~
接下来要进入优化啦~
数据结构优化
不全面,刷过的题少,所以就浅浅的谈一下。
前缀和优化
并非给定数组的前缀和优化计算,而是,上一轮
如果转移式可以写成:
那么,在求解
前缀积也可以~。
优化
首先,二维偏序是
平面上有若干个点,求对于每个点,有几个其他的点横坐标和纵坐标都小于这个点的横坐标和纵坐标。
方法:开一个
因此我们可以总结出一般的可优化转移式。
为啥可优?把
当然,如果转移式中出现了小于当前下标的最大转移值,也可以用
平衡树优化
线段树优化
线性
在这个转移式中,
用线段树维护
思考从
用线段树维护这一过程,支持上述的影响所需要的的操作,以及
单调队列优化
“当一个选手比你小还比你强,你就打不过他了”是这个算法的核心。我们在遍历可以转移的决策集合中往往只会在意可以取得最优解的决策,那么是否存在在某一次转移后,永远不可能成为最优决策点的决策呢?没错,针对此,我们可以用单调队列优化。具体的,一般形式的可优化转移式如下(以最小值为例):
其中
拓展:单调队列优化多重背包。来个直观的:已知多重背包转移方程如下:
请将它化简为可单调队列优化转移式。
解:令
令
第一层循环枚举
当然了,具体实现没这么恶心。
决策单调性优化
四边形不等式/分治优化
斜率优化
二分栈/二分队列
二分
矩阵快速幂优化
动态
插头
长链剖分
虚树
整体
本文作者:2021hych
本文链接:https://www.cnblogs.com/2021hych/p/17068869.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步