本题是典型的树上背包,正确的算法复杂度是O(nk),其中n是树的大小,k是可选的物品数量。subtask1是用来卡O(n3)的错误写法的(不开O2的情况下)。在我正确通过了subtask1之后,我终于想通了为啥以及如何正确地做上下界优化,特发此贴以帮助后面卡在这里的人思考。
正如很多别的复杂度分析的文章指出,只要单个子树的算法复杂度是O(n2)的,那么在dp数组限制上界到k之后,整体算法复杂度就是O(nk)。这一点不再赘述。本贴核心在于怎么控制子树复杂度到O(n2)。
暂且不考虑dp数组上界控制到k的优化。每个子树的dp数组上界开到ni,即该子树大小;于是,n=∑i=1kni。设对大小为n的树复杂度是T(n),那么应该有T(n)=∑i=1kT(ni)+M(n1,⋯,nk),其中M(⋅)是合并步的复杂度。
一种(可能)正确的写法如下。
int sz = 1;
for (const auto& [nt, w]: gr[cr]) {
if (nt==pa) continue;
auto [cnt, ndp] = dfs(dfs, nt, cr);
for (int i = min(sz+cnt, k); i>=0; --i) {
for (int j = max(0, i-min(sz, k)); j<cnt && j<=i; ++j) {
dp[i] = max(dp[i], dp[i-j]+ndp[j]+1LL*w*(j*(k-j)+(cnt-j)*(n-cnt-k+j)));
}
}
}
为什么这样是对的?因为(继续忽略k)我们需要把循环控制在{(i,j):0≤i≤sz+cnt,max{0,i−sz}≤j≤min{cnt,i}}内,这是一个平行四边形(请自行画图),所以总的大小是(1+sz)(1+cnt)≤(1+ni)(1+∑j=1i−1nj)。适当牺牲严谨性但便于表达,扔掉括号里的1,这样只会扩大后面大O里的系数。那么,可归纳证明T(n)≤n2。因为,T(n)≤∑i=1kT(ni)+∑i=1kni∑j=1i−1nj<=∑i=1kni2+2∑i=1k∑j=1i−1ninj=n2。在加上一堆一次项和常数项后,不会改变这个阶的判断。
一个常见的错误写法如下:
int sz = 1;
for (const auto& [nt, w]: gr[cr]) {
if (nt==pa) continue;
auto [cnt, ndp] = dfs(dfs, nt, cr);
for (int i = min(sz+cnt, k); i>=0; --i) {
for (int j = 0; j<cnt && j<=i; ++j) {
dp[i] = max(dp[i], dp[i-j]+ndp[j]+1LL*w*(j*(k-j)+(cnt-j)*(n-cnt-k+j)));
}
}
}
它丢掉了j的下界控制。这样会给上面的平行四边形加个小三角,变成一个梯形。这个小小的三角形看似并没有改变合并步的复杂度,但是确实给整体复杂度提升了一个阶。此时算法复杂度大致是T(n)=∑i=1kT(ni)+n2/2。如果树大致是平衡的,那么可以参考主定理,整体算法仍然是O(n2)的。但是,对于极度不平衡的树,比如一个链,算法复杂度将满足T(n)=T(n−1)+T(1)+n2/2,此时必然有T(n)=Θ(n3),将完全无法接受。
所以请控制好上下界。