[八省联考 2018] 林克卡特树 题解

这道题我前前后后做了一年,共过了 4 4 4 遍,每次都有的新的理解;这次我认为自己理解透了,于是就写了一篇题解。

这道题是我入坑看到的第一道黑题(当时很萌,不知道黑题是什么,看到这题感觉很好玩),另外还有就是《切树游戏》和 Spiders Evil Plan,记载着我的回忆(

Description

传送门

Solution

算法一

为方便叙述,令树根为 1 1 1 w u , v w_{u,v} wu,v 表示 u , v u,v u,v 之间的边权, son { u } \text{son}\{u\} son{u} 表示 u u u 的儿子组成的集合。

可以发现,答案即为 ⌈ \lceil 在树上选出 k + 1 k+1 k+1 条不相交的路径 ⌋ \rfloor 的最大边权和。

考虑 dp \text{dp} dp

f u , i , j f_{u,i,j} fu,i,j 表示,看了以 u u u 为根的子树,子树内一共选了 i i i 条不相交的路径,且 u u u 当前度数 j j j 的最大边权和。

  • j = 0 j=0 j=0,则目前没有包含 u u u 的路径。特别的,若以 u u u 为根的子树已经遍历完,则 f u , i , j f_{u,i,j} fu,i,j 表示所有该子树中的路径都被固定时的最大边权和(也就是说,遍历完子树后 f u , 0 f_{u,0} fu,0 对应的状态中 u u u 的度数可以为 0 0 0 1 1 1 2 2 2)。
  • j = 1 j=1 j=1,则目前恰有一条以 u u u 为一端的非固定路径(也就是说可以继续延伸出去,没有彻底固定这条路径的形态)。因为这条路径不固定,所以这条路径并没有被计入 i i i
  • j = 2 j=2 j=2,则目前有一条包含 u u u 的路径,且其任意一端均不为 u u u(跨越了 u u u 的两个子树)。

考虑使用树形背包转移。具体来说,令目前要将以 v ( v ∈ son { u } ) v(v \in \text{son}\{u\}) v(vson{u}) 为根的子树合并上来,则有转移:

f u , i , 0 : = max ⁡ ( f u , i , 0 , max ⁡ j = 0 i { f u , j , 0 + f v , i − j , 0 } ) f_{u,i,0}:=\max(f_{u,i,0},\max_{j=0}^i \{f_{u,j,0}+f_{v,i-j,0}\}) fu,i,0:=max(fu,i,0,j=0maxi{fu,j,0+fv,ij,0}) f u , i , 1 : = max ⁡ ( f u , i , 1 , max ⁡ j = 0 i { f u , j , 0 + f v , i − j , 1 + w u , v } , max ⁡ j = 0 i { f u , j , 1 + f v , i − j , 0 } ) f_{u,i,1}:=\max(f_{u,i,1},\max_{j=0}^i \{f_{u,j,0}+f_{v,i-j,1}+w_{u,v}\},\max_{j=0}^i \{f_{u,j,1}+f_{v,i-j,0}\}) fu,i,1:=max(fu,i,1,j=0maxi{fu,j,0+fv,ij,1+wu,v},j=0maxi{fu,j,1+fv,ij,0}) f u , i , 2 : = max ⁡ ( f u , i , 2 , max ⁡ j = 0 i − 1 { f u , j , 1 + f v , i − j − 1 , 1 + w u , v } , max ⁡ j = 0 i { f u , j , 2 + f v , i − j , 0 } ) f_{u,i,2}:=\max(f_{u,i,2},\max_{j=0}^{i-1} \{f_{u,j,1}+f_{v,i-j-1,1}+w_{u,v}\},\max_{j=0}^i \{f_{u,j,2}+f_{v,i-j,0}\}) fu,i,2:=max(fu,i,2,j=0maxi1{fu,j,1+fv,ij1,1+wu,v},j=0maxi{fu,j,2+fv,ij,0})

在转移结束后,执行:

f u , i , 0 : = max ⁡ ( f u , i , 0 , f u , i − 1 , 1 , f u , i , 2 ) f_{u,i,0}:=\max(f_{u,i,0},f_{u,i-1,1},f_{u,i,2}) fu,i,0:=max(fu,i,0,fu,i1,1,fu,i,2)

答案即为 f 1 , k + 1 , 0 f_{1,k+1,0} f1,k+1,0

这个做法的时间复杂度是 O ( n k ) O(nk) O(nk) 而非 O ( n k 2 ) O(nk^2) O(nk2) O ( n 2 ) O(n^2) O(n2) 的,期望得分 60 60 60 分。下面是来自我说说的简要证明:

  • 每次合并,将第一维度从 0 0 0 枚举到了 m i n ( s i z [ u ] , k ) min(siz[u],k) min(siz[u],k),第二维度从 0 0 0 枚举到了 m i n ( s i z [ v ] , k ) min(siz[v],k) min(siz[v],k),那么其对复杂度的贡献就是它们的乘积。
  • 我们可以认为,这里将 ⌈ \lceil 目前已合并到 u u u 处的部分中 dfs \text{dfs} dfs 序前 k k k 大的 ⌋ \rfloor ⌈ \lceil v v v 为根的子树中 dfs \text{dfs} dfs 序前 k 小的 ⌋ \rfloor 两两合并。
  • 因此,从全局的角度来看,每个节点只会与 dfs \text{dfs} dfs 序与其差不超过 2 k 2k 2k 的点进行合并。
  • 从而,总复杂度为 O ( 4 n k ) O(4nk) O(4nk)

算法二

运气足够好可以发现, f 1 , 0 , f 1 , 1 , ⋯   , f 1 , k f_{1,0},f_{1,1},\cdots,f_{1,k} f1,0,f1,1,,f1,k 组成了一个上凸函数。

于是,直接 wqs 二分就可以 O ( n log ⁡ w ) O(n \log w) O(nlogw) 地通过本题了,尽管常数大得离谱。

这题就这么结束了?不,我们还需要注意一个细微的问题——凸函数上三点共线

这会意味着什么呢?为什么会导致错误呢?考虑一段区间 [ l , r ] [l,r] [l,r],其中的点( f 1 , l , f 1 , l + 1 , ⋯   , f 1 , r f_{1,l},f_{1,l+1},\cdots,f_{1,r} f1,l,f1,l+1,,f1,r)在一条直线上。那么,若 k ∈ [ l , r ] k \in [l,r] k[l,r],那么很容易出现切不到 k k k 的情况(即,虽然斜率正确,但是切到的点不对),导致 wqs 二分结束了都没有输出答案,从而导致 WA。

那么该如何处理呢?可以发现,在 wqs 二分结束后,若没有找出答案,那么我们依然可以得到该线的斜率截距,其中前者即为二分结束后的下界 l l l,后者可以通过再跑一轮 dp \text{dp} dp 得出。于是,我们不难确定这个一次函数的表达式,就能简单地得出该一次函数在 k k k 处的值了。

综上所述,我们处理完了细节,本题被彻底解决。

Code

由于 dp \text{dp} dp 不仅要存储最大边权和,还要存储选出路径的条数,因此我采用了一个叫做 LCT 的结构体来维护,重载加法,小于之后十分方便,这里建议大家采用。

另外,请注意开 long long,并把二分上下界的绝对值设大。同时,二分上下界有人证明过可以设置为整数了,于是我就写了整数。虽然我觉得这是错的,所以大家还是用 double 吧。

#include <bits/stdc++.h>
#define int long long
#define chkmax(a,b) ((a)<(b)?(a)=(b):0)
using namespace std;
const int maxl=300005;

int read(){
	int s=0,w=1;char ch=getchar();
	while (ch<'0'||ch>'9'){if (ch=='-')  w=-w;ch=getchar();}
	while (ch>='0'&&ch<='9'){s=(s<<1)+(s<<3)+(ch^'0');ch=getchar();}
	return s*w;
}
int n,k,cnt,l,r;
int head[maxl];

struct edge{int nxt,to,dis;}e[maxl<<1];

struct LCT{
	int val,x;
	LCT(int xx=0,int yy=0):val(xx),x(yy){};
	friend bool operator<(LCT a,LCT b){
		return a.val==b.val?a.x>b.x:a.val<b.val;
	}
	friend LCT operator+(LCT a,LCT b){
		return LCT(a.val+b.val,a.x+b.x);
	}
	friend LCT operator+(LCT a,int b){
		return LCT(a.val+b,a.x);
	}
}f[maxl][3],s;

void add_edge(int u,int v,int w){
	cnt++;
	e[cnt].to=v,e[cnt].dis=w,e[cnt].nxt=head[u],head[u]=cnt; 
}

void dfs(int u,int fath){
	f[u][0]=f[u][1]=f[u][2]=LCT();
	for (int i=head[u];i;i=e[i].nxt){
		int v=e[i].to;
		if (v==fath)  continue;
		dfs(v,u);
		
		chkmax(f[u][2],max(f[u][2]+f[v][0],f[u][1]+f[v][1]+s+e[i].dis));
		chkmax(f[u][1],max(f[u][1]+f[v][0],f[u][0]+f[v][1]+e[i].dis));
		chkmax(f[u][0],f[u][0]+f[v][0]);
	}
	chkmax(f[u][0],max(f[u][1]+s,f[u][2]));
}

signed main(){
	n=read(),k=read()+1;
	for (int i=1;i<n;i++){
		int u=read(),v=read(),w=read();
		add_edge(u,v,w),add_edge(v,u,w);
	}
	l=-3e11,r=3e11;
	while (l<=r){
		int mid=(l+r)>>1;
		s=LCT(-mid,1);
		
		dfs(1,0);
		if (f[1][0].x==k)  return cout<<f[1][0].val+k*mid<<endl,0;
		else if (f[1][0].x>k)  l=mid+1;
		else r=mid-1;
	}
	s=LCT(-l,1),dfs(1,0);
	cout<<f[1][0].val+k*l<<endl;
	
	return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值