题解:P3023 [USACO11OPEN] Soldering G
P3023 [USACO11OPEN] Soldering G
提供一篇复杂度正确且没有被 hack 原版官方解法。
做法解释是本人从英语题解翻译而来,为效果有删减增添。代码同上,为易懂做了小改。
经 @WZRYWZWY 提议,二编给出官方题解。
解法说明:
考虑使用动态规划。 选择点(最优的)作为根,并考虑在根处焊接电线连边(以下称该边为切割线)成为树。 这样会有两种情况,分别是切割线竖穿当前点(连接父节点和子节点)和切割线横穿当前点(连接两个子节点)。 成本就为切割线的长度与所有其他线的总成本,且切割线的长度取决于其余焊接的边。
这里给出一个相对简单的动态规划解决方案:
对于当前点,如何计算最小成本?
我们从下到上计算,如果为叶子节点,则将其切割线视为长度为
为了进一步减少运行时间,必须注意成本函数的凸性特性。
设当前子树的长度
我们可以从所有子树中更新值,同时使用启发式算法加快时间。
存储所有解添加到数据结构,这些解的长度和成本都会随着合并子树而变化。
对于二分搜索节点的操作总数,最多是每个子树节点的总和(由于凸包所以可能比子树少),时间为
总共时间复杂度为
代码:
请结合代码注释和以上说明理解,有问题欢迎指出。
#include<bits/stdc++.h> //by: hansang
using namespace std;
typedef long long LL;
const int N=5e4+10;
const LL inf=1e9;
LL dep[N], dp[N]; int n, p[N];
//dp[i]为i子树的最小成本但缺少切割线有关的部分值(看calc函数和主函数注释),p数组表示偏移(启发式)
struct node{
LL d, c, f; //层数,当前点切割线有关花费,特殊值(后面会解释)
bool tag; //0表示切割线竖穿当前点(连接父节点和子节点),1表示切割线横穿当前点(连接两个子节点)
};
vector<int> G[N]; set<node> b[N];
bool operator<(node a, node b) {
if(a.tag || b.tag) return (a.f<b.f); //结合主函数的注释理解
if(a.d!=b.d) return (a.d>b.d); //非查找情况下
return (a.c<b.c);
}
LL sqe(LL x) {return x*x;}
LL calc(node n1, node n2){ //这个函数是求出特殊值f(可以理解为斜率,那我们维护的就是一个下凸壳)
if(n1.d>n2.d) swap(n1, n2);
//这个函数面向dep较小的,联系79,100和127行自行理解
LL res=(sqe(n1.d)+n1.c-sqe(n2.d)-n2.c)/(2*(n2.d-n1.d));
//L^2+2*L*l+(l^2+c),目前只有l和c确定,那么就先计算2*L*l+(l^2+c)
//当前我们有2*L*l1+(l1^2+c1)和2*L*l2+(l2^2+c2)
//相减得到:(l1^2+c1)-(l2^2+c2)+2*L*l1-2*L*l2 (一种符号) 0
//化一下: l1^2+c1-l2^2-c2 (一种符号) -2*(l1-L2)*L
//将-2*(l1-L2)除过去(不用变号),就有:(l1^2+c1-l2^2-c2)/(2*(l2-L1)(一种符号)L
//当(一种符号)为大于号时,代表着2*L*l1+(l1^2+c1)>2*L*l2+(l2^2+c2)
//而我们要改变L直到(一种符号)为小于或等于号,这样n1才比n2优
//就像这样:(l1^2+c1-l2^2-c2)/(2*(l2-L1)>L,只要不断给 L+1,总会变号的
while((sqe(n1.d)+n1.c-sqe(n2.d)-n2.c)>res*2*(n2.d-n1.d)) res++;
//实际上,L(f)就代表着该节点较优时,子树外切割线的最小值,所以上面才按f排序
return res;
}
void dfs(int x, int fa) {
LL res=-1, sum=0; //成本最小的切割线的目前x子树成本,不含切割线的目前x子树成本
for(auto y: G[x]) if(y!=fa){
dep[y]=dep[x]+1; dfs(y, x);
auto n1=*(--b[p[y]].upper_bound({0, 0, -dep[x], 1})); //f查找x就代表着要查找子树外切割线为x的最优解
//如果n1的f!=dep[x],那么t就会大一点
LL t=sqe(n1.d-dep[x])+n1.c+dp[y]; //由y的切割线延申到x
if(res!=-1) res+=t; //如果y不是第一个子节点,就累计
if(b[p[x]].empty()){ //启发式,如果x没有切割线
swap(p[x], p[y]);
dp[x]=dp[y];
//dp[y]=0;
//到时候取答案会直接取x,dp[y]不重要
}
else{
LL os=dp[x]+t, ot=dp[y]+sum; //分别为切割线在y上x的最小成本,切割线不在y上x的最小成本
if(b[p[x]].size()<b[p[y]].size()){ //启发式,如果y的切割线比x的多话
swap(p[x], p[y]);
swap(os, ot);
//不改dp的原因是os和ot后面会赋值dp
}
for(auto it: b[p[y]]){ //合并两条边
auto n2=*(--b[p[x]].upper_bound({0, 0, it.d-2*dep[x], 1})); //见39行
LL tmp=sqe(it.d+n2.d-2*dep[x])+it.c+n2.c+dp[x]+dp[y]; //把x的切割线和y的切割线连在一起
if(res==-1 || tmp<res) res=tmp; //找到了成本更小的切割线,更新
}
for(auto it: b[p[y]]){
it.c+=ot-os; //加上差值,代表当前情况切割线变到x其它不变(感性理解
auto n2=b[p[x]].insert(it).first; //这里first代表着插入it之后it在set里的位置
bool flag=0; //当前解是否最深
while(n2!=b[p[x]].begin()){ //不是最深的(维护it前的下凸壳)
auto n3=n2; n3--;
if((*n3).d==it.d){ //有层数和当前一样的解还更优
b[p[x]].erase(n2); //删除当前解
flag=1;
break;
}
node no=*n3;
it.f=calc(no, it); //赋值
b[p[x]].erase(n2);
n2=b[p[x]].insert(it).first;
if(it.f<=(*n3).f) b[p[x]].erase(n3); //维护下凸壳
else break;
}
if(flag) continue;
if(n2==b[p[x]].begin()){ //当前最深解
it.f=-inf; //第一个的特殊值为无穷小
b[p[x]].erase(n2);
n2=b[p[x]].insert(it).first;
}
auto n3=n2; n3++;
while(n3!=b[p[x]].end()){ //维护it后的下凸壳
if(it.d==(*n3).d){ //同上
b[p[x]].erase(n3);
n3=n2; n3++;
continue;
}
node no=*n3;
no.f=calc(it, no);
if(no.f<=it.f){
b[p[x]].erase(n2);
break;
}
b[p[x]].erase(n3);
n3=b[p[x]].insert(no).first;
auto n4=n3; n4++;
if(n4!=b[p[x]].end() && (*n4).f<=no.f) {
b[p[x]].erase(n3);
n3=n4;
}
else break;
}
}
dp[x]=os; //更新(加上y的贡献)
}
sum+=t;
}
if(b[p[x]].empty()){ //没有切割线或叶子节点
b[p[x]].insert({dep[x], 0, -inf, 0}); //见88行
}
else if(res!=-1){ //之前x有切割线(其实还是维护下凸壳)
node tmp={dep[x], res-dp[x], 0, 0}; //res-dp[x]为x点最优切割线有关花费
while(!b[p[x]].empty()){
node n1=*b[p[x]].rbegin();
tmp.f=calc(n1, tmp);
if(tmp.f>n1.f) break;
else b[p[x]].erase(n1); //tmp更优
}
if(b[p[x]].empty()) tmp.f=-inf; //同88行
b[p[x]].insert(tmp); //插入
}
}
int main()
{
scanf("%d", &n);
for(int i=1; i<=n; i++) p[i]=i; //偏移值初始化
for(int i=1; i<n; i++){
int a, b; scanf("%d%d", &a, &b);
G[a].push_back(b);
G[b].push_back(a);
}
dep[1]=0; dfs(1, 0);
if(G[1].size()==1){
auto it=--b[p[1]].upper_bound({0, 0, 0, 1}); //查找最优解
printf("%lld\n", dp[1]+(*it).c+sqe((*it).d));
//1除切割线外花费+某点切割线花费+1到某点距离的平方
}
else{
auto it=b[p[1]].rbegin(); //取最后一个最靠近1的,含信息量高且因为维护过是最优的
printf("%lld\n", dp[1]+(*it).c+sqe((*it).d));
}
return 0;
}
感谢观看。
"Non mollare mai. "