今天是赵和旭老师的讲授~
背包 dp 模型
背包 dp
一般是给出一些“物品”,每个物品具有一些价值参数和花费参数,要求 在满足花费限制下最大化价值或者方案数。
最简单几种类型以及模型:
0/1背包;
完全背包;
多重背包;
01背包
状态设置:dp [ i ][ j ] 表示前 i 个物品花了 j 的花费能选出物品的最大价值是多少;
状态转移方程:
如果我们选了第 i 个物品,那么前 i-1 个物品只能花 j - w [ i ] 的钱,再加上这个物品本身的价值;
如果我们不选的话,那么花费和前 i-1 个物品的花费是一样的;
则 dp [ i ][ j ] = max { dp [ i-1 ][ j ] , dp [ i-1 ][ j-w [ i ] ] + v [ i ] };
复杂度 O ( N * M ) 。
更简洁更省空间更常用的写法:
如何求方案数?
状态设置:f [ i ][ j ] 前 i 个物品花费 j 元的方案数;
那么对于第 i 个物品买与不买都会产生不同的方案,我们加起来就好了:
f [ i ][ j ] = f [ i -1 ][ j ] + f [ i-1 ][ j - w [ i ] ];
如何求最大价值时的方案数?
我们只需要看 dp [ i ][ j ] 是从哪里转移过来的就好了:
dp [ i ][ j ] = max ( dp [ i-1 ][ j ],dp [ i-1 ][ j - w [ i ] ] ) ;
如果 dp [ i-1 ][ j ] > dp [ i-1 ][ j - w [ i ] ] 的话,那么说明 dp [ i ][ j ] 会从 dp [ i-1 ][ j ] 转移过来,那么 f [ i ][ j ] = f [ i-1 ][ j ];
如果 dp [ i-1 ][ j ] < dp [ i-1 ][ j - w [ i ] ] 的话,那么说明 dp [ i ][ j ] 会从 dp [ i-1 ][ j - w [ i ] ] 转移过来,那么 f [ i ][ j ] = f [ i-1 ][ j - w [ i ] ];
如果 dp [ i-1 ][ j ] = dp [ i-1 ][ j - w [ i ] 的话,那么说明 dp [ i ][ j ] 从这两个状态转移过来都可以,那么 f [ i ][ j ] = f [ i-1 ][ j ] + f [ i-1 ][ j - w [ i ] ];
完全背包
每一个物品可以选无限个。
dp [ i ][ j ] = max { dp [ i ][ j-w [ i ] ] , dp [ i-1 ][ j ] };
在这里提一下一个贪心的预处理:
对于所有 Vi ≥ Vj,Ci ≤ Cj 的物品 i,都可以完全扔掉,对于体积相同的物品只需要留下价值最大的物品;
对于随机数据这个优化的力度非常大。
更简洁更省空间更常用的写法:
多重背包
对每一个物品,最多能用 t [ i ] 次。
最暴力的方法就是转移的时候枚举这个物品选几个即可。
dp [ i ][ j ] = max { dp [ i-1 ][ j-w [ i ] * k ] + v [ i ] * k | k <= t [ i ] };
复杂度O( N * M * t [ i ] );
更简单更省空间更常用的写法:
优化一
可以把 t [ i ] 拆成 1 + 2 + 4 + 8 … 2k + x, 这样 k+1 组,x 小于 2k+1 ,然后 我们会发现这些组能拼成 0 … t [ i ] 每一种情况,然后这样我们就成了 n * log ( t [ i ] ) 个物品的 0/1 背包问题。
对于 [ 0 , 2k+1 - 1 ] 都能拼成,所以 [ x , 2k+1-1 + x ] 也都能拼成,x <= 2k+1 -1, 则[ 0 , 2k+1 -1 + x ] 全能拼成。
复杂度 O ( n * log ( t [ i ] ) * m );
另一个优化
我们来观察一下式子:
我们发现对于第二维,我们的j和能转移过来的 j - w [ i ] * k 在模 w [ i ] 意义下是同余的,也就是说我们可以对于第二维按照模 w [ i ] 进行分类,不同类之间不会互相影响。
设 f [ j ] = dp [ i-1 ][ j * w [ i ] + r ] 。r 是我们枚举模 w [ i ] 的一个类。
实际上就是一个滑动窗口取最值的问题,直接单调队列优化即可。
说人话
首先设 fr [ i ][ j ] = dp [ i-1 ][ j * w [ i ] + r ],按照 j % w [ i ] 的结果进行分类,也就是把所有 j % w [ i ] == r 的 dp [ i ][ j ] 的值都提出来;
考虑 f 数组的转移,fr [ i ][ j ] = max { fr [ i-1 ][ j-k ] + v [ i ] * k ( 0 <= k <= t [ i ] };
就是枚举选了几个物品 i,k 就是选的物品i的数量,现在考虑 fr [ i ][ j ],枚举选了 k 个,那原来就是 fr [ i ][ j-k ],用 dp 数组表示就是 dp [ i-1 ][ ( j-k ) * w [ i ] + r ] + v [ i ] * k;
然后对原来的方程进行神仙化简:
fr [ i ][ j ] = max { fr [ i-1 ][ p ] + v [ i ] * ( j-p ) } ( j >= p >= j - t [ i ] );
就是用 p 来代替 j-k;
然后我们发现 v [ i ] * ( j-p ) 可以拆开,就成了这个样子:
fr [ i ][ j ] = max { fr [ i-1 ][ p ] - v [ i ] * p } + v [ i ] * j ( j >= p >= j - t [ i ] );
发现此时 fr [ i ][ j ] 只与 p 有关,与 j 无关,并且看 p 的取值范围:
所以我们可以用单调队列来维护最大值:max { fr [ i-1 ][ p ] - v [ i ] * p },最后再加上 v [ i ] * j 就好了。
单调队列均摊是 O(m),总复杂度就是 O(n * m)的了。
分组背包
其实都是一样的。
代码实现:
此外还有二维背包问题,实质上和本身和普通背包毫无思维上的区别。
因为限制条件多了一个,所以我们需要给最初最基本的 dp 多加一维状态。
设 dp [ i ][ x ][ y ] 表示前 i 个物品,消耗了 x 的氧气和 y 的氢气所能得到的最大收益是多少。
然后考虑一个物品选还是不选即可。
对于一般的背包问题
做背包问题最关键的就是找清楚并反问自己?
这题里面 什么是容量? 什么是物品? 什么是物品的费用? 什么是物品的价值?
容量,就是这题当中我们怎样表示状态的数组。
费用,就是用来 f [ i ] ----> f [ i+v [ k ] ],状态转移的跨度。
价值,就是你这个dp的数组,所维护的东西。维护的数值!
背包 dp 一定要理解好这三点,因为很多时候题目中的 “ 费用 ” 并非背包 dp 中的 “ 费用 ”。
还是要先明确,对于一道背包dp的题目来说,我们需要有容量,物品, 费用,价值(权值,因为有些题要求最小)。
本题中:求给所有的人安排房间的最小支出是多少?那么在这里,几个 人对应就是dp的数组下标,每个房间就是一个物品,房间支出就是物品 的权值。 虽然这里看上去房间支出是花费,是作为数组下标的存在,实 际上是作为我们要求的东西,是dp数组存的内容。
当然肯定不是这么简单就完了的。
观察一个性质
首先要观察出题目的一个小性质,即如果有两对 (或以上) 夫妻住在一起的话,那么交换之后结果不会变差。因为首先这两个房间的容量至少为 2, 如果男男在一个房间,女女在一个房间此时花费不变,有一个房间容量大于 2 的时候,就还可以再入住其他人。这样结果变得更优了。 综上,要 么至多存在 1 对夫妻住在一起,要么不存在夫妻住在一起。
f [ i , j , k , 0 ] 表示前 i 个房间住 j 名男性 k 名女性并且没有夫妇住在一起的最小花 费;
f [ i , j , k , 1 ] 表示前 i 个房间住 j 名男性 k 名女性并且有一对夫妇住在一起的最小花费;
f [ i , j , k , 0 ] = min ( f [ i-1 , j , k , 0 ] , f [ i-1 , j -v [ i ] , k , 0 ] + p [ i ] , f [ i-1 , j , k -v [ i ] , 0 ] + p [ i ] ) ;
f [ i , j , k , 1 ] = min ( f [ i-1 , j , k , 1 ] , f [ i-1 , j -v [ i ] , k , 1 ] + p [ i ] , f [ i-1 , j , k -v [ i ] , 1 ] + p [ i ] , f [ i-1 , j -1 , k-1 , 0 ] + p [ i ] );
一类牛逼的套路题
看到 w 很大,看似不可做。
但是这题肯定能做啊!我们就找有什么特殊的约束条件。
w = a * 2b,我们发现 a,b 都不大,就启发我们用 2b 分组。
将物品按 b 值从大到小排序分阶段处理,在阶段内 b 值都相同,直接忽略不足 2b 的部分,f [ i ][ j ] 表示前 i 个物品,剩余的能用重量为 j * 2b 的最大价值。
从上一阶段到下一阶段时,将 f [ i ][ j ] -> f ’ [ i ][ j * 2 + 零碎部分 ],注意到 n = 100, a <= 10,所以剩余重量最多纪录到 1000 即可。
复杂度O(n * 1000);
几个背包变种和模型
分治+背包 ◦
初始 solve ( 1 , n );
递归的函数到 Solve ( l , r ) ,维护的 dp 数组,记录的是除去 [ l , r ] 外的物品的构成的背包数组。 Solve ( l , mid ) 时,把 [ mid+1 , r ] 内的物品加入 dp 数组。
我们这里定义的加入这个物品 u,就是多考虑上这个物品之后构成的 dp 数组。 若是 0/1 背包的加入也就是做以下这个操作。
For ( int i = n ; i >= w [ u ] ; i -- ) dp [ i ] = max ( dp [ i ] , dp [ i-w [ u ] ] + v [ u ] );
当 l=r 时,将对应所有的询问在 dp 数组查询即可。
单调队列优化的话,复杂度 O ( n * m * log ( n ) ) ,每个物品被加进去 log 次,每次 O ( m ) ;
代码实现:
Insert ( dp , i ) : 是在 dp 数组当中加入 i 号物品。
设 f [ x ] 表示全部物品都可用恰好装满 x 体积时的方案数,可以用 01 背包算法求出。这是总方案数。
然后考虑不选某物品的情况。
设 g [ x ] 为不选当前物品恰好装满 x 体积时的方案数。
当 x 小于 w [ i ] 时,i 物品一定不会被选上 g [ i ] = f [ i ] 。
当 x 大于等于 w [ i ] 时,i 物品可能会被选上,直接求不选的情况比较困难。
总方案数及 f [ x ],不选的方案数可以想为先不选 i 再最后把 i 选上,即用总的方案数减去选上 i 的方案数 。
所以 g [ x ] = f [ x ] - g [ x-w [ i ] ],为什么 g [ x - w [ i ] ] 表示选上 i 的方案数呢?
g [ x - w [ i ] ] 是不选 i 刚好装满 x - w [ i ] 的方案数,加上 w [ i ] 不就是选 i 刚好装满 x 的方案数了嘛?
x 从小到大枚举计算 g 即可。
每次都是线性复杂度,一共 n 次计算,总复杂度是 O ( n * m ) 。
另外两种理解方式:
1:也可以用生成函数来理解。
2:直接考虑程序代码也可以很简单的理解。
对比前两道题
第一道是求最优值,第二道是求方案数。
方案数是可以取逆的,但是最优值是无减法定义的。
所以第一道不能用第二道的方法也就是不能把复杂度降到 O ( n * m ) 。
可行性的背包问题
我们设总体积为 sum,那么我们必定会存在一堆体积和是小于等于 sum/2 的,然后就是要求去选出一些物品总体积小于等于 sum/2,最大能取到的体积是多少。
这就是经典的背包问题了。
同时可行性的背包问题可以 bitset 优化。
整数划分性质
一类整数划分的问题:
1:求把 n 划分成 k 个正整数的方案数?
2:求把 n 划分成互不相同 k 个正整数的方案数?
3:求把 n 划分成 k 个不大于 m 的互不相同正整数的方案数?
4:求把 n 划分成 k 个奇数的方案数?
………………………………………………
如果 n , k 和 m 是同阶的,我们看看最优能做到多少的复杂度。
求把 n 划分成 k 个正整数的方案数:暴力 dp
设 dp [ i ][ j ][ sum ] 前 i 个数,选了 j 个数,和为 sum 的方案数是多少,答案就是 dp [ n ][ k ][ n ],考虑这一个数选几个来转移即可。这个状态是 n * k * n 的转移 O( sum/i )。均摊 O( n * k * n * log ( n ) ) 复杂度有些高。
本质是个完全背包的,那种写法的话可以做到 O ( n * k * n ) 。
我们直接设 dp [ i ][ j ] 表示把 i 划分成 j 个数的方案数。
我们考虑分成的这几个数有没有 1:
如果有 1,我们就将 1 删除,那么就成了 dp [ i-1 ][ j-1 ] ;
如果没有 1,我们将每个数减去 1,这样的话数的个数还是不变,那么就是 dp [ i- j ][ j ];
我们可以得到 dp [ i ][ j ] = dp [ i-j ][ j ] + dp [ i-1 ][ j-1 ] 。以下是 dp [ 22 ][ 4 ] 的两类情况: 考虑有没有 1 的部分。
我们考虑数形结合来理解。
考虑去掉蓝色的部分。
左图代表:dp [ i-j ][ j ] ,右图代表:dp [ i-1 ][ j-1 ]:
求把 n 划分成互不相同 k 个正整数的方案数:暴力
大同小异。
暴力 dp:设 dp [ i ][ j ][ sum ] 前 i 个数,选了 j 个数,和为 sum 的方案数是多少,答 案就是 dp [ n ][ k ][ n ],考虑下一个数选不选来转移即可。
本质是个 0/1 背包, 复杂度 O ( n * k * n );
正解也是大同小异。
我们还是直接设 dp [ i ][ j ] 表示把 i 划分成 j 个数的方案数。
我们可以得到 dp [ i ][ j ] = dp [ i-j ][ j ] + dp [ i-j ][ j-1 ] 。考虑去掉蓝色的部分。
左图代表:dp [ i-j ][ j ] ,右图代表:dp [ i-j ][ j-1 ] ;
与上一题不同这里第一维是 [ i-j ],因为要限制没有相同的数字。
还是考虑有没有 1:
如果没有 1,我们还是像之前一样直接把每个数都减 1,即 dp [ i-j ][ j ];
如果有 1,如果我们还是仅仅将 1 删除的话,那么剩下的 i-1 还是会分出 1 来,这就与 “ 分成不相同的数 ” 相违背,且剩下多少个 1 我们也不知道,即第二维不一定是 j-1;既然我们知道了每个数只出现了一次,那么分成的数中只有一个 1,所以我们可以还是将每个数减 1,这样的话一定只会删去一个数,那么就是 dp [ i-j ][ j-1 ];
这里复杂度比上题更低!!
求把n划分成k个不大于m的互不相同正整数的方案数。
背包 dp 的方法同上。
N = 20 , k = 4 , m = 6 的不合法方案如图。
若有一项超过了 m (最小为 m+1),那么剩下的数( i - ( m+1 ) ) 无论怎么分也不满足条件了,所以要减掉。
dp [ i ][ j ]= dp [ i-j ][ j ] + dp [ i-j ][ j-1 ] - dp [ i - ( m+1 ) ] [j-1 ]
减的这一项就是超过线的那种情况。
其实就是 -n ~ n 中求选 k 个不同的数,和为 0 的方案数。
显然求出来 f [ i ][ j ] 表示选出 j 个数和为 i 的方案数,然后枚举其中一端拿走几个a,以及拿走的数的重量之和 x,把 f [ x ][ a ] * f [ x ][ k-a ] 累加之和就是最后的答案了。
这里 j 个数是互不相同的,也就转化成了我们的 “ 把 n 划分成互不相同 k 个正整数的方案数 ”
算f的复杂度和统计答案的复杂度均是 O ( n * k * k ) 。
由此类问题对状态转移的一点感触
dp 问题中,转移就是分情况讨论,每种情况对应一个方案数或最优值, 而这个方案数或最优值可以表示为之前已经求出来的 dp 值的组合。
只不过分情况讨论可能方法很多,以一种方式讨论能转化为已知的 dp 值的叠加,另一种方式也可以。我们需要保证的是:讨论不漏掉任何情况, 像计数问题也不能出现方案重叠(求 max 和 min 其实是可以重叠的),同时选择分类项数尽量少的方案,以便得到更优的复杂度。
上面几道题的转移方式可能没有原先一些问题的分类方式直观,但是也的确满足了不重不漏尽量简洁的条件。当然这可能也并非是唯一的转移方式,只要保证能划归到之前已经求出的 dp 值就行。
1:分类讨论要不重不漏。
2:讨论时要保证划归到的 dp 状态已经求出来,而不是还未求的。
以上保证正确性。
实际上能保证以上两点,无论怎样的转移方式都是可行的。
3:尽量分的类少一些,转移更快速。
这是关于复杂度。
总结
0/1背包;
完全背包;
多重背包以及其两种优化方式;
如何处理强制不能选一个物品的背包问题,几种不同的方法;
从高位到低位做dp,处理高位时暂时忽略低位的常见技巧;
可行性的背包问题可以 bitset 优化;
背包的费用可以是多维的;
整数划分模型 ;
此外:还有依赖关系的背包dp,例如取某个物品必须先取另一个,我 将之后结合树形dp来进行讲解。
数位 dp
经典的数位 dp 是要求统计符合限制的数字的个数。
一般的形式是:求区间 [ n , m ] 满足限制 f ( 1 ) 、f ( 2 ) 、f ( 3 ) 等等的数字的数量是多少。条件 f ( i ) 一般与数的大小无关,而与数的组成有关。
善用不同进制来处理,一般问题都是 10 进制和二进制的数位 dp。
数位 dp 的部分一般都是很套路的,但是有些题目在数位 dp 外面套了一个 华丽的外衣,有时我们难以看出来。
HDU3652简化版
暴力的去枚举每一个数然后去计算必然太慢。
我们先来考虑一个更简单的形式:统计区间 [1 , n ] 中含有 '3' 的数字有多少个。
N = x1 x2 x3 x4….. xtotal 。 xi 为 n 的从高到低第 i 位是多少。Total是总的位数。
如果我们考虑从高到低位不断填数 y1 y2 … 。那么问题其实就是问有多少填数的方案,一要满足上限的限制(对应区间 [ 1 , n ] ),二要满足题目的其他限制。
这样其实就比 [ 1 , n ] 看起来更能 dp 了。
假设到了第 k 位 yk != xk,则 k 位之后就没有上限的限制了,情况就简化了。
如果前面 y 中没有出现 3:那么假如我们可以求出来,f [ k ][ 0 ] 表示 k 位之后没有上限限制(随意填),但是必须填个 3(前面没有出现),有多少种填数的方案。
如果前面 y 中出现了 3:那么假如我们可以求出来,f [ k ][ 1 ] 表示 k 位之后没有上限限制(随意填),没有必须出现 3 的限制(前面出现过了),有多少种填数的方案。
首先我们可以枚举到哪一位 yk != xk,然后再枚举这一位是多少,把对应的 F 加起来就是答案了,一共需要加位数 * 10 次。这运算次数是不大的。
而 f 数组总大小也很小,位数 * 2。
边界 f [ total + 1 ][ 0 ] = 0,f [ total + 1 ][ 1 ] = 1,转移复杂度 O ( 10 )
总复杂度为:
那回归到原题呢?
枚举哪一位不同没什么变化吧,跟原先一样枚举就好了。
就是 f 数组要变,因为约束条件更多了,所以状态的维数要增加。
设 f [ k ][ 前面是否已经出现 13 ][ 上一位是否是1 ][ 前面的那些数 mod 13 等于多少 ],转移的话同样还是枚举这一位是填什么即可。
用记忆化搜索来实现:
i 表示到了第几位; State:上一位是否为 1 ;Have:是否已经有 13; K:已经加上的数 mod13 的值;
注意只有不顶到上界才能记忆化下来答案。
关于数位dp的经验:
1:注意很多时候带进去是 n==0 要特殊处理。
2:还有一般问 [ m , n ],我们求 [ 1 , n ] - [ 1 , m-1 ] ,但是有的时候 m 为 0 就炸了。然后一道题 wa 一个小时。。。。。。正常。。。。。
3:求所有包含49的数,其实就是(总数 - 所有不包含 49 的数)。前者的话需要有两维限制,一个是上一位是什么,一个是之前有没有49。但是后者只需要记一个上一位是什么。就能好写一些。
4:一般问题的数位 dp 部分,都是套路,但是这并不代表它外面 “ 华丽的外衣 ” 和与其他算法结合的的部分也是无脑的。要看出它是考数位 dp, 要看出问题怎么变化一下就是数位 dp 了。
5:dp 初始化 memset 要置为 -1。不能置为0!!!!!!因为有很多时候 dp 值就应该是 0,然后我们如果误以为是因为之前没有计算,从新计算的话,就会tle。
这里不能写成 0;
6:既然是记忆化搜索,那就可以剪枝!可行性剪枝!
7:注意 windy 数的情况,有时前导 0 也需要记的!
数位 dp 当然可以和各种算法套起来。
这题就是典型的枚举+数位dp。
枚举啥?
QAQ:“ 还用问???”
首先,感觉这道题如果记录 当前位、选没选平衡轴、当前左边平衡干权值减右边平衡杆权值,这样感觉并不好转移。而分析题目性质可以发现, 一个非 0 数只能会有一个(一一对应)平衡轴(0 除外,最后特殊处理一 下就好),那么如果数位 dp 外面枚举平衡轴的话,只需计算到最后差是否为 0 就好。因为每一种中心轴对应的合法集合互不相交。
注意0的特殊情况:就是0被统计了位数次,减掉即可。
虽然输入的 10 进制数,但是本质有影响的是二进制形态啊!
怎么来转换一下,求 1 ~ n 中每个数的一的个数总相乘之积,首先感觉到, 每个数都会有唯一对应的 1 的个数,且一的个数的取值不到 60,因为 n 最大 1015, 那么我就想,如果枚举 1 的个数 k,计算有多少个数含有 k 个1, (因为数位 dp 就是来做,有多少满足的数,且不关注数的大小)这样就转化为数位 dp 的模型了。另外,发现含有 k 个 1 的数个数可能非常多,快速幂搞一搞啦。
这题的关键就是发现一的个数的情况比较少可以枚举再转化为另一种情况计算其实,这题本质就是转化一下,注意在模型难以建立的情况下, 通过转化,可以将题目简化。
dfs ( i , same , last , appear , occur8 , occur4 , limit )
Same:上一位和上上一位是否相同 ;
Last:上一位数字 ;
Appear:连续三个相同是否出现过;
Occur4:4是否出现过;
Occur8:8是否出现过。
limit:是否顶上界;
转移的话差不多一样的。
总结
10 进制数位 dp 的基本最简单的形式。
记忆化搜索处理数位 dp 的代码实现,数位 dp 一般都用记忆化搜索来做。
考察思维的数位 dp 往往会和其他如枚举算法结合,或作为原问题的子问题。
除了十进制,二进制的数位 dp 也是常见的,此外 K 进制的也是可以的。
基础树形 dp
1:与树或图的生成树相关的动态规划。
2:以每棵子树为子结构,在父亲节点合并,注意树具有天然的子结构。 这是很优美的很利于 dp的。
3:巧妙利用 bfs 或 dfs 序,可以优化问题,或得到好的解决方法。
4:可以与树上的数据结构相结合。
5:树形 dp 的时间复杂度要认真计算,部分问题可以均摊复杂度分析。
6:一般设 f [ u ] 表示 u 子树的最优价值或者是说方案数,或者 f [ u ][ k ] 表示 u 子树附加信息为 k 的最优值,往往是通过考虑子树根节点 的情况进行转移。
7:树形dp,在树结构上,求最优值,求方案等 dp 问题,就可以考虑是树形 dp。
当然也有可能是点分治或者是分析性质的贪心题。但是树形 dp 绝对是一 个很好的思考方向。
dp [ i ][ 0/1 ] 表示做完了 i 的子树,i 点是否选的最大独立集,即可直接转移。
如果一个结点在独立集里,那么它的儿子都不能选,因为选了就不能满足独立集的性质了;
如果一个结点不在独立集里,那么它的儿子可选可不选,选了的话因为父亲不在独立集里也符合条件,两种情况取较大值即可;
代码还是很好写的。
1:设 f [ i ] 表示i这个点到子树里面的最远点是多远的,然后对于每一个点 u 求出求出以这个点为根的最远路径距离,直接找 { f [ soni ] + edgei } 的前两大值加起来即可。然后再在每一个点构成的答案里面取最大值就是全局的最优值了。
2:随便找一个点bfs求它的最远点,设为 x,再从 x 跑一遍 bfs,求 x 最远点 y, 则 ( x , y ) 就是一个直径了。
考虑离每一个点最远的点肯定是直径的其中一个端点。
考虑树形 DP,设 f ( x ) 为以 x 为根的子树内选取不相交树链的价值和的最大值, 枚举一条 LCA 为 x 的链 ( u , v , w ),那么当前方案的价值为 w + 去除 u 到 v 路径上的点后深度最小的点的 f 的和 。
复杂度 O ( M * N );
树链剖分优化可以做到 O ( M * log ( n )2) ;
f [ i ][ 0 ] ,f [ i ][ 1 ],f [ i ][ 2 ] 分别表示根是绿红蓝三种颜色时的最多/最少绿色的数量,转移的时候只要保证上面的约束就行,并不难。
f [ u ][ 0/1 ][ 0/1 ] :第 u 个点自己亮不亮,自己按不按,子树里面所有的点都亮的最小次数;
辅助数组 g [ 0/1 ] 表示按偶数 / 奇数次的方案;
状态转移方程:
如果当前点 u 不亮,且自己没有按得话,它的孩子要按偶数次:
f [ u ][ 0 ][ 0 ] = f [ v ][ 1 ][ g [ 0 ] ];
如果当前点 u 不亮,且自己按得话,它的孩子要按奇数次:
f [ u ][ 0 ][ 1 ] = f [ v ][ 1 ][ g [ 0 ] ];
如果当前点亮了,且自己没有按得话,它的孩子要按奇数次:
f [ u ][ 1 ][ 0 ] = f [ v ][ 1 ][ g [ 1 ] ];
如果当前点亮了,且自己按得话,它的孩子要按偶数次:
f [ u ][ 1 ][ 1 ] = f [ v ][ 1 ][ g [ 0 ] ];
上面几种情况取个 min 就好了。
树上背包
f [ i ][ j ] 表示在以 i 为根子树中选择,i 强制选择,选择 j 个点的最大价值,转移 时每次将一个孩子暴力合并到父亲上,合并就枚举这个孩子内部选择了多少点即可。
F [ i ][ j ] = max { f [ i ][ j-k ] + f [ son ][ k ] | k = 0… ( j-1 ) },就是枚举 son 内选了多少点。
我们按照一般的分析复杂度的方式的话是:状态数 N2 * 转移复杂度 N,总复杂度是 O ( N3 )。
实际上我们考虑每次合并的时候相当于是一组点对数量的复杂度,总的 来看的话就是 n 个点点对的数量,均摊复杂度 O ( N2 )。
考虑边的贡献:两端黑点数量的乘积 + 两端白点数量的乘积。
定义状态 f [ i ][ j ] 表示 i 号节点为根节点的子树里面有 j 个黑色节点时最大的贡献值。
然后我们要知道的就是 i 到其父亲这条边会计算次数就是:子树中白色节点数 ∗ 子树外白色节点数 + 子树中黑色节点数 ∗ 子树外黑色节点数。
这条边的贡献和 i 的各个孩子的子树内各有多少黑点是无关的,所以我们可以做背包求出来每个点子树内有 j 个黑点时贡献和是多少。
代码如下:(红线部分必须按图示这个写法,保证 N2 的均摊复杂度分析)
最朴素的做法
这里不是选点的数量而是重量,所以这里的朴素做法是 O ( n3 );
f [ i ][ j ] 表示在以 i 为根子树中选择,i 强制选择,重量为j的最大价值,转移时每次将一个孩子暴力合并到父亲上,合并就枚举这个孩子内部选择了多 少的重量即可。
F [ i ][ j ] = max { f [ i ][ j-k ] + f [ son ][ k ] | k = 0 … ( j-1 ) },就是枚举 son 内用了多少重量。
注意我们这里两个一维数组的背包合并是 n2 的,所以慢。
但一个一维数组和一个单独的物品合并是线性的。
DFS序上做DP
在 dfs 序上 dp,如果不选一个点,则跳过代表他的子树的 dfs 上连续一段。
设 f [ i ][ j ] 表示 dfs 序上第 i 个点到第 n 个点,选了 j 的重量得到的最大的价值是 多少。i 可以选也可以不选。不选的话就要跳过整个子树。
设 T [ i ] 表示 dfs 序为 i 的点标号。
不选:f [ i + size [ T [ i ] ] ][ j ],选:f [ i+1 ][ j - w [ T [ i ] ] ] + v [ T [ i ] ];
两种情况取最大值即可。
另一个奇妙的方法
不是每次将孩子与自己合并,我们直接把dp数组复制再传给孩子,再从 孩子递归下去,最后原来自己的 dp 数组和孩子的 dp 数组直接在对应重量的价值中取 max。
以下是步骤:
我们现在在 u 节点,对 u 节点的 dp 数组中加入 u 点的物品。
做 dp [ i ] = dp [ i - w [ u ] ] + v [ u ] 操作,表示强制加入了 u 这个物品。
Dpson数组 = dp数组。
递归计算 son 的 dp 值,传入的参数是 dpson 数组。
回溯回 u 点,对每一个 i 做 dp [ i ] = max { dpson [ i ] , dp [ i ] };
树形 dp + 记忆化搜索。
设 f [ l ][ r ] 表示根节点为 [ l , r ] 的线段树,匹配选择根节点的最大匹配&方案数,g [ l ][ r ] 表示根节点为 [ l , r ] 的线段树,匹配不选择根节点的最大匹配 & 方案数。那么这是一个很普通的树形dp。
注意到区间长度相等的线段树的结果是一样的,且每层至多有两种区间长度不同的区间 ( 打表或者推推式子都行 ),因此直接以区间长度为状态进行记忆化搜索即可。
基环树
基环树,也是环套树,简单地讲就是树上再加一条边。
如果把那个环视为中心,可看成:有一个环,环上每个点都有一棵子树的形式。
因此,对基环树的处理两部分分别为对树处理和对环处理。
基环树问题处理方法
处理方法有:
1:断开环上一条边,枚举边端点的情况,然后当树来处理。
2:先把环上挂着的树的信息都计算完,然后转化为序列问题,或者说是 环形的序列问题。
dfs找环
基环树,环是关键,所以做这类题 目我们首先得找到环。
找环的方式很多,这里讲解 dfs 找环。
对于dfs找环,我们就对这个基环树 做 dfs 遍历。我们知道对于一个在图, 它 dfs 树上的每一个返祖边 (v - > u), 和 dfs 树上构成的路径就会构成一个环。也就是我们只需要找到这个返祖边即可。
dfs找环
主函数调用时,要枚举每一个点。
因为有可能是个基环树森林。
这是很容易犯的一个坑:n 个点 n 条边不一定是个基环树,准确来讲是基环树森林!!
如果说我们要采用断开一条边,当成树来处理。我们不需要找出来整个环,只需要找一个在环上的边, 按下图写法会简便很多。
基环树内向
首先它是一个有向图,它构成类似基环树的结构,有一个特点是每个点 都有且只有一个出度,并且环外的节点方向指向环内。
如果题目说满足每一个点都有一个唯一出度,则本质上就是给了我们一个基环内向树森林(不只是一个基环内向树!!!!)
任何一个点沿着唯一出边走都会走到环上,利用这个性质可以随便选一个点再通过一 个简单循环找到基环树的环。
把这个讨厌关系的图画出来,就是个基环内向树森林,然后我们要求最大权独立集。
求最大独立集内向和外向和无向图毫无区别,都是相邻的不能选。
这里的基环树上有且仅有一个环,就是从任意环上一条边 ( u , v ) 断开环,分两种情况,一种强制不选 u ,跑一遍树形 dp;一种强制不选 v,跑一遍树形 dp,两种情况取最大值。 转化成树的话,就是那个简单的树形 dp。
找环 dfs 找就好,或者从一个点顺着父亲一直走直到走到一个曾经走到过的点就找到一个环了。
先找出环,很明显答案有两种情况。
1:在以一个环上节点为根的向外的树的直径。
2:以两个环上节点分别为根的最大深度再加上两个节点在环上距离。
第一种情况就是之前讲的树形 dp。
第二种情况要处理出以环上每个节点为根的最大深度 d [ i ],环上的点重标号,选环上 1 号点作为基准点,求出 s [ i ] 表示 i 号点到 1 号点的距离,sum 为总的环长。设我们找的两个环上节点是 i , j 则 max ( min { s [ i ] - s [ j ] , sum - ( s [ i ] - s [ j ] } + d [ i ] + d [ j ] ) 即为所求,但如果暴力求是 n2 的。并没有比最开始直接枚举快多少。
考虑优化。
我们考虑把内部的一个 min 去掉,式子能看起来更清晰一些。
max ( min { s [ i ] - s [ j ] , sum - ( s [ i ] - s [ j ] ) } + d [ i ] + d [ j ] ) ;
Max ( 1: s [ i ] - s [ j ] + d [ i ] + d [ j ] | s [ j ] >= s [ i ] - sum / 2, 2 : sum - s [ i ] + s [ j ] + d [ i ] + d [ j ] | s [ j ] < s [ i ] - sum / 2 );
考虑枚举选的两个点的后一个点 i,然后求对于 i,离 i 最远的 j 距离是多少,然后对于每一个i的答案求最大值就是整个基环树的直径了。
第一种情况 s [ j ] >= s [ i ] - sum / 2 :求 d [ j ] - s [ j ] 的最大值即可,注意可选的j区间会移动, 所以这里需要单调队列。
第二种情况 s [ j ] < s [ i ] - sum / 2:这个可行区间只会变大,不会缩小,所以直接记录 s [ j ] + d [ j ] 的最大值即可。
总结
一般模式的树形背包dp,求直径求最大独立集等,以及几道经典题目强 化理解。
树上背包问题,一种经典且常用的均摊复杂度分析的方式,很多时候一 道看似复杂度超的题目就因此复杂度正确了。
dfs 序上树上背包或者传数组做树上背包的两个 O ( n * m ) 的做法。
有关基环树 / 基环内向 / 基环外向树的理解。
基环树两个处理方法,断环为树,或处理树上信息后转化成环形的序列问题。