先拿一道题(糖果分发)做引子(by Ly)
大意:给你n个物品,每个物品有个重量,你可以取m次,每次取出的物品重量之和不超过一个给定的数k,而且每次你只能在一段区间内选择,每次选的区间不能相交,求最多能取到的物品数。需要注意的是,题目中要求你在“一段区间内”取,但并不要求你取走这个区间内的所有物品
数据范围:1<=n<=2000,1<=m<=50,1<=k<=10000
分析:
算法显然是dp,首先容易得到一个O(n*m*k)的方程,但显然是过不了的
上面那个方程其实很废,记录了大量的冗余状态(尽管背包类题目大都有状态分布稀疏的特点)
当然,我们可以dfs预处理出有用的状态再做,这样的确能快不少,但不是本文重点
为什么硬要将当前背包剩余重量和取的次数加到状态中?仔细考虑这个问题,发现背包当前容量和已经取了几次其实并不会造成多大的后效性,问题要求的“第一关键字”是我们用掉的背包的个数(就是取了几次啦)
算法:
记f[i,j]表示前i个物品中,选了j个物品,最少的取的次数,g[i,j]表示在f[i,j]最少的条件下,最后一次取了多重
考虑第i个物品时,用f[ii,j]+取第i个物品来转移,更新f[ii+1,j+1],如果g[ii,j]+w[i]没超过k,则给g[ii,j]加上w[i],否则给f[ii,j]+1
最后ans=j|max{f[n,j]<=m}
时间复杂度就降到了O(n^2),完美解决了这道题
小结:
这个解法实际上是把原方程要记的值表示到了状态中,把原状态表示到了要记的值里去,由于题目中要求的数的特殊性,这样做是正确的
这里用到了一个方法,将原dp方程稀疏的状态表示到新方程记的值里去,而把原方程记的值表示到新方程的状态中来
再看另外一道题:
(集装箱)
大意:给你n个物品,要求你按顺序将这n个物品放入集装箱中,你只能在两个打开着的集装箱中做选择,放入任意一个中,集装箱的容积相同,满了你就可以把这个封装,同时开一个新的,要求使用尽量少的集装箱
数据范围:n<=2000,集装箱容积L<=2000
分析:
首先容易得到一个O(n*L^2)的dp方程,f[i,j,k]表示前i个物品,第一个箱子装了j,第二个箱子装了k时最少所用集装箱数
同样是稀疏的状态分布,那我们就可以考虑上题类似的优化方法了,将一维状态表示到值中
算法:
记f[i,j]表示前i个物品,第一个箱子装了j所用的最少集装箱数,g[i,j]表示在f[i,j]的情形下,第二个集装箱已经用了的最小容积
转移跟上一题类似,也是考虑每个物品装在哪个箱子里面,超过容量限制就给f[i,j]+1
总结:
我们可以从这两道题中看出,在有效状态分布非常稀疏的dp方程中,有时候是可以将缩掉一维状态,或是将结果表示到状态中,状态记到结果里去,从而达到优化方程的目的。
值得一提的是,可以这样优化的方程通常都是要求类似“使用尽量少的背包装下一些物品”之类的东西,结果是连续的而且一般很小,状态则是零散的,而且每次转移答案最多+1
优化算法,要从问题的本质特点出发,充分挖掘、利用性质,才能达到一个好的效果
附关键代码
第一题:
第二题:
第二题std用的是dfs预处理状态,我yy出了这个怪怪的算法一不小心虐了std,爽