技巧与错误(3)

来源:互联网 发布:caffe训练googlenet 编辑:程序博客网 时间:2024/06/14 15:17

11.技巧 DP 背包

    (1)01背包的基本思路,一个并不太高明的方程,当然就是用f[i][j]表示前i个物品装入容量为j的背包当中所能够获得的最大的价值,其状态转移为f[i][j]=max(f[i-1][j],f[i-1][j-w[i]]+w[i]),这样做的局限性就是在处理价值都为正数的物品的问题当中,浪费了空间。另外一种方式就是从大到小循环容量,即使用f[i]表示容量为i的背包所能获得的最大价值,在遍历每个物品的时候,从大到小循环容量,这样当前的f[j]实际上就继承了上一次的f[i-1][j],并且用来更新它的f[j-w[i]]也是f[i-1][j-w[i]],正好实现了前面一个方程的过程,然而这样做的局限性就是,如果物品的价值当中有负数,并且问题比较复杂,不容许你预处理先去掉某些物品的时候,这样做就会出错,这个时候,使用前面一个方程加上滚动数组优化就可以解决。第二个方程的代码如下:

void Simple(int c,int v){for(int i=maxn;i>=c;i--)   f[i]=max(f[i],f[i-c]+v);}
    单独提出来写的好处是便于模块化地处理,与下文讲到的接轨。

    注意,如果是满背包问题,即要求正好正满的问题,那么在设初值的时候就将除了f[0]=0以外的状态全部设为负的极大值。

    (2)完全背包问题,代码如下:

void Complete(int c,int v){for(int i=c;i<=maxn;i++)   f[i]=max(f[i],f[i-c]+v);}
    我们会发现与01背包的不同就是把循环顺序调换了一下,即f[i][j]可以由f[i][j-w[i]]转移而来,也就是说允许一个数取多次,听起来很自然,然而其中的内涵是很深的,记得2014年NOIP那道 Flappy Bird 的那道题,很多人拿了60,70分,就是因为在DP的时候去枚举了到达这一地点之前连续点了几下,这样的话,f[i][j]就只由f[i-1][k]转移过来,而没有使用我们本可以使用的f[i][k]这一层的信息,导致时间复杂度增加了一维,还有很早的IOI里的那道现在已经是基础题的橱窗布置,也有人在处理f[i][j]表示前i束花放到前j个花瓶的最大观赏值时,状态转移时枚举了最后一束花被放在了哪一个瓶子里,即f[i][j]=max(f[i-1][k-1]+a[i][k]),这样的时间复杂度是n(m^2)的,然而实际上f[i][j]的转移可以表示为f[i][j]=max(f[i][j-1],f[i-1][j-1]),成功将时间复杂度降低到了nm,因为我们充分利用了当前这一层的信息,完全背包的解法也是如此,有人疑惑为什么不用枚举物品i被放了多少个,而只是单单讨论物品i在当前容量中是放还是不放,想一想,当我们枚举最后一个物品被放了多少个的时候,在我们的想法当中,我们是将我们想象中的一堆物品,打包批发一起放进去的,是一次就更新了一坨,然而实际上,物品却是一个一个放进去的,也就是说,当我们对于每个容量都枚举物品i放了多少个的时候,其实我们所枚举的放了多个的情况,都已经被之前的DP过程给算过了,我们不过是一次又一次重复了我们已经得到的答案的计算而已,比如说,当我们枚举物品i放两个,即是否能被f[i-1][j-2*w[i]]优化的时候,实际上等价于是否能被f[i][j-w[i]](这个f[i-1][j-w[i]]还不是真正意义上的最优的f[i][j-w[i]])优化而已。

    完全背包的代码非常简单,然而其中的道理蕴含得却很深刻,所告诫我们的是,即使我们得到了一个正确的DP方程的定义,在问题比较复杂的情况下,我们也应当思考如何精致而完美地进行方程的转移,而不是简单草率地得出一个虽然正确但是耗费了许多时间进行重复计算的决策方式,充分地利用所得到的信息!

    (3)多重背包问题,这个也是很经典的问题了,高效的解决有两种方法,一个是基于分组的单调队列,一个是二进制法,前者虽然在理论时间复杂度上要优秀一些,然而个人觉得细节要更难把握,容易写错,所以选择后者,其思路就是将物品的个数分解成1,2,4,8……即2的幂,因为它们的组合可以表示1到物品个数的所有数,通过这样将物品的个数降低到log级别,比较简洁的写法如下:

