OSDN Git Service

Update memo.md
authorinterestingLSY <30743214+interestingLSY@users.noreply.github.com>
Thu, 23 Aug 2018 06:06:38 +0000 (14:06 +0800)
committerGitHub <noreply@github.com>
Thu, 23 Aug 2018 06:06:38 +0000 (14:06 +0800)
docs/dp/memo.md

index e69de29..ebe0bbc 100644 (file)
@@ -0,0 +1,232 @@
+# 聊聊动态规划与记忆化搜索\r
+\r
+by $\color{Gray}InterestingLSY$ (菜到发灰)\r
+\r
+由于我讲的比较磨叽,兜售目录一份,dalao们可以选择自己喜欢的部分看\r
+\r
+## 目录:\r
+\r
+- 记忆化搜索是啥\r
+\r
+- 记忆化搜索和动态规划有啥关系\r
+\r
+- 记忆化搜索的优缺点\r
+\r
+- 记忆化搜索的注意事项\r
+\r
+## 1.记忆化搜索是啥\r
+\r
+好,就以[这道题](https://www.luogu.org/problemnew/show/P1048)为例,~~假设我现在特别菜~~我本来就特别菜,不会动态规划,只会搜索,我就会直接写一个粗暴的 $DFS$ :\r
+\r
+*注:为了方便食用,本文中所有代码省略头文件*\r
+\r
+```cpp\r
+int n,t;\r
+int tcost[103],mget[103];\r
+int ans = 0;\r
+void dfs( int pos , int tleft , int tans ){\r
+    if( tleft < 0 ) return;\r
+    if( pos == n+1 ){\r
+               ans = max(ans,tans);\r
+               return;\r
+       }\r
+       dfs(pos+1,tleft,tans);\r
+    dfs(pos+1,tleft-tcost[pos],tans+mget[pos]);\r
+}\r
+int main(){\r
+    cin >> t >> n;\r
+    for(int i = 1;i <= n;i++)\r
+        cin >> tcost[i] >> mget[i];\r
+    dfs(1,t,0);\r
+       cout << ans << endl;\r
+    return 0;\r
+}\r
+```\r
+这就是个十分智障的大暴搜是吧......\r
+\r
+emmmmmm....... $\color{Red}30$分\r
+\r
+然后我心血来潮,想不借助任何"外部变量"(就是dfs函数外且**值随dfs运行而改变的变量**),比如ans\r
+\r
+把ans删了之后就有一个问题: 我们拿什么来记录答案?\r
+\r
+答案很简单:\r
+\r
+### 返回值!\r
+\r
+此时 $dfs(pos,tleft)$ 返回在时间$tleft$内采集 **后**$pos$个草药,能获得的最大收益\r
+\r
+不理解就看看代码吧:\r
+\r
+```cpp\r
+int n,time;\r
+int tcost[103],mget[103];\r
+int dfs(int pos,int tleft){\r
+    if(pos == n+1)\r
+        return 0;\r
+    int dfs1,dfs2 = -INF;\r
+    dfs1 = dfs(pos+1,tleft);\r
+       if( tleft >= tcost[pos] )\r
+       dfs2 = dfs(pos+1,tleft-tcost[pos]) + mget[pos];\r
+    return max(dfs1,dfs2);\r
+}\r
+int main(){\r
+    cin >> time >> n;\r
+    for(int i = 1;i <= n;i++)\r
+        cin >> tcost[i] >> mget[i];\r
+    cout << dfs(1,time) << endl;\r
+    return 0;\r
+}\r
+```\r
+\r
+~~emmmmmm....... 还是$\color{Red}30$分~~\r
+\r
+但这个时候,我们的程序已经不依赖任何外部变量了.\r
+\r
+然后我非常无聊,将所有dfs的返回值都记录下来,竟然发现......\r
+\r
+### 震惊,对于相同的pos和tleft,dfs的返回值总是相同的!\r
+\r
+想一想也不奇怪,因为我们的dfs没有依赖任何外部变量.\r
+\r
+旁白:像 $tcost[103]$,$mget[103]$ 这种东西不算是外部变量,因为她们在dfs过程中不变.\r
+\r
+然后?\r
+\r
+开个数组 $mem$ ,记录下来每个 $dfs(pos,tleft)$ 的返回值.刚开始把 $mem$ 中每个值都设成 $-1$ (代表没访问过). 每次刚刚进入一个dfs前(我们的dfs是递归调用的嘛),都检测 $mem[pos][tleft]$ 是否为 $-1$ ,如果是就正常执行并把答案记录到 $mem$ 中, 否则?\r
+\r
+### 直接返回 $mem$ 中的值!\r
+\r
+```cpp\r
+int n,t;\r
+int tcost[103],mget[103];\r
+int mem[103][1003];\r
+int dfs(int pos,int tleft){\r
+       if( mem[pos][tleft] != -1 ) return mem[pos][tleft];\r
+    if(pos == n+1)\r
+        return mem[pos][tleft] = 0;\r
+    int dfs1,dfs2 = -INF;\r
+    dfs1 = dfs(pos+1,tleft);\r
+       if( tleft >= tcost[pos] )\r
+       dfs2 = dfs(pos+1,tleft-tcost[pos]) + mget[pos];\r
+    return mem[pos][tleft] = max(dfs1,dfs2);\r
+}\r
+int main(){\r
+       memset(mem,-1,sizeof(mem));\r
+    cin >> t >> n;\r
+    for(int i = 1;i <= n;i++)\r
+        cin >> tcost[i] >> mget[i];\r
+    cout << dfs(1,t) << endl;\r
+    return 0;\r
+}\r
+```\r
+此时 $mem$ 的意义与dfs相同:\r
+\r
+> 在时间$tleft$内采集 **后**$pos$个草药,能获得的最大收益\r
+\r
+这能ac?\r
+\r
+能.**这就是"采药"那题的AC代码**\r
+\r
+好我们 yy 出了记忆化搜索\r
+\r
+### 总结一下记忆化搜索是啥:\r
+\r
+- 不依赖任何**外部变量**\r
+\r
+- 答案以返回值的形式存在,而不能以参数的形式存在(就是不能将dfs定义成 dfs( int pos , int tleft , int nowans ),这里面的nowans不符合要求.\r
+\r
+- 对于相同一组参数,dfs返回值总是相同的\r
+\r
+## 2.记忆化搜索与动态规划的关系:\r
+\r
+~~基本是朋(ji)友关系~~\r
+\r
+时间复杂度/空间复杂度与**不加优化的dp**完全相同\r
+\r
+不管定义咋扯,反正我觉得\r
+\r
+> 记忆化搜索就是动态规划,**(印象中)任何一个dp方程都能转为记忆化搜索**\r
+\r
+比如:\r
+\r
+$dp[i][j][k] = dp[i+1][j+1][k-a[j]] + dp[i+1][j][k]$\r
+\r
+转为\r
+\r
+```cpp\r
+int dfs( int i , int j , int k ){\r
+       边界条件\r
+    if( mem[i][j][k] != -1 ) return mem[i][j][k];\r
+    return mem[i][j][k] = dfs(i+1,j+1,k-a[j]) + dfs(i+1,j,k);\r
+}\r
+int main(){\r
+       memset(mem,-1,sizeof(mem));\r
+       读入\r
+    cout << dfs(1,0,0) << endl;\r
+}\r
+```\r
+\r
+$dp[i] = max\{dp[j]+1\}\quad 1 \leq j < i \text{且}a[j]<a[i]$  (最长上升子序列)\r
+\r
+转为\r
+\r
+```cpp\r
+int dfs( int i ){\r
+       if( mem[i] != -1 ) return mem[i];\r
+       int ret = 1;\r
+       for( int j = 1 ; j < i ; j++ )\r
+       if( a[j] < a[i] )\r
+               ret = max(ret,dfs(j)+1);\r
+    return mem[i] = ret;\r
+}\r
+int main(){\r
+       memset(mem,-1,sizeof(mem));\r
+       读入\r
+    cout << dfs(n) << endl;\r
+}\r
+```\r
+\r
+### 当然,以我的经验更多情况下记忆化搜索是写完暴力dfs(本来想骗分)后突然发现能改成记忆化搜索\r
+\r
+~~然后AC了一道全场没几个人会的超难dp~~\r
+\r
+感受以下那种发现自己写的暴力改改就是正解的快感吧!\r
+\r
+啊哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈!\r
+\r
+## 3.记忆化搜索的优缺点\r
+\r
+咳咳,说正事\r
+\r
+优点:\r
+\r
+- 记忆化搜索可以避免搜到无用状态,特别是在有状态压缩时\r
+\r
+- 边界情况非常好处理,且能有效防止数组访问越界\r
+\r
+- ~~写起来简单易懂~~至少我镇么认为qwq\r
+\r
+- 有些dp(如区间dp)用记忆化搜索写很简单但正常dp很难\r
+\r
+缺点:\r
+\r
+- 致命伤:不能滚动数组!(哪位dalao会记搜+滚动的请在评论区留名)\r
+\r
+- 有些优化比较难加\r
+\r
+- 由于递归,有时效率较低但不至于TLE\r
+\r
+- 代码有点长~~其实也不算太长~~\r
+\r
+## 4.记忆化搜索的注意事项:\r
+\r
+- 千万别忘了加记忆化! (别笑,认真的\r
+\r
+- 边界条件要加在检查当前数组值是否为-1前(防止越界)\r
+\r
+- 数组不要开小了(逃\r
+\r
+## 如有疑问或质疑,请留下评论或私信我\r
+\r
+### questions are welcome\r