2024集训D6总结

集训D6总结

讲课内容

主要是树上DP , DS维护 , 已经一些构造型题 , 以讲题为主 . 没必要单独总结 .

训练记录

P2515

裸拓扑序问题 , 套路地直接缩点 , 从 0 向所有连边变成一颗外向树 , 然后直接树上背包 .

虽然自己写的还是比较轻松的 . 不过看起来确实比较考验代码能力 .

CF494D

题目中的 \(S(v)\) 其实就是 \(v\) 子树 , 然后发现问题变成了一个从 \(u\)\(v\) 的子树内所有点的 \(dis^2\) . 考虑简单情况 , \(u\) 不在 \(v\) 子树内 , \(u\)\(x\)\(lca\) 确定为 \(v\) , 可以通过预处理子树深度和 , 深度和平方解决 .

而在子树内发现 \(lca\) 是从 \(u\)\(v\) 的一条链 , 要求的是

\[dep_u+dep_v-2\times dep_{lca} \]

对每个点维护子树内与 \(lca\)\(dep_v-2\times dep_lca\) 和与平方和 , 链上差分即可做出 .

本身不算难的一道题 , 同样 , 只要始终保持思路清晰 , 不写到 "神秘化" , 就可以较为容易地解决 .

ARC101E

发现正着做很难从 \(n^3\) 优化到 \(n^2\) , 而且配对这个限制本身很难避免单开一维来记录子树内没匹配的点 .

于是考虑反着做 , 用容斥 . 发现考虑钦定某些边没有覆盖 , 其余的随意 , 相当于把原来的树分成若干的连通块 , 每个连通块内相当于 \(sz!!\) 的任意匹配 .

因为对 \(sz\) 进行 dp , 可以进行经典的树上背包 \(n^2\) , 容斥系数就是最平凡的一类 .

这道题比较有思维含量 , 想到容斥以及双阶乘处理 , 虽然思路连贯 , 但是对基本功的要求还是有的 .

CF1100F

昨天的数据结构题 . 一本通 (或者别的哪里) 见过原题 .

整体思路类似于 (拟阵) 贪心 , 扫描线扫 \(r\) , 对所有情况统一维护线性基 , 记录每个元素在原序列的下标 .

希望在查询 \(l\) 时组合出的数最大 , 就要尽可能让下标 \(I\) 大的元素放在高位上 , 这对于线性基来说并不难 .

具体地 , 在插入时 , 只要当前能插入 , 下标比这一位原来的数大 , 就替换这个数继续向后跑 . 因为线性基的性质保证正确性 . 同时根据贪心可以保证最大化 .

查询时 , 直接选择所有下标 \(\ge l\) 的即可 .

这时一道线性基题 , 根据问题还是可以找到思路的 .

loj6669

经典的交互构造方案题 . 这一类题肯定优先把最直接的信息读取出来 , 比如这一题可以直接读取 \(dep_i\) , 然后考虑如何在 $n\log n $ 时间内确定其余点 .

没有具体方向 , 用数据范围猜测一下做法 . 一个方向是对 \(\log n\) 个点考虑 \(O(n)\) 的查询来确定所有点 , 但是在极端情况下树高为 \(\log n\) 时 , 从 \(\log\) 个点出发查询始终有两个点没有本质区别 , 因此不可行 .

另一个就是确定每一个点用 $\log n $ 次. 发现这个过程类似于一个链查询 , 只不过是从链顶开始跳 , 可以用 \(1\) 次操作确定 \(x\) 在任意一条链上的子树位置 .

结合范围 , 直接使用重链剖分 , 从上向下跳链即可做到 .

非常有趣的一道题 , 作为一道构造方案题 , 问题本身并不很强调找性质 , 反而很好地利用了重链剖分这一算法 . 这可以提醒在构造题中找 熟悉方法 的重要性 .

IOI2020 链接擎天树

与上一题不同 , 更接近找性质的一个题 .

先手玩 , 可以发现 \(b_{i,j}=3\) 的情况显然不合法 , 于是简化到 \(b_{i,j}=0/1/2\) .

从简单开始想起 , 不考虑 \(2\) , 那么只需要判断给出的 \(b_{i,j}=1\) 产生的连通块是否与 \(b_{i,j}=0\) 的条件矛盾 , 用并查集就可以完成 , 构造只需要把连通块连成任意的树 .

