「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{ ...
随机推荐
- Dart 2.17 正式发布
文/ Michael Thomsen, Google Dart 团队产品经理,2022 年 5 月 12 日发表于 Dart 官方博客 随着 Flutter 3 在本次 I/O 大会的发布,我们也同时 ...
- 认识并安装WSL
认识并安装WSL(基于Windows的Linux子系统) 什么是WSL WSL(Windows Subsystem for Linux),这是在windows平台运行的linux子系统.也就是说可是不 ...
- python目录索引
python目录索引 python基础数据类型1 目录 part1 part2 运算符 格式化 part3 字符串 字符串常用操作方法 part4 列表 列表的创建: 列表的索引,切片 列表的增删改查 ...
- AGC007E Shik and Travel 解题报告
AGC007E Shik and Travel 题目大意:\(n\) 个点的二叉树,每个点要么两个儿子,要么没有儿子,每条边有边权. 你从 \(1\) 号节点出发,走到一个叶子节点.然后每一天,你可以 ...
- redis+lua实现脚本一键查询
场景 经常需要查redis某个key的值,需要执行三条命令才能查到 redis-cli,启动redis select num,选择db get key,查询语句 需要执行三条命令才能实现某个key的查 ...
- Hive参数与性能企业级调优
Hive作为大数据平台举足轻重的框架,以其稳定性和简单易用性也成为当前构建企业级数据仓库时使用最多的框架之一. 但是如果我们只局限于会使用Hive,而不考虑性能问题,就难搭建出一个完美的数仓,所以Hi ...
- Maven笔记---超详细
显眼位置标注来源:此文章为B站课程黑马程序员Maven全套教程笔记,由本人整理. Maven简介 Maven的本质是一个项目管理工具,将项目开发和管理过程抽象成一个项目对象模型(POM) POM (P ...
- go-zero微服务实战系列(三、API定义和表结构设计)
前两篇文章分别介绍了本系列文章的背景以及根据业务职能对商城系统做了服务的拆分,其中每个服务又可分为如下三类: api服务 - BFF层,对外提供HTTP接口 rpc服务 - 内部依赖的微服务,实现单一 ...
- 你真的很了解printf函数吗?
对C语言中经常使用的printf这个库函数,你是否真的吃透了呢? 系统化的学习C语言程序设计,是不是看过一两本C语言方面的经典著作就足够了呢?答案是显而易见的:不够.通过这种典型的入门级的学习方式,是 ...
- ABAP CDS - DEFINE VIEW, name_list
Syntax ... ( name1, name2, ... ) ... Effect Defines the element names of a CDS view in ABAP CDS in a ...
若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是直径矛盾,所以这种情况也不成立: