NOIP大纲整理:(十)动态规划巩固与提高1:DP与记忆化搜索概念

记忆化搜索概念讲解

经典例题:数字金字塔(Luogu 1216) 

写一个程序来查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。每一步可以走到左下方的点也可以到达右下方的点。

我们现在这里讨论搜索如何实现: 

状态:目前在第x行第y列 

行动:向左走,向右走 

例如:一个底边为4的三角形共有八种状态:

我们按照一般的搜索思路,进行深度优先搜索:

void dfs(int x,int y,int val)
{
    val+=a[x][y];//加上权值
    if(x==n-1)
    {
        if(val>ans)ans=val;//更新更大的ans
        return;
    }
    dfs(x+1,y,val);//往左边走
    dfs(x+1,y+1,val);//往右边走
}

考虑时空效率,DFS确实很暴力啊,有没有什么优化呢?? 

我们引入“冗余搜索”这个概念:无用的,不会改变答案的搜索 

例子:观察下面两个例子。用两种方式都能到达第 3 行第 2 列,只是路径不同,同时走到这个点两条路权值和不一样,其中一个总和为 8,一个总和 12。 

那么可以观察可得,总和为 8 的搜索是冗余的(不会改变答案),即使不继续搜索,答案也不会改变。 

因为 12 往下搜索,无论往左往右,都会比 8 对应的路径大。

可见,冗余就是剪枝的“枝”,那么如何利用冗余搜索,来优化程序呢? 

我们可以对于每一个位置记录一个值 F,代表搜索到此位置时,最大的路径和是多

少,这样如果搜到某一个位置时候,路径和不大于记录值F,说明这个搜索是冗余搜索,直接退出,如果大于,就需要更新F值并且继续搜索。 

我们就把这种搜索叫做记忆化搜索,根据之前的“记忆”来优化搜索;在这道题中,每个位置的“记忆”就是最大的路径和

//T1:数字金字塔(记忆化搜索)
void dfs(int x,int y,int val)
{
    val+=a[x][y];
    // 记忆化过程
    if(val<=f[x][y])return;//发现冗余搜索,退出
    f[x][y]=val;//f[x][y]记录这个点当前最大权值
    if(x==n-1)//如果搜到了最后一个点,ans更新保存最大值,退出即可
    {
        if(val>ans)ans=val;
        return;
    }
    dfs(x+1,y,val);//继续搜索
    dfs(x+1,y+1,val);
} 

01背包问题

经典例题:采药(Luogu 1048) 

辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。” 

草药 1 :时间 71;价值 100

草药 2 :时间 69;价值 1

草药 3 :时间 1 ;价值 2

@最优选择:草药 2+3:总时间 70;总价值 3 

这题是经典的背包问题,和金字塔问题不同的地方在于:它是有重量限制的。 

我们还是先用记忆化搜索来思考这个问题: 

状态:目前已经决定到第x件物品,当前背包中物品总重量为w,总价值为v; 

行动:这件物品取还是不取; 

约束:物品总重量不超过w(背包总重量); 

目标:物品总价值最大; 

比较下列两种情况: 

状态相同:x1=x2(当前搜索到同一件物品),w1=w2(当前总重量相等); 

价值不同:但它们的背包总价值不同,其中v1<v2。(经过不同的路径到达同一个点,但是后者的val更大)

则我们可以说状态1为冗余的,因为它肯定比状态2要差。

*记忆化:对于每个状态(x,w),记录对应的v的最大值。

//T5:采药(记忆化搜索)
void dfs(int t,int x,int val)//t为剩余时间,x为当前决定的第几株草药,val为总价值
{
    //记忆化
    if(val<=f[t][x])return;
    f[t][x]=val;
    if(x==n)//把草药采摘完了,直接返回
    {
        if(val>ans)ans=val;//更新最大值ans
        return;
    }
    dfs(t,x+1,val);
    if(w[x]<=t)dfs(t-w[x],x+1,val+v[x]);//如果我们还有时间,继续采摘!
}

那好的,说完记忆化搜索我们回到正题:动态规划啦!记忆化搜索是DP的基础。 

我们再回到数字金字塔这个问题来,下图的黑色三角形是我们记忆化搜索的路径,我们想想,是不是可以不通过记忆化搜索就能得到这个黑色三角形??

最优性:设走到某一个位置的时候,它达到了路径最大值,那么在这之前,它走的每一步都是最大值。 

-考虑这条最优的路径:每一步均达到了最大值

最优性的好处:要达到一个位置的最优值,它的前一步也一定是最优的。 

-考虑图中位置,如果它要到达最优值,有两个选择,从左上方或者右上方的最优值得到:

所以从这里,定义动态规划(DP):只记录状态的最优值,并用最优值来推导出其他的最优值。 

记录 F[i][j] 为第 i 行第 j 列的路径最大值,有两种方法可以推导:(两个分支两种状态,选取最大) 

@顺推:用 F[i][j] 来计算 F[i+1][j],F[i+1][j+1] 

@逆推:用 F[i-1][j],F[i-1][j-1] 来计算 F[i][j] 

这两种思考方法也是动态规划中最基本的两种方法,解决绝大部分DP我们都可以采用这样的方法。

//T2:数字金字塔-顺推(有点类似于记忆化搜索的思路)
f[0][0]=a[0][0];
for(int i=0;i<n-1;++i)
for(int j=0;j<=i;++j)//f数组为最优值路径(黑色金字塔,a为源数据数组(紫色金字塔)
{
    //分别用最优值来更新左下方和右下方
   f[i+1][j]=max(f[i+1][j],f[i][j]+a[i+1][j]);//和当前的f[i+1][j]比较
   f[i+1][j+1]=max(f[i+1][j+1],f[i][j]+a[i+1][j+1]);//和当前的f[i+1][j+1]比较
}
//T4:数字金字塔-逆推(自顶向下)
f[0][0]=a[0][0];
for(int i=0;i<n;++i)//单独处理
{
   f[i][0]=f[i-1][0]+a[i][0];//最左的位置没有左上方
   f[i][i]=f[i-1][i-1]+a[i][i];//最右的位置没有右上方
    for(intj=1;j<i;++j)//在左上方和右上方取较大的
   f[i][j]=max(f[i-1][j-1],f[i-1][j])+a[i][j];
}
//答案可能是最后一行的任意一列
ans=0;
for(int i=0;i<n;++i)
ans=max(ans,f[n-1][i]);

*转移方程:最优值之间的推导公式。 

@顺推: 

F[i+1][j]= MAX (F[i][j] + a[i+1][j]); 

F[i+1][j+1]= MAX (F[i][j] + a[i+1][j+1]); 

@逆推: 

F[i][j]= MAX (F[i-1][j], F[i-1][j-1]) + a[i][j]; (注意!逆推时要注意边界情况! )

顺推和逆推本质上是一样的(复杂度一致);顺推和搜索的顺序类似;而逆推则是将顺序反过来;顺推考虑的是“我这个状态的下一步去哪里” ,逆推的考虑的是“从什么状态可以到达我这里” 。同时在转移的过程中我们要时刻注意边界情况。 

我们还可以改变搜索顺序:

//T3:数字金字塔-逆推/路径自底向上
//改变顺序:记录从底部向上走的路径最优值
for(int i=0;i<n;++i)
f[n-1][i]=a[n-1][i];//备份底部自己这一行
//逆推过程:可以从左下方或右下方走过来;没有边界情况
for(int i=n-2;i>=0;--i)
for(int j=0;j<=i;++j)
f[i][j]=max(f[i+1][j+1],f[i+1][j])+a[i][j];//当前[i][j]左下方和右下方取较大加上当前的
//答案则是顶端
ans=f[0][0];
//和之前的逆推区别:这样较自顶向下不需要判断边界,更加简单

 *转移顺序:最优值之间的推导顺序 

一个小问题:在数字金字塔中,为什么能够使用动态规划呢??答:因为有明确的顺序:自上而下,也就是说,能划分成不同的阶段,这个阶段是逐步进行的,这和搜索顺序也是类似的,所以,只要划分好阶段,从前往后推,与从后往前推都是可以的

接下来我们进入重点,还是回到刚才的采药问题,我们回忆刚才这题的记忆化搜索。 

状态设计:记录 F[i][j] 为,已经决定前 i 件物品的情况,在总重量为 j 的情况下,物品总价值的最大值。同样也是有两种方法可以推导:

@顺推: “我这个状态的下一步去哪里”

@逆推: “从什么状态可以到达我这里” 

当前状态: F[i][j] 为,已经决定前 i 件物品的情况,在总重量为 j的情况下,物品总价值的最大值。

@顺推: “我这个状态的下一步去哪里” :我现在要决定下一件物品取还是不取。

>如果不取的话,可以达到状态 F[i+1][j];

>如果取的话,可以达到状态 F[i+1][j+w[i+1]](需要满足重量约束);

@逆推: “从什么状态可以到达我这里” :考虑我这件物品取不取。

>如果是不取的,那可以从 F[i-1][j] 推导而来;

>如果是取的,可以从 F[i-1][j-w[i]] 推导而来的(同样需要满足重量约束)

//T6:采药(DP/顺推)
for(int i=0;i<n;++i)
for(int j=0;j<=t;++j)
{
    //不取
   f[i+1][j]=max(f[i+1][j],f[i][j]);
    //取
    if(j+w[i]<=t)//满足重量限制(类比背包问题)
   f[i+1][j+w[i]]=max(f[i+1][j+w[i]],f[i][j]+v[i]);
}
//答案
ans=0;
for(int i=0;i<=t;++i) ans=max(ans,f[n][i]);

学到这里,我们大概摸清了动态规划的轮廓是什么,使用动态规划较DFS解决了时间上的问题,那么我们可不可考虑解决一下空间上的问题呢?由于动态规划满足”无后效性原则“,当前状态F[i]之和上一个状态F[i-1]有关,和上个状态之前的都没有关系,所以我们可以考虑使用滚动数组来保存这两个状态,一上一下,互为前后状态,节省空间啊!! 

这就是——数组压缩! 

所以一个直观的做法是,记录两个数组,分别记录 F[i-1] 与 F[i] 的值。

*但更进一步,我们可以甚至不记录两行,只记录一行的状态。

-我们倒着枚举,在 F[i-1] 数组中逐步更新,让它逐步变为 F[i]。

因为是倒着枚举的,先枚举的位置都已经无用了,可以直接用 F[i] 的元素来替换。

  

//T10:采药(DP/逆推/数组压缩)
//用一个一维数组来代替二维数组
for(int i=1;i<=n;++i)
for(int j=t;j>=0;--j)//重量:倒着枚举
{
    //不取:对数组没有影响
    //f[i][j]=f[i-1][j];
    //取
   //if(j>=w[i])f[i][j]=max(f[i][j],f[i-1][j-w[i]]+v[i]);
   if(j>=w[i])f[j]=max(f[j],f[j-w[i]]+v[i]);//如果还有采药时间,执行
}
//在枚举过程中,大于j的位置等于f[i][j],小于j的位置等于f[i-1][j]

猜你喜欢

转载自blog.csdn.net/liusu201601/article/details/81435946