考虑 \(b_{i,j}=2\) 显然路径中间是恰好构成了一个环 , 再次手玩可以发现整个连通块不应该有第二个环 , 否则必然会出现 \(b_{i,j}=4\) 的情况 .

因此就是构造一个基环树森林 , \(b_{i,j}=1\) 的必须在同一颗子树上 , 而 \(=2\) 的必须不在同一子树上 , 注意子树个数 \(=2\) 时也无法完成 , 因为构不成环 .

剩下的就是简单构造了 .

虽然找性质 , 但是性质并不算藏得很深 , 关键还是手玩 , 而且简化掉不合法的无关情况 , 剩下的就是并查集判断了 ..

WC2018 通道

去年也做过这个题 , 不过是用随机化水过的 , 代码还大部分借鉴了题解 ; 今年再遇到 , 花了不到一晚上 , 总算把正确的做法给写出来了 .

这道题本身没有什么思维难度 , 就是直接在三棵树上找 \(dis_{u,v}\) 和最大值 .

  • 第一棵树用边分治 , 每次把两侧的边染成不同颜色供后面匹配 , 同时转化不同颜色的 \(dis(u,v)=dep1_u+dep1_v\) . 用边分是为了保证两个颜色匹配容易做 . 点分配合Huffman树也可以做 , 但是太毒瘤了 .

  • 第二棵树直接建出虚树 , 如果只考虑这两颗树 , 那么类似于 暴力写挂 这道题 , 直接在LCA处匹配左右子树不同颜色的最大值 .

    有第三课树也同理 , 还是考虑 \(LCA\) 合并左右子树 , 这时考察的是来自不同子树 , 颜色不同 的两个点 的 \(dep1_u+dep1_v+dep2_u+dep2_v+dis3(u,v)-dep2_{LCA}\times 2\) , 把后面的 $LCA $提出来 , 就变成了第三棵树上只关于 \(u,v\) 的问题 .

  • 第三棵树我们有经典的直径引理 , 考虑上式相当于把 \(dep1_u+dep2_u\) 挂在了 \(u\) 点下面 , 然后求这些挂点构成的某个点集的直径 , 我们只需枚举直径端点 , 合并点集时枚举 \(6\) 对点 , 更新答案时枚举来自不同点集的 \(4\) 个点 , 就保证了最大化 .

至此思路就结束了 , 考虑写的过程 , 还是很体现了一些树题 , 尤其是需要大量模板类东西维护的树题的代码技巧 :

  • 三棵树之间仅仅用 \(dep1 , dep2\) 虚树点集等等沟通 , 第三棵树更是只需提供 $dis(u,v) $ , 因此善用 namespace , 把函数写的 模块化 一些 , 调试时先对每一个模块是否正常工作进行研究 .
  • 保持良好代码习惯 , 写之前做好 int/ll 区分 . LCA 就正常用 \(nlogn - O1\) 写法就行 , 这减少了常数 , 也易于实现 . 边分治正常用链式前向星存图 , 做好 rebuild , 边分治本身并不难写 . 注意在考虑路径时不要忘记分治边本身 . 这也保证不会让代码 "神秘化" .
  • 保持自信 . 在大码量题中一定对调试好的东西有自信 . 集中精力去调试复杂的 , 核心的 , 与模板差别大的部分 . 不要草木皆兵 .

总而言之 , 这道题类似于一个模板集合 , 但是确实很考验对树的基本东西的应用 . 写出来不管怎么说还是很有成就感 .

#include<bits/stdc++.h>
#define file(x) freopen(#x ".in","r",stdin),freopen(#x ".out","w",stdout)
#define ll long long
#define INF 0x7fffffff
#define INF64 1e18
using namespace std;

constexpr int N=1e5+5;

int n;
ll d1[N*2],d2[N];
int c[N*2],b[N*2],bn;
ll res;

bitset<N*2> inq;

namespace Tree3{

	vector<pair<int,ll > > e[N];
	int fd[N*2][25],od,pos[N],dfn[N],rnk[N],tot,lg[N*2];
	ll dep[N];