void Multiple(int c,int v,int cnt){if(c*cnt>=maxn){Complete(c,v);return;}int k=1,temp=cnt;while(k<cnt){Simple(k*c,k*v);temp-=k;k*=2;}Simple(temp*c,temp*v);}
    (3)二维费用背包问题,裸的这种问题和01背包几乎没有什么区别,然而精髓的地方在于对于2维这种东西的理解,它不仅仅代表了一个所谓的生硬的费用,而是代表了一系列二维特征的信息组织形式,比如说USACO 三角形牧场,使用指定的n块木板围成一个面积最大的三角形,其中n<=30,l<=40,有人可能会想要暴搜,然而一种稳定的解法就是,因为总的周长是一定的,且可能构成答案的最长的长度不超过600,所以我们可以使用f[i][j]表示两条边的长度分别为i,j的情况是否存在,使用二维费用背包式的DP将其求出来,最后暴力枚举两条边,判断它们与剩下的确定的第三条边是否能组成一个三角形,然后计算面积就OK。

    (4)分组背包问题,也就是每一组物品最多只能选一个的背包问题,要特别注意循环的顺序,代码如下:

for(int i=1;i<=the_number_of_group;i++)       for(int j=money;j>=0;j--)          for(int l=1;l<=the_size_of_this_group;l++)if(j>=cost[l])f[j]=max(f[j-cost[l]],f[j]);
    其中容量这一维一定要在遍历组里的物品这一维的外面!切记。

    (5)有依赖的背包问题,这个也很经典啊,比如NOIP2006的金明的预算方案,那个就是一个简化的有依赖的背包问题,一个比较常规的做法就是,先对每个组进行DP,计算出在该组中符合条件的花费下所能取得的最大值,因为主件必选,其余的完全是01背包的选法,所以是很好处理的,然后把这一组的各种合法花费当做一组,最后进行分组背包的DP就可以了,然而这样做还不够接近本质,我们完全可以以一个主件为数根,将它与它所属组的附件全部连边,其余的也是如此处理,像这样建立一棵树,最后进行一次树形DP就OK了。

12.技巧 DP 部分和

    部分和是个能够非常方便地组织与分析信息的信息总结方式,其优秀之处不仅在于其形式,还在于其思想。

    (1)长度不超过m的最大连续子段和问题,使用部分和,我们可以简洁有效地表示,f[i]为以i为结尾的长度不超过m的最大连续子段和,sum[i]为1到i的数的和,那么

    f[i]=sum[i]-min(sum[i-k])   k<=m,得到这样一个式子,一个非常明显的时间复杂度为nm的暴力算法就出来了,而再仔细分析,我们会发现当i<j&&sum[i]>sum[j]的时候,sum[i]一定不是答案,运用这个思想,我们就可以使用单调队列优化,维护一个单调递增的队列,从而实现O(n)的复杂度解决。

    (2)最大子矩阵问题,在给定的一个由整数组成的矩阵中找出一个最大的规模任意的子矩阵,如果采用暴力的方法,纯暴力,枚举左上角,右下角,再现场求和,那么复杂度将是n^6级别的,如果懂得使用二维前缀和,(或者一维的前缀和配合递推),那么暴力枚举左上角和右下角再直接取出和就能降到n^4,再进一步,若是使用前缀和压缩矩阵,枚举上边和下边,做最大连续和的DP,那么时间复杂度就是n^3。

    若所求的矩阵的规模是固定的,那么就只需要直接枚举右下角(或左上角),运用二维部分和直接求和即可。

    (3)最大正方形问题,在给定的一个01矩阵当中,求一个最大的全部由1组成的正方形,这个的思想与前缀和类似,用f[i][j]表示以坐标(i,j)结尾的最大正方形,那么f[i][j]=min(f[i-1][j-1],f[i][j-1],f[i-1][j])+1,当然也可以是min(f[i-1][j-1]+1,l[i][j],u[i][j]),即要么继承(i-1,j-1),要么是向左最长的1的个数要么是向上最长的1的个数。

    (4)悬线法求最大子矩阵,给出一个由非负整数组成的矩阵,要求一个权值最大的不含0的子矩阵,一个比较常规的思路就是,将0全部设为负的极大值,直接使用(2)中所讲的n^3,然而实际上还有一种专门用于解决此种问题的悬线法,即记l[i][j]为以i,j为坐标,最多向左延伸(不遇到0)的长度,r[i][j],最多向右延伸的长度,h[i][j],最多向上延伸的长度,那么直接n^2扫描,对于一个点i,j,若其h[i][j]大于1,就继承i-1.j的l和r,然后直接计算和,就行了,也就是说,我们枚举了以每个点为矩阵的底边,以其最大延伸长度为高的最大的矩形,听起来比较简单,可是我一开始就有些疑惑,为什么非要记l,r,而不能只记一个l和h,然后依然按照这种方法计算呢?想了想,发现有可能存在以某个点为右下角的矩形其高并非其所能延伸的最高点,而是一个矮胖型的矩阵,这样就算漏了,所以若是只记l,h的话需要在枚举右下角的基础上再枚举向上延伸的高度才可以,而记了l,r,h又为什么不会算漏矮胖型的矩阵呢?我们假设我们得到了我们所想要得到的最大子矩阵,那么它的底边上的某一点的最大延伸长度一定等于其高——这是显然的,因为若是全部都大于其高的话就可以至少再向上延伸一层,与它本身是最大子矩阵矛盾,这样就避免了漏算正确答案。

