「APIO2010」巡逻 题解
来源 LCA
个人评价:lca求路径,让我发现了自己不会算树的直径(但是本人似乎没有用lca求)
1 题面
大意:有一个有n个节点的树,每条边权为1,一每天要从1号点开始,遍历所有的边,再回到1号点,每条道路都经过两次,为了减少需要走的距离,可以增加K\((1\leq K\leq 2)\)条新的边(可以自环),且每天必须经过这K条边正好一次,请计算最佳方案是总路程最小,并输出最小值
2 分析题面
因为K很小,所以我们可以试着手推一下每种情况
2.1 不加边
从1号点出发,要把每个边遍历一遍再回到1号点,会恰好经过每条边2次,经过的路线总长为2(n-1)。
2.2 加1条边
因为这是一棵树,我们加一条边就会使它变成环,这个环便可以在遍历图的时候减少重复遍历的长度
如:
观察可以发现,在2和8之间加一条边后,2~8的路径经过的次数就会减一,加上新的这条边的边权,所以对应的,总的需要经过的路径总长度也会改变
那么也能很显然的看出,我们要尽量选择隔得远的两个节点建边,可以使得节省的路径更长
换个说法:找到树上距离最长的两点
于是,是不是想到了什么?
对,树的直径
那这样我们就可以用树的直径来求出两个距离最长节点,在他们之间建一条边,然后用lca算出他们的路径长度dist1,答案就应该是\(2(n-1)-dist1+1\)
那么这样,我们也就拿到了30分
2.3 加两条边
第一条边加完了,再各种手推的加第二条边的情况
2.3.1 两个环有重叠
两环重叠部分1-3-5-8,长度为3;车子还要跑正好一遍1-8这条新路,就导致1-3-5-8要多走一遍,多增加了4的长度
肯定不行!
所以我们要让它的重叠部分长度为0(不然还不如自环)!
2.3.2 两个环无重叠
那就没有多跑的影响,如:
2.3.3 如何计算
那应该怎么计算多在哪里建一条边呢?
经过我们之前的推导,肯定也是在直径上,但是我们这里要不让它有重叠,也就是说,直径上的路径不应该有之前第一次直径的路径,所以就可以考虑把之前直径的路径的边权变为-1,在跑一次直径就ok了,长度记为dist2
那么答案就应该是\(2(n-1)-dist1-dist2+2\)
2.4 时间复杂度
第一次直径O(n),修改边权O(n),第二次直径O(n)
噢,O(n)的整体算法!
3 代码实现(注释)
3.0 树的直径
考虑到我一开始都忘了这个知识点,还是简单补充一下吧
这道题的一个最直观的考点——树的直径
树的直径简单来说就是树中最长的链,下面将有两种O(n)的方法来求树的直径。
背景:假设树以N个节点N-1条边以无向图的形式给出并以邻接表的形式给出。
第一种:树形DP求树的直径
我们用dis[x]表示x节点到叶子节点的最大值(单方面往下)
看不懂转移的可以自己推一下很显然
代码实现:
void dp(int x){
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
dp(v);
ans=max(ans,dis[x]+dis[v]+e[i].w);
dis[x]=max(dis[x],dis[v]+e[i].w);
}
}
优点:可以处理负权值的问题(就比如我们这里的第二条直径)
但是缺点就是不好记录直径的起始点和终结点和路径(也可能是我太逊
第二种:两次dfs(bfs)求直径
方法:先从任意一点P出发,找离它最远的点Q,再从点Q出发,找离它最远的点W,W到Q的距离就是是的直径
(我不想写证明!)
证明:
若P已经在直径上,根据树的直径的定义可知Q也在直径上且为直径的一个端点
若P不在直径上,我们用反证法,假设此时WQ不是直径,AB是直径
若AB与PQ有交点C,由于P到Q最远,那么PC+CQ>PC+CA,所以CQ>CA,易得CQ+CB>CA+CB,即CQ+CB>AB,与AB是直径矛盾,不成立,如图:
若AB与PQ没有交点,M为AB上任意一点,N为PQ上任意一点。首先还是NP+NQ>NQ+MN+MB,同时减掉NQ,得NP>MN+MB,易知NP+MN>MB,所以NP+MN+MA>MB+MA,即NP+MN+MA>AB,与AB是直径矛盾,所以这种情况也不成立:
代码:
void dfs(int x,int fa){
if(dist[x]>maxx){
maxx=dist[x];
st=x;
}
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa){
dist[v]=dist[x]+e[i].w;
dfs(v,x);
}
}
}
.....一堆代码
int main(){
...
dfs(1,0);
S=st;
maxx=0;
dist[st]=0;
dfs(st,0);
T=st;
.....
}//S,T就是直径的两个端点
优点:很容易记录直径的两个端点和路径
缺点:无法处理负边权
3.1定义
struct node{
int to,nxt,w;
}e[200100];//存边结构体
int cnt,head[100100];//存边需要
int dist[100100];//dp需要,表示x节点到叶子节点的最大值
int l2;//第二条直径的长度
int l1;//记录dfs找直径时的直径长度
int FA[100100];//在更新边权的时候需要直接跳fa,这里面保存的是以直径的其中一个端点为根的情况下的fa情况
int vis[100100];//dp需要
3.2 输入
scanf("%d%d",&n,&k);
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);//建双向边
add(y,x);//因为后面会修改边权,所以add直接把初始边权变为1
}
3.3 计算
3.3.1 求第一条直径
void dfs(int x,int fa){//第一次直径
FA[x]=fa;//因为会跑两边dfs,所以保存的是第二次的(真正直径)
if(dist[x]>maxx){//如果x到根的距离比最大值大,更新
l1=dist[x];
st=x;//记录节点
}
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa){
dist[v]=dist[x]+e[i].w;//更新到根节点的距离
dfs(v,x);
}
}
}
这里面第二次计算后l1的值就是直径的长度,当然知道了两个节点你也可以用lca求(有什么必要呢)
3.3.2 修改边权
因为第二次dfs完后的根节点就是第一条直径其中的一个端点,所以直径一定是另一个端点到根节点的路径
void dfs1(int x,int fa){//更新边权
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa){//如果这条边是连接它和它父亲
//记得改双向边!!!!!!
e[i].w=-1;
e[i+1].w=-1;
dfs1(v,FA[v]);//继续找父亲的父亲~··
}
}
}
注意:这里是需要把两条边都修改为-1,我在这里wa了好久!
3.3.3 求第二条直径的长度
我一开始一直在搞两个dfs的方法,后面静态调试才发现不对(再次声明两次dfs的方法不可以处理负边权!!)
树形dp就好了,这个可以处理负边权
void Dp(int x){
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
Dp(v);
l2=max(l2,dist[x]+dist[v]+e[i].w);//更新"直径"长度
dist[x]=max(dist[x],dist[v]+e[i].w);//更新dist的值
}
}
//最后l2就是第二条直径的长度了
3.4 输出
if(k==1){//判断一下输出就好了
printf("%d",(n-1)*2-l1+1);
}else{
printf("%d",n*2-l1-l2);
}
3.5 总体代码
#include<bits/stdc++.h>
using namespace std;
int n,k;
struct node{
int to,nxt,w;
}e[200100];
int cnt,head[100100],dist[100100],l2;
void add(int u,int v){//建边
cnt++;
e[cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
e[cnt].w=1;//手动增加边权以便后面好处理
}
int l1,st,FA[100100],vis[100100];
void dfs(int x,int fa){//第一次直径
FA[x]=fa;
if(dist[x]>l1){
l1=dist[x];//记录直径长度
st=x;//节点
}
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v!=fa){
dist[v]=dist[x]+e[i].w;//更新长度
dfs(v,x);
}
}
}
void dfs1(int x,int fa){//更新边权
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa){
e[i].w=-1;//更新双向边边权
e[i+1].w=-1;
dfs1(v,FA[v]);
}
}
}
void Dp(int x){
vis[x]=1;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
if(vis[v])continue;
Dp(v);
l2=max(l2,dist[x]+dist[v]+e[i].w);//更新第二条直径
dist[x]=max(dist[x],dist[v]+e[i].w);//更新dist
}
}
int main(){
scanf("%d%d",&n,&k);
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
add(x,y);//加边
add(y,x);
}
dfs(1,0);
int S=st;//记录一个节点
l1=0;
dist[st]=0;
dfs(st,0);
int T=st;//记录另一个节点
memset(dist,0,sizeof(dist));
memset(vis,0,sizeof(vis));
dfs1(T,FA[T]);//更新边权,是以S为根,到T的路径
Dp(1);//第二次求直径
if(k==2){
printf("%d",(n-1)*2-l1-l2+2);
}else{
printf("%d",(n-1)*2-l1+1);
}
return 0;
}
4 总结
- 别看这个题目在lca里面,其实考的是树的直径,也就提高了对题目的分析和转换问题的能力
- 没有什么难的容易错的地方,其实还是很基础的题目,就是看对树的直径的应用和理解
- 如果你真的很想用lca来做,那就直接算出两次直径的节点然后计算路径就好了(何必呢)
「APIO2010」巡逻 题解的更多相关文章
- 「SDOI2016」征途 题解
「SDOI2016」征途 先浅浅复制一个方差 显然dp,可以搞一个 \(dp[i][j]\)为前i段路程j天到达的最小方差 开始暴力转移 \(dp[i][j]=min(dp[k][j-1]+?)(j- ...
- LuoguP7713 「EZEC-10」打分 题解
Content 某个人去参加比赛,\(n\) 个评委分别给他打分 \(a_1,a_2,\dots,a_n\).这个人可以最多执行 \(m\) 次操作,每次操作将一个评委的分数加 \(1\).定义他的最 ...
- LuoguP7715 「EZEC-10」Shape 题解
Content 有一个 \(n\times m\) 的网格,网格上的格子被涂成了白色或者黑色. 设两个点 \((x_1,y_1)\) 和 \((x_2,y_2)\),如果以下三个条件均满足: \(1\ ...
- 「LeetCode」全部题解
花了将近 20 多天的业余时间,把 LeetCode 上面的题目做完了,毕竟还是针对面试的题目,代码量都不是特别大,难度和 OJ 上面也差了一大截. 关于二叉树和链表方面考察变成基本功的题目特别多,其 ...
- 【FZYZOJ】「Paladin」瀑布 题解(期望+递推)
题目描述 CX在Minecraft里建造了一个刷怪塔来杀僵尸.刷怪塔的是一个极高极高的空中浮塔,边缘是瀑布.如果僵尸被冲入瀑布中,就会掉下浮塔摔死.浮塔每天只能工作 $t$秒,刷怪笼只能生成 $N$ ...
- LuoguP7441 「EZEC-7」Erinnerung 题解
Content 给定 \(x,y,K\).定义两个数列 \(c,e\),其中 \(c_i=\begin{cases}x\cdot i&x\cdot i\leqslant K\\-K&\ ...
- loj#2076. 「JSOI2016」炸弹攻击 模拟退火
目录 题目链接 题解 代码 题目链接 loj#2076. 「JSOI2016」炸弹攻击 题解 模拟退火 退火时,由于答案比较小,但是温度比较高 所以在算exp时最好把相差的点数乘以一个常数让选取更差的 ...
- loj#2552. 「CTSC2018」假面
题目链接 loj#2552. 「CTSC2018」假面 题解 本题严谨的证明了我菜的本质 对于砍人的操作好做找龙哥就好了,blood很少,每次暴力维护一下 对于操作1 设\(a_i\)为第i个人存活的 ...
- loj#2015. 「SCOI2016」妖怪 凸函数/三分
题目链接 loj#2015. 「SCOI2016」妖怪 题解 对于每一项展开 的到\(atk+\frac{dnf}{b}a + dnf + \frac{atk}{a} b\) 令$T = \frac{ ...
随机推荐
- MinIO学习
1.Minio及背景 Minio是一个开源的分布式文件存储系统,它基于 Golang 编写,虽然轻量,却拥有着不错的高性能,可以将图片.视频.音乐.pdf这些文件存储到多个主机,可以存储到多个Linu ...
- 在MySQL中保存Java对象
需要在MySQL中保存Java对象. 说明: 对象必须实现序列化 MySQL中对应字段设置为blob 将Java对象序列化为byte[] public static byte[] obj2byte(O ...
- 48. Rotate Image - LeetCode
Question 48. Rotate Image Solution 把这个二维数组(矩阵)看成一个一个环,循环每个环,循环每条边,每个边上的点进行旋转 public void rotate(int[ ...
- [源码解析] TensorFlow 分布式之 ClusterCoordinator
[源码解析] TensorFlow 分布式之 ClusterCoordinator 目录 [源码解析] TensorFlow 分布式之 ClusterCoordinator 1. 思路 1.1 使用 ...
- Go到底能不能实现安全的双检锁?
不安全的双检锁 从其他语言转入Go语言的同学经常会陷入一个思考:如何创建一个单例? 有些同学可能会把其它语言中的双检锁模式移植过来,双检锁模式也称为懒汉模式,首次用到的时候才创建实例.大部分人首次用G ...
- 一次XGBoost性能优化-超线程影响运算速度
一.问题背景 一个朋友在使用 XGBoost 框架进行机器学习编码,他们的一个demo, 在笔记本的虚拟机(4核)运行的时候,只要8s, 但是在一个64核128G 的物理机上面的虚拟机去跑的时候,发现 ...
- Typora使用手册(小白入门级)
Typora软件的简单使用 1.简介 Typora是一款支持Markdown语法的文档编辑器 特点:功能强大.画面极简. 下载地址:https://typoraio.cn/ 2.基础设置 偏 ...
- 线上问题定位利器 jprofiler
1.导出dump windows: jps -l 查看Java进行 jmap -dump:format=b,file=webapi.hprof 20840 查看进程,根据进程号导出hprof文件 ...
- js 循环生成元素,并为元素添加click事件,结果只执行最后一个点击事件
问题描述:有一个参数集合data,for循环为每一个参数生成一个dom元素,并附加onclick事件.生成之后发现点击事件里的参数全是data集合里的最后一个. 代码如下: var dom=$('#d ...
- JQuery实现图片轮播无缝滚动
图片轮播无缝滚动实例 实现效果展示预览: 思路: 1.设置当前索引curIndex,和前一张索引prevIndex.(curIndex为下一次要显示的图片索引,prevIndex为现在看见的图片) 2 ...