	void dfs1(int u,int fa){
		dfn[u]=++tot;rnk[tot]=u;fd[++od][0]=dfn[u];pos[u]=od;
		for(auto [v,w]:e[u]){
			if(v==fa) continue;
			dep[v]=dep[u]+w;
			dfs1(v,u);
			fd[++od][0]=dfn[u];
		}
	}

	int LCA(int x,int y){
		x=pos[x],y=pos[y];
		if(x>y) swap(x,y);
		int t=lg[y-x+1];
		return rnk[min(fd[x][t],fd[y-(1<<t)+1][t])];
	}

	void init(){
		for(int i=1;i<n;i++){
			int u,v;ll w;cin>>u>>v>>w;
			e[u].push_back({v,w});
			e[v].push_back({u,w});
		}
		dfs1(1,1);
		for(int j=1;(1<<j)<=od;j++)
			for(int i=1;i+(1<<j)-1<=od;i++)
				fd[i][j]=min(fd[i][j-1],fd[i+(1<<(j-1))][j-1]);
		lg[0]=lg[1]=0;
		for(int i=2;i<=od;i++) lg[i]=lg[i/2]+1;
	}
	
	ll dis(int u,int v){
		return dep[u]+dep[v]-2*dep[LCA(u,v)];
	}
}


namespace Tree2{

	vector<pair<int,ll > > e[N];
	int fd[N*2][25],od,pos[N],dfn[N],rnk[N],tot,lg[N*2];

	void dfs1(int u,int fa){
		dfn[u]=++tot;rnk[tot]=u;fd[++od][0]=dfn[u];pos[u]=od;
		for(auto [v,w]:e[u]){
			if(v==fa) continue;
			d2[v]=d2[u]+w;
			dfs1(v,u);
			fd[++od][0]=dfn[u];
		}
	}

	int LCA(int x,int y){
		x=pos[x],y=pos[y];
		if(x>y) swap(x,y);
		int t=lg[y-x+1];
		return rnk[min(fd[x][t],fd[y-(1<<t)+1][t])];
	}

	void init(){
		for(int i=1;i<n;i++){
			int u,v;ll w;cin>>u>>v>>w;
			e[u].push_back({v,w});
			e[v].push_back({u,w});
		}
		dfs1(1,1);
		for(int j=1;(1<<j)<=od;j++)
			for(int i=1;i+(1<<j)-1<=od;i++)
				fd[i][j]=min(fd[i][j-1],fd[i+(1<<(j-1))][j-1]);
		lg[0]=lg[1]=0;
		for(int i=2;i<=od;i++) lg[i]=lg[i/2]+1;
	}

	
	pair<int,int> f[N][2];

	ll calc(int x,int y){
		if(x==0||y==0) return 0;
		return d1[x]+d1[y]+d2[x]+d2[y]+Tree3::dis(x,y);
	}

	pair<int,int> merge(pair<int,int> x,pair<int,int> y){
		if(x==make_pair(0,0)) return y;
		if(y==make_pair(0,0)) return x;
		pair<int,int> tmp[6];
		tmp[0]=x;tmp[1]=y;
		tmp[2]={x.first,y.first};
		tmp[3]={x.second,y.first};
		tmp[4]={x.first,y.second};
		tmp[5]={x.second,y.second};
		sort(tmp,tmp+6,[](pair<int,int> xx,pair<int,int> yy){
				return calc(xx.first,xx.second)>calc(yy.first,yy.second) ;
				});
		return tmp[0];
	}

