动态dp
这个鬼玩意我已经点开-关上无数次了,当时dkw给洛谷贡模板题的时候还问过我一次来着......然而我并不会,然后,,,,然后NOIP就爆炸了。
所以,趁着难得滚到机房的时间,赶快学习一下QwQ。
直接搬洛谷上的模板题吧
题面
给定一棵(n)个节点的树,点有点权,有(m)次修改单点点权的操作,回答每次操作之后的最大独立集大小。
solution
首先有一个(O(nm))的傻逼做法,显然对于每次修改之后我们只需要(O(n))的做一遍树(dp)就可以了。当然了,既然叫做动态dp,那么自然和dp逃不开关系了,所以我们还是简单的考虑一下这个转移。
设(f_{i,0})表示(i)这个点不选时,子树中的最大独立集;(f_{i,1})表示(i)这个点选时,子树中的最大独立集。那么转移很显然:(f_{i,0}=sum max(f_{v,0},f_{v,1})),(f_{i,1}=V_i+sum f_{v,0})。其中(v)是(i)的儿子,(V_i)是(i)的点权。首先这个(dp)有一个弱智的加速方法,就是对于每一次修改的时候只修改这个点到根这一条链上的所有点就好了,然而随便把复杂度卡满。
类似(Floyd)的转移,我们这里的转移是(max)和(+)的形式,既然(Floyd)的(min)和(+)可以矩乘,那么这里也可以矩乘。(你把所有数取个相反数不就从(max)变成(min)了吗?)。说白点,就是原本矩阵转移是(C_{i,j}=sum_k A_{i,k}*B_{k,j}),将其变成(C_{i,j}=max_k(A_{i,k}+B_{k,j}))就好了。
那么,在这道题目中,我们显然是一个一个子树的贡献逐个加入进当前节点的。我们先从比较容易的地方入手,先考虑一条链应该怎么做。显然一条链的转移很简单,即(f_{i,0}=max(f_{v,0},f_{v,1}),f_{i,1}=V_i+f_{v,0})。那么把转移写成矩阵就是:
我知道这样子是假的矩乘啊,你把小点的矩阵自己补成(2*2)的就好了
那么,对于一条链的时候,我们只需要用线段树维护中间的那个(2*2)的矩阵就可以很容易的解决问题了。(如果这样子的话似乎(NOIP)的(D2T3)就可以拿到(70+)的分数了,突然觉得我自己菜爆了)
回到树上,考虑如何解决树上的问题。想清楚树和链的区别在哪里,显然,在于一个只有一个儿子,另外一个有多个儿子。考虑多个儿子如何合并答案,我们假装前面若干子树合并一起的结果是(f'_{i,0},f'_{i,1}),并且(f_{i,1})中已经考虑完自身权值的贡献了。现在要合并进来的儿子是(v)。那么我们考虑一下当前的转移是什么:(f_{i,0}=f'_{i,0}+max(f_{v,0},f_{v,1}),f_{i,1}=f'_{i,1}+f_{v,0})。
然而,我们发现我们似乎没法让(f'_i)构成的矩阵变成(f_i)的矩阵。
然而为啥我们要吊死在一棵树上啊(当然是在周围再找一棵树吊上去啊),我们把(f_v)的值写成方程就好了啊。
很完美啊,虽然这玩意目前还是一个需要暴力修改到根节点的东西。
恩,暴力修改到根节点,我们怎么样才能让一个点到根节点的跳跃次数复杂度很对呢?——树链剖分啊。
似乎有点想法了,我们发现一个点的(dp)可以用它的所有儿子转移过来。而在进行修改操作的时候,我们只考虑等于重链条数的修改操作。想想这里要怎么实现:我们只修改重链条数次,意味着我们会在重链顶端修改重链顶端对于其父亲的贡献,但是修改当前位置的时候我们并没有顺势修改其重链上的父亲的值,那么我们显然就无法知道重链端点的值了。看起来似乎是这样一回事,仔细想想,重链是啥?是一条链。那么其重儿子的贡献可以单独用上面链的转移贡献进来,即用线段树维护每个节点的矩阵,这样子重儿子的值是可以很容易得到的。那么考虑当前重链端点对于上面父亲的贡献的时候就可以直接修改了。
总结一下的话,大致的过程是这样子的:首先我们考虑我们的转移方程,发现能够将其改写为矩乘的形式,那么我们首先将转移改为矩乘。我们把轻链和重链的转移分开考虑。这样子想,我们的重链被我们单独拎了出来,每个重链上都挂上了若干轻儿子,显然轻儿子对于重链上的独立集的选择是没有影响的,换而言之,如果轻儿子的贡献考虑完之后,那么等价于链上每个点选或者不选有一个权值,求最大独立集。而对于链上的这个东西,是可以直接用线段数维护矩阵支持修改和查询的,那么这题就只需要这么做就好了。即只修改这个点到达根节点上的所有轻链对于父亲的贡献,对于重儿子的贡献先暂时不考虑,每次线段树查询矩阵乘积即可求解出每个点的矩阵,就可以快速求解答案了。