zoukankan      html  css  js  c++  java
  • dp的一种理解角度及[NOI2020]命运 题解

    在APIO2019的时候,一个讲座的主题是“用集合的角度理解DP”,虽然当时讲的很水(真的很水),但是后来思考了一下,这个角度其实还是有一些探究价值的,不过更像是从“生成函数”的角度理解。

    我们考虑一个简单的背包dp,求选(i)个物品的方案数,每个物品有个数(d_k)(f_i)表示选(i)个的方案数,转移方程对任意(i,j)都有(f_{i + j} = f_i * f_j)。这个很容易理解。但是为了拓展,我们把这个乘法这样看待:我们把一个状态(f_i)视作所有选(i)个物品的方案的“集合”,既每种选法的方案数之和((v_1+v_2 cdots +v_n))的形式,其中每个(v)的值显然就是一种方案中选择的那特定(i)个物品个数的乘积(d_{a_1}*d_{a_2}cdots),我们把它叫做这个方案的权值。这样(f_i*f_j)的意义就是:在大小为(i)的所有方案中和大小为(j)的所有方案中各选择一个拼起来,拼起来的新方案权值是两者的权值乘积。不难发现两个方案拼起来以后的权值还是对应的选法数,而新的dp状态(f_{i + j})就包含了所有选法,也可以看成((v_1+v_2 cdots +v_n))的形式,可以继续计算。可以发现,这个方法的本质就是:把可以进行乘法合并的部分(方案数)作为系数,把通过加法合并的部分(选的个数)作为指数,手动进行多项式乘法。而更加复杂的dp也可以抽象成这种形式,只不过多项式系数意义不同,多项式的每一项可能不止是(kx^n),可能是(kx^ny^mz),而合并的时候也不一定是正常的卷积,也可以是(min max)卷积,位运算卷积等。

    考虑拓展这种形式,我们以(NOI2020)(Day1T2)为例子(https://www.luogu.com.cn/problem/P6773)。这道题中,考虑一种暴力的容斥:选择一个链的集合计算贡献。我们发现一个集合的贡献形如((-1)^{选的链数}*方案数*2^{n - 1 - 边集并大小})。其中方案数就是所有选的链数和边集并大小都互相相同的选法数目。我们尝试把这个视为它的权值记录在(dp)方案里,发现两个方案的权值不能通过乘法合并!这时我们就可以考虑:把其中的某项移到外面单独记录,也就是增加dp数组的维度。由于方案数和容斥系数在乘起来以后都可以正确合并,我们不妨就在dp中记录边集并大小,最后在乘上去。可以发现这就是一个提取同类项的过程。事实上,这是由于边集并的贡献采用的运算形式为(2^x * 2^y = 2^{x + y - (n - 1)}),不符合乘法的规则,所以我们只能进行手动处理。

    这样,我们初步的设计了一个dp:(f_{j})表示选的链并大小为(j)的所有方案的权值和。我们发现,如果在选择一条链以后直接把他的边集大小加上去,那么就会进行重复计算。而记录当前的边并集情况需要指数级别的状态。这时,我们可以发现题目的性质有所有链都是直上直下的,也就是说我们如果从下到上选择这些链,只需要知道当前的所有链中端点深度最小的是多少就可以知道新的链与原来集合的并大小。

    所以,一个可行的dp状态为:(f_{i,j,k})表示(只选择下端点在(i)子树中的链,他们的边集并大小为(j),且选择的所有链中最浅的上端点深度为(k))的所有方案的权值和,这里的权值是((-1)^{选的链数}*方案数),因为我们把不能用乘法合并的部分提出去记录到了dp数组里,最后计算答案的时候再乘上去。而转移就很简单了,判断合并的两个方案深度决定是否增加并集大小,然后直接使用乘法即可。

    为了降低复杂度,我们进一步考虑如何合并(2^{n - 1 - 边集并大小})。如果你做的题比较多或者脑子比较好使,就会知道“转概率”的方法。也就是把所有dp值都除一个(2^{n - 1}),最后在乘回去。这样,一个集合的贡献就是((-1)^{选的链数}*方案数* frac{1}{2}^{边集并大小})。这样,我们就可以直接合并两个不相交的集合了。所以,我们令dp中的边集并大小只包含子树中的边,在合并的时候再考虑是否加入子树的父边。这样的话,来自两个子树的并一定不相交,可以直接做乘法合并。而新加入一条边我们可以直接用除2的方法实现贡献处理。

    这样,我们的dp只需要记录端点深度了。如果从正常角度思考,由于这样的dp记录的不只是方案数,还有更复杂的贡献信息,很难理解这么做直接把dp值乘起来的正确性。但是把dp值视为集合以后,把相乘看作方案之间两两合并就很容易设计优化算法和证明正确性。(dp_{i,j})表示的是在(i)子树中选链的所有方法中,选择的链最浅的点深度为(j)的所有方案的权值和。可以发现,因为两个方案的权值可以通过乘法合并,所以两个记录方案和的dp状态做乘法的意义等价于将两种特征的方案两两合并再相加,与前文的背包问题是一样的。

    所以,我们得到了一个简单的(O(n*min(n,m)))的算法。事实上,用生成函数的角度, 把dp里的值看作多项式系数,把dp数组的维度记录的值看作多项式的指数和变量((x^ny^m)中的(x,y,n,m))的话,这道题里优化后的dp数组(dp_{i,j}),列出方程后我们发现这个dp其实就是把(j)视作(x^j),然后做(min)卷积,既合并(f_{i,j}与f_{k,l})时新的状态为(f_{i,min(k,l)})。而在树上做这种形式的dp可以用线段树合并优化,具体可以参考题目https://loj.ac/problem/2537。这样,我们就用(O(nlog n))的时间复杂度解决了本题。

    所以,通过定义广义的“运算”,我们可以更深刻的理解dp:把遵循不同运算规则的生成函数项放在dp的每一维,(其中数组里的数值也是一维,通常存放的是可以用乘法,加法合并的简单贡献),然后进行运算。而这些维度事实上是可以随意交换的,所以用这种角度观察是我们优化时间复杂度,提取同类项简化dp的有利方法,可以尝试使用。

  • 相关阅读:
    [Django]Windows下Django配置Apache示范设置
    《职场》笔记20061119
    Python Django还是RoR,这是一个问题
    收集证据:fsjoy.com的流氓推广和幕后流氓主子[updated]
    爱尔兰网友邀请我对Dublin交通监视器进行手机端开发
    {基于Applet的J2ME模拟器}和{microemulator}[J2ME推荐]
    中国移动IM飞信0802上线新版本 试用手记
    [AsyncHandle]什么引发了ObjectDisposedException?
    百度的“搜索背后的人”的战略
    [Python]检查你的站点的人气
  • 原文地址:https://www.cnblogs.com/Cubelia/p/13533718.html
Copyright © 2011-2022 走看看