13.技巧 树形DP

    (1)一个经典的树形DP的问题,给出一个有根树,每一条边都有其固定的权值,要求保留指定条数的边,使剩下的依然是一棵以原来的根为根的树,如果我们把视野放宽一点,把它当成背包问题来看,那么这就是一个,多重依赖关系的背包,即在一组物品里面有主件(子树根),有附件(儿子节点),附件中还有可能重复这个过程,即附件中还有更低一级的主件……我们定义f[i][j]为以i为根,保留j条边的最大权值,那么f[i][j]=max(f[i][j],f[i][j-k]+f[l][k]) (k<j,l为i的儿子),我们发现,这和前文中提到的金明的预算方案的解决思路一致,相当于将其当做一个分组背包,计算出每个权值下的最大值,再把它们拿来优化根,倒不如说这道题的做法是定义更加广泛的金明的预算方案,代码如下:

void DFS(int u){           int v;for(int i=first[u];i;i=edge[i].next){v=edge[i].to;DFS(v);for(int j=maxn;j>=1;j--)   for(int l=0;l<j;l++)f[u][j]=max(f[u][j],f[u][j-l-1]+f[v][l]+edge[i].v);}return;}
    这个算法的时间复杂度是n(m^2)的,然而还有一种比较牛逼的基于01背包的写法,代码如下:
void DFS(int u){           int v;for(int i=first[u];i;i=edge[i].next){v=edge[i].to;for(int j=1;j<=maxn;j++)f[v][j]=f[u][j];DFS(v);for(int j=1;j<=maxn;j++)f[u][j]=max(f[u][j],f[v][j-1]+edge[i].v);}}
    我们能够很容易地看出这个算法的时间复杂度是nm的,比上面那一个算法整整少了一个数量级!但是为什么呢?怎么做到的?又付出了什么代价呢?经过对代码的分析,我们发现在计算儿子的时候,儿子继承了老爸的值,然后才开始进行下一步的DP,而转移方式就变成了,是否需要连接儿子的这一条边?准确地说,是是否需要已经接受儿子优化的目前已经探明的整颗树,如果要,那么就必须要固定的那条父子边。这样做,所付出的代价就是,我们只是得到了根的正确信息,没有得到各个子树的相同定义的正确答案,然而题目也只需要根的正确信息!其中的关键在于,题目的要求,只是相当于在普通的01背包的基础上,增加了一些依赖关系而已,而我们却把这个并非等价于分组背包的限制的题目要求,强行转化为了分组背包的问题,而去使用分组背包来解决,就好比我们在解决01背包问题的时候,吃饱了撑去给物品分块!那么相对于树形DP来说,我们的DP思路就由解决子树的问题之后再来解决根的分组背包的问题,转换成了调整DP顺序之后的,是否要某一条边的,自然的符合依赖关系的01背包问题。

    (2)讨论子节点状态的树形DP问题,比如说,没有上司的晚会,即给出一棵树,每个节点都有其权值,要求选出任意个点,使任意两个点不在一条边上,求最优值。其DP方程一般都会增加维度描述节点信息,比如这一道就是f[i][0]代表以i为根且不取i的子树的最大值,f[i][1]代表以i为根且取i的子树的最大值,那么对于i的儿子j,f[i][0]=f[i][0]+max(f[j][0],f[j][1]),f[i][1]=f[i][1]+f[j][0]。

    (3)几个经典的树上问题:求树的重心,这个不算是DP但是写在这里也还好,代码:

void get(int x,int fa){son[x]=1;f[x]=0;for(int i=first[x];i;i=e[i].next)if(!vis[e[i].to]&&e[i].to!=fa){get(e[i].to,x);son[x]+=son[e[i].to];f[x]=max(f[x],son[e[i].to]);}f[x]=max(f[x],sum-son[x]);if(f[x]<f[root])root=x;}
    求树的直径,一种方法是两次DFS,第一次随便从一个点出发DFS找最远点,也就是直径的其中一个端点,再从找到的这个点出发,找到直径的另一个端点。

    另一种方法是,两次DP,求所有点为起点的最长路,然后枚举就行了,这一个就是纯粹的DP了,用f[i][0]表示以i为起点却不走它的子树的最长路,f[i][1]表示以i为起点且走它的子树的次长路,f[i][2]表示以i为起点且走它的子树的最长路,注意,这里的最长与次长不允许是同一个儿子走出来的,那么f[v][0]就要么是向上走到老再向上走,要么走到老爸后向下拐(注意判重),最后的答案就是每个点的max(f[i][0],f[i][2])。




2 0
原创粉丝点击