	void solve(ll w){
		sort(b+1,b+bn+1,[](int x,int y){return dfn[x]<dfn[y];});
		int tmp=bn;
		for(int i=1;i<tmp;i++) b[++bn]=LCA(b[i],b[i+1]);
		sort(b+1,b+bn+1,[](int x,int y){return dfn[x]<dfn[y];});
		bn=unique(b+1,b+bn+1)-b-1;
		for(int i=1;i<=bn;i++) {
			f[b[i]][0]=f[b[i]][1]={0,0};
			if(inq[b[i]]) f[b[i]][c[b[i]]]={b[i],0};
		}
		for(int i=bn;i>=1;i--){
			int u=b[i];
			if(i==1) continue;
			int fa=LCA(b[i],b[i-1]);
			res=max(res,max({
						calc(f[fa][0].first,f[u][1].first),
						calc(f[fa][0].first,f[u][1].second),
						calc(f[fa][0].second,f[u][1].first),
						calc(f[fa][0].second,f[u][1].second),
						calc(f[fa][1].first,f[u][0].first),
						calc(f[fa][1].first,f[u][0].second),
						calc(f[fa][1].second,f[u][0].first),
						calc(f[fa][1].second,f[u][0].second)
						})-d2[fa]*2+w);
			f[fa][0]=merge(f[fa][0],f[u][0]);
			f[fa][1]=merge(f[fa][1],f[u][1]);
		}
		for(int i=1;i<=bn;i++) inq[b[i]]=0;
	}
}

namespace Tree1{

	struct node{
		int u,v;ll w;int nxt;
	}te[N*4],e[N*4];

	int headt[N*2],head[N*2],tt=1,tot=1,pn;

	bitset<N*4> vis;

	void addt(int u,int v,ll w){
		te[++tt]={u,v,w,headt[u]};headt[u]=tt;
		te[++tt]={v,u,w,headt[v]};headt[v]=tt;
	}
	void add(int u,int v,ll w){
		e[++tot]={u,v,w,head[u]};head[u]=tot;
		e[++tot]={v,u,w,head[v]};head[v]=tot;
	}

	void build(int u,int fa){
		int last=0;
		for(int i=headt[u];i;i=te[i].nxt){
			auto [u,v,w,nxt]=te[i];
			if(v==fa) continue;
			if(last==0){
				last=u;
			}
			else{
				add(last,++pn,0);
				last=pn;
			}
			add(last,v,w);
			build(v,u);
		}
	}

	int sz[N*2];

	void getrt(int u,int fa,int siz,int &rt){
		sz[u]=1;
		for(int i=head[u];i;i=e[i].nxt){
			if(vis[i]) continue;
			auto [u,v,w,nxt]=e[i];
			if(v==fa) continue;
			getrt(v,u,siz,rt);
			sz[u]+=sz[v];
			if(!rt) rt=i;
			else if(max(sz[v],siz-sz[v])<max(sz[e[rt].v],siz-sz[e[rt].v])) rt=i;
		}
	}

	void dfs(int u,int fa,int tag){
		sz[u]=1;
		if(u<=n){
			b[++bn]=u;c[u]=tag;
			inq[u]=1;
		}
		for(int i=head[u];i;i=e[i].nxt){
			if(vis[i]) continue;
			auto [u,v,w,nxt]=e[i];
			if(v==fa) continue;
			d1[v]=d1[u]+w;
			dfs(v,u,tag);
			sz[u]+=sz[v];
		}
	}

	void solve(int x){
		if(sz[x]==1) return;
		int rt=0;
		getrt(x,x,sz[x],rt);
		int u=e[rt].u,v=e[rt].v;
		d1[u]=d1[v]=0;bn=0;
		vis[rt]=vis[rt^1]=1;
		dfs(u,u,0);
		dfs(v,v,1);
		Tree2::solve(e[rt].w);
		solve(u);solve(v);
	}

	void init(){
		pn=n;
		for(int i=1;i<n;i++){
			int u,v;ll w;
			cin>>u>>v>>w;
			addt(u,v,w);
		}
		build(1,1);
		sz[1]=n;
	}
	void main(){
		solve(1);
	}
}

int main(){
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	cin>>n;
	Tree1::init();
	Tree2::init();
	Tree3::init();
	Tree1::main();
	cout<<res;
}

总结

最近的 \(OI\) 赛制重视对树的考察 , 因为很多东西放在树上可以有很灵活的解决方式 , 同时还考验对树的基本理解 . 因此要学会适可而止地找性质 , 快速抓取关键思路用来做题 , 然后把精力用在良好的实现上 .

同时要重视代码习惯 , 树题有时思路不清晰时虽然可做 , 但是产生一些边界讨论 , 这在某些情况下是致命的 . 良好地梳理思维 , 简明 , 易于调试的实现也是树题基本功的重要部分 .

posted @ 2024-12-20 10:17  youlv  阅读(12)  评论(0编辑  收藏  举报