「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{ ...
随机推荐
- 102_Power Pivot DAX 排名后加上总排名数
焦棚子的文章目录 请点击下载附件 1.背景 每次写rank的时候,有了排名就可以了,排名1,2,3,4,5这样不是很清晰吗?但是中国式报表的老板们说你能不能在排名后面加一个总排名数呢,就像1/5,2/ ...
- DML数据操作语言
DML数据操作语言 用来对数据库中表的数据记录进行更新.(增删改) 插入insert -- insert into 表(列名1,列名2,列名3...) values (值1,值2,值3...):向表中 ...
- 可变数组Vector
package com.demon.languang.business.rest; import java.util.Vector; public class DemonTest { @Suppres ...
- 如何使用Superset可无缝对接MRS进行自助分析
摘要:本文主要介绍如何在MRS之上使用Superset进行数据分析. 本文分享自华为云社区<使用商业智能软件Superset分析MRS数据之最佳实践>,作者: 啊喔YeYe . 1. 概要 ...
- BUUCTF-webshell后门
webshell后门 老方法,D盾直接查杀. flag{ba8e6c6f35a53933b871480bb9a9545c}
- MySQL-2-DQL
DQL:数据查询语言 SQLyog中格式化某段语句片段:CTRL+F12 基础查询 语法: select 查询列表 from 表名: 特点: ① 查询列表可以是:表中的字段.常量值.表达式.函数 ② ...
- Linux安装MySQL,数据库工具连接Linux的MySQL
1.centOS中默认安装了MariaDB,需要先进行卸载 rpm -qa | grep -i mariadb rpm -e --nodeps 上面查出来的mariadb 2.下载MySQL仓库并安装 ...
- RocketMQ消息的顺序与重复
1.如何保证消息的顺序 原因:生产者将消息发给topic,topic分发给不同的队列再给多个消费者并发消费,难以保证顺序. 方案:topic和队列之间加入MessageQueueSelector.将一 ...
- HashMap1.8常见面试问题
1.hashmap转红黑树的时机: for (int binCount = 0; ; ++binCount) { if ((e = p.next) == null) { p.next = newNod ...
- 11.2 Android Studio如何切换主题和更改字体
如何进入设置? 全平台启动界面 Configure-Preferences 主界面 Windows版本:File-Settings Mac版本:Android Studio-Preferences 外 ...