算法/DP {反向DP}

算法/DP {反向DP}

反向DP

定义

对于数组A[N], DP(I)表示A[i...N]这个后缀, 即从DP(>I) 更新 DP(I); (即他等价于 你把数组翻转之后的 正向DP);

性质

反向DP 通常是用DFS, 当然可以用递推 (但如果用递推的话 实际上 你在做的是正向DP, 即你可以把数组翻转下 使用正向DP, 这就等价于 你使用递推方式的反向DP);
用DFS的话 比较方便, 即直接DFS(0)开始 但注意DP[0]他表示的是整个序列 (即后缀[0,1,...N]), 即DFS(i)表示[i,...,N]后缀;
DFS式反向DP 一定是前驱更新方式, 因为你DFS(i)时 你并不知道DP[i]的值, 你是通过DFS(> i)的回溯值 来更新当前DP值的;

@DELI;

这个做法 要比递推式DP复杂的多, 当递推式DP行不通时 才来考虑这个做法, 这通常是用记忆化DFS来实现的;
具体做法为: 对DAG的所有终点进行记忆化DFS, 对于DFS( x) 他里面会调用所有的DFS( x的所有前驱节点) 这是因为DP[x]的值 他需要用到 其所有的前驱节点的DP值 才能更新;

为什么不用递推式DP, 而反着来呢? 这有几个原因{你知道DAG的终点 但不知道他的所有起点具体是什么状态; 对于任意点 你知道他的前驱节点 但不知道他的后继};
举个例子: @LINK: https://editor.csdn.net/md/?articleId=137524604;
. DP状态为(ed序列的最后元素, cont序列长度, mi序列相邻元素差的最小值) (即所有合法序列中 形如[PRE, ed, SUF] ([PRE,ed]长度为cont 且[PRE,ed]的相邻元素最小值为mi, 注意[PRE,ed,SUF]是一个合法序列 即他的长度是K)的序列);
. . #错误#: 你千万不要以为 这个DP状态定义的是: 所有[PRE, ed]的子序列, 否则的 这你就成递推式DP了, 那么你DP更新是(ed,cont,mi) -> (ed+{1/2/...}, cont+1, mi2) 答案(即DAG的终点)是(?, K, ?); 而我们这里是DFS式DP, 他的定义非常特殊 他是说的: 所有答案序列(即长度为K)中 以当前子序列(即[PRE, ed])为前缀的所有答案序列, 他的DP更新是(ed,cont,mi) <- (ed+i, cont+1, mi2)(正好是反着的), 他的答案(即DAG的终点)是(?, 1, 2e9)(也正好是反着的);
. 此时, 我们知道DAG的所有终点((0/1/2/..., 1, 2e9)), 但是 你不知道 DAG的所有起点((0/1/2/..., K, ?)其中第三个参数 即mi你是未知的 当然如果你预处理出所有相邻元素的差Dels 那么?他肯定是Dels里面的 但信息其实是不全的 Dels里的很多值 其实并不会作为起点的状态), 但相对来说 你要得到DAG的起点 很不容易;
. 其次, 对于一个状态(ed,cont,mi), 你要获取其DAG的前驱节点 很简单 是(ed + {1/2/3/...}, cont+1, min(mi, A[ed+i]-A[ed])); 可是 如果你要获得他的后继节点 即( ed - {1/2/3/...}, cont-1, ?) 第三个参数 只知道他满足? >= mi 但我们并不知道?具体会等于哪些值, 而且 ed-i不一定是正确的, 比如如果说A[ed] - A[ed-1] < mi 那么一定说明 (ed-1, cont-1, ?)一定不是他的后继节点;
综上, 我们很难知道: {DAG的所有起点; 对于任一点 很难知道他的后继节点}, 所以这导致了 我们很难使用一般的递推式DP; 然而 我们知道他的逆向信息, 因此可以反向遍历整个DAG, 使用DFS的回溯来进行DP更新;

@DELI;

比如当前DFS是[pre,...] (pre是已知的), 那么其返回值就是dfs(...) 他表示的是...对答案的贡献 (注意与pre无关!);

@DELI;

由于后继更新方式的递推DP 他无法直接转换成DFS方式 (@LINK: (https://editor.csdn.net/md/?not_checkout=1&articleId=137568310)-(@MARK_0));
所以, 才有了这里的DFS式DP? 是吗? 思考下他两者的关系;

@DELI;

@DELI;

注意, 虽然说 DFS函数的过程 他正好是逆着DAG进行的 (即DFS函数的DAG图 正好是 DP的DAG图的反图), 但是 DP更新方向一定是唯一固定的! 即DP[x] -> DP[x的后继], DFS虽然是从x进入到x的前驱进行DFS, 然后他的DP更新 仍然是DP[x的前驱]->DP[x];

@DELI;

DFS和递推 从本质上来说 一定是可以互相转换的, 一个正向 一个逆向, 因为他俩对应的DAG是唯一确定的;
但是, 从代码层面上讲, 他俩其实是很难相互转换的, 比如使用DFS式DP 正是因为我们无法使用递推 即此时你信息不足 你无法知道DAG的所有起点 所以两者在代码层面上 很难相互转换;

@DELI;

错误

#以下论述是错误的#;
DFS式DP 有2种:
1: (前驱更新方式的)递推DP的DFS; (@LINK: (https://editor.csdn.net/md/?not_checkout=1&articleId=137568310)-(@MARK_0)); 其实他本质上 还是递推DP, 只不过 他会优化 即只遍历合法的状态, 而朴素FOR循环 他会遍历很多非法状态;
2: 真正的DFS式的DP (即我们这里讲的这种);

这是错误的, 因为1: 毫无用处, 对于递推DP 不存在使用DFS来优化的情况 (DFS和DP的代码逻辑完全相同), 对于DFS式DP 只有一种情况 就是我们这里讲的反向求解的DFS;

@DELI;

假如你把DFS写成了: void DFS( ...){ if( 终点){ 更新答案 return;}}, 这不是DFS式DP了已经 也不是记忆化DFS! 他就是普通的DFS, 即遍历所有的DFS路径;

@DELI;

@LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=136842739;
记忆化DP 更新答案的方式, 并不是说 到达路径终点(即DFS的尽头) 才去更新答案; 这是完全错误的, 因为正是由于记忆化, 即便是很多不同路径 他们中 只有1个会进入到DFS尽头 其他的路径 都因为记忆化而提前终止了, 因此 要在DFS的返回值时 更新答案, 而不是到DFS尽头才更新答案;

应用

@LINK: https://editor.csdn.net/md/?not_checkout=1&articleId=137524604;
对所有的C(N,K)这些方案, 进行DP, 通过记忆化DFS;

笔记

DFS式DP, 通常是: 将答案方案集合S *划分为*不同的答案方案子集合(比如答案方案有`S={s1,s2,s3,...}` 划分为子集合`S = {Sa U Sb U...}`);
 . 即DP定义为: S中满足某种条件的方案的集合;
 
举例:  A[N]中枚举长度为K的子序列; (设所有答案方案集合为S)
DP定义: DP(ed,X)表示S中 以T为前缀的方案的集合; (其中`T`: 以`ed`为结尾的(满足某种X条件的)序列, 即`T = [...,ed]`);
DP更新: (ed,X) <- (ed+{1/2/...}, XX); (注意箭头方向 DFS调用是-> 但DP更新是回溯时即<-);
DP答案: (开头元素, ?);
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值