\(\mathcal{NOIP2018}\) 保卫王国 - 竞赛题解

按某一个炒鸡dalao名曰 taotao 的话说:

\(\ \ \ \ \ \ \ \ \ “一道sb倍增题”\)

顺便提一下他的〔题解〕(因为按照这个思路写的,所以代码看起来也差不多)

因为比较复(胡)杂(炸),可能需要理解久一点


『题目』

参见 〔洛谷 P5024〕


『解析』

一、初步思考

如果不考虑多次询问的话,显然可以进行一次简单的树形DP,特殊判断一下当前的点(也就是城市)能不能选(放军队)就行了~ 但是显然会\(TLE\)。

由于\(m\)也就是询问次数非常大,而dp状态非常之多,不可能 \(O(1)\) 的时间处理每次询问。那么剩下两种时间复杂度的可能性要么是 \(O(log_2n)\) 要么是 \(O(\sqrt{n})\)。根据我浅薄的对算法的理解……好像没有什么用来优化dp的有关树的算法是 \(O(\sqrt n)\) 的,倒是有一个树上倍增(\(LCA\))是 \(O(log_2n)\) 的。

(至于一般的树形dp相信大家都会)

对这道题本身进行分析——假设现在被限制的点是 \(u\),\(v\) ,根节点是 \(1\),那么:

① \(lca\) 到 点\(1\) 的路径上的 \(dp\) 值被 \(u\),\(v\) 共同影响;

② \(u\) 到 \(lca\) 的路径上的 \(dp\) 值被 \(u\) 影响;

③ \(v\) 到 \(lca\) 的路径上的 \(dp\) 值被 \(v\) 影响;

恰好我们知道有一种倍增优化dp的算法,所以正解顺理成章就是树上倍增优化树形dp~(出题人的思维还是非常厉害的)

对于倍增求lca的运用不止可以求出lca,还经常用来统计两点经过lca的简单路径上的一些特殊值,其转移方法大多数是:

如果要求 u 到 u的第\(2^i\)个祖先 的路径上的值,就综合 u 到 u的第\(2^{i-1}\)个祖先 的路径上的值,和 u的第\(2^{i-1}\)个祖先 到 u的第\(2^i\)个祖先 的路径上的值;

二、倍增推导dp数组

那么对树形dp的优化也是这样的~因为一般的dp状态定义是 “dp[u][0/1] 表示u选/不选时以u为根的子树的最小花费”,那么我们定义 \(fdp[u][k][0/1][0/1]\) 表示 “u选/不选(第三维)”并且 u的第\(2^i\)个祖先 选/不选(第四维)时,以 u的第\(2^i\)个祖先 为根的子树的最小花费。另外几个定义如下:

  • \(fa[u][i]\) 表示“点u的第\(2^i\)个祖先”,如果没有则值为0;
  • \(dep[u]\) 表示“点u的深度”;

一次DFS就可以求出 dp,dep 的值(DFS1)。

因为我们考虑了2个点的取舍,必然就会有一些之前的(没有任何限制时的最优值)是不合法的,所以我们需要对 fdp 进行一些修改。这就进入了第2次DFS:

根据 taotao 的说法,大概就是“合法的值=可能不合法的值-原有贡献+合法的贡献”

倍增求lca的基本操作——先把 u 到 u的父亲 的值处理出来,然后依次处理出 fa和fdp 。也就是说我们先要处理 \(fdp[u][0][0/1][0/1]\) ,以下简称pre为u的父亲:

  • fdp[u][0][0/1][0/1]=INF :因为 u 和 pre 至少有一个要选;
  • fdp[u][0][1][0]=dp[pre][0] :因为如果pre不选那么u一定选
  • fdp[u][0][0][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][0]:因为选pre时,u的贡献为min(dp[u][0],dp[u][1]),但是我们必须限制u不选,所以减去它原有贡献再加上dp[u][0]也就是合法贡献;
  • fdp[u][0][1][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][1]:其他的都和上面的那种情况是相同的,只不过这是限制了u必须选也就是必须产生dp[u][1]的贡献。

重要的不是递推式,是思路“合法的值=可能不合法的值-原有贡献+合法的贡献”——

假设我们现在要推导从u到u的第\(2^k\)个祖先的状态,也就是fdp[u][k][a][b](a决定u选/不选,b决定u的第\(2^k\)个祖先选/不选)

那么,我们可以根据已经得到的

\(\ \ \ \ \ \ \ ①\)u到u的第\(2^{k-1}\)个祖先(定义为anc)的状态也就是fdp[u][k-1][a][0/1]

\(\ \ \ \ \ \ \ ②\)anc到anc的第\(2^{k-1}\)个祖先的状态也就是fdp[anc][k-1][0/1][b]

可见anc有两种状态——选/不选,那么我们定义这个状态为c

① 那么上面说的“可能不合法的值”是指fdp[anc][k-1][c][b],因为我们并不知道这个值所代表的状态是否满足u的状态;

② “原有贡献”也就是dp[anc][c],在这种状态下,u的状态是不受限制的;

③ “合法的贡献”说的是fdp[u][k-1][a][c],这样的话也就限制了u的状态

综上所述,我们可以得到转移式:

\[fdp[u][k][a][b]=\min\begin{cases}
fdp[anc][k-1][0][b]-dp[anc][0]+fdp[u][k-1][a][0]\\
fdp[anc][k-1][1][b]-dp[anc][1]+fdp[u][k-1][a][1]
\end{cases}
\]

(感觉开始difficult了)

这样的话我们就可以转移了~这就是第二次DFS(DFS2)。

三、lca求解答案

接下来就是求LCA,顺便求解答案的过程——类比之前的转移,这里的转移要点也是“合法的值=可能不合法的值-原有贡献+合法的贡献”;只是说这里的转移可以大致分为3步(下面都假设u的深度大于v):

  1. 将u移动到与v深度相同的位置,移动的路径就是u的状态会影响的路径;
  2. 将u,v同时移动到它们的lca,u,v各自移动的路径分别受u,v的状态的影响;
  3. 将lca移动到根节点,lca移动的路径由u,v的状态共同影响;

那么我们设在限制下,当前的以u(这里强调“当前”是因为u在不断移动)为根的子树的最小花费为 val[1](选择u) 和 val[2](不选择u);以及当前的以v为根的子树的 val[3] 和 val[4]。然后在定义一个fval[1~4]分别对应val[1~4],方便转移~

(1)将u移动到与v同层

这里涉及到的变量有 u 对应的状态 val[1],val[2],以及 u 将要移动到的位置对应的状态 fval[1],fval[2]。

假设现在要将u移动到u的第\(2^i\)个祖先,显然u的状态可以选也可以不选,那么定义a为u的状态:

对于fval[1],这里的“可能不合法的值”是fdp[u][i][a][1](因为不知道是否满足限制),“原有贡献”为dp[u][a],“合法贡献”为val[a](u的状态对应的val值);当然fval[2]也可以类比上面的方法。

(2)将u,v同时移动到lca

和(1)相同,只是多了v也要同时转移~(也就是说还涉及到val[3],val[4])

但是有一种特殊情况——v是u的祖先,这样的话根据v的限制,我们就需要舍去一个val值:

现在的val[1],val[2]也就是lca选/不选所对应的值,但是如果v就是lca,v就会对val产生限制,也就是说当v不能选时,表示选的val[1]就要被舍去,而当v必须选时,表示不选的val[2]就要被舍去。

(3)将lca移动到根节点

完全一样了,这里的lca就代替了u,val[1],val[2]分别指lca选/不选时以lca为根的子树的花费。

最后得到答案就是min(val[1],val[2])

(感觉好难讲懂啊 \(QwQ\),不如先看一看代码?)


『源代码』

/*Lucky_Glass*/
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100000;
const ll INF=(1ll<<60);
struct GRAPH{
struct NODE{
int to,nxt;
NODE(){}
NODE(int _to,int _nxt):to(_to),nxt(_nxt){}
}edg[N*2+5];
int edgtot,adj[N+5];
void Init(){
edgtot=0;
memset(adj,-1,sizeof adj);
}
void AddEdge(int u,int v){
edg[++edgtot]=NODE(v,adj[u]);
adj[u]=edgtot;
}
}grp;
int n,m;
int cst[N+5],dep[N+5],fa[N+5][23];
ll dp[N+5][2],fdp[N+5][23][2][2];
void DFS1(int u,int pre){
dep[u]=dep[pre]+1;
dp[u][0]=0;dp[u][1]=cst[u];
for(int it=grp.adj[u];it!=-1;it=grp.edg[it].nxt){
int v=grp.edg[it].to;
if(v==pre) continue;
DFS1(v,u);
dp[u][0]+=dp[v][1];
dp[u][1]+=min(dp[v][0],dp[v][1]);
}
}
void DFS2(int u,int pre){
fa[u][0]=pre;
fdp[u][0][0][0]=INF;
fdp[u][0][1][0]=dp[pre][0];
fdp[u][0][0][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][0];
fdp[u][0][1][1]=dp[pre][1]-min(dp[u][0],dp[u][1])+dp[u][1];
for(int i=1;i<20;i++){
int anc=fa[u][i-1];
fa[u][i]=fa[anc][i-1];
fdp[u][i][0][0]=min(fdp[anc][i-1][0][0]-dp[anc][0]+fdp[u][i-1][0][0],fdp[anc][i-1][1][0]-dp[anc][1]+fdp[u][i-1][0][1]); //u -> anc -> fa[anc][i-1]
fdp[u][i][1][0]=min(fdp[anc][i-1][0][0]-dp[anc][0]+fdp[u][i-1][1][0],fdp[anc][i-1][1][0]-dp[anc][1]+fdp[u][i-1][1][1]);
fdp[u][i][0][1]=min(fdp[anc][i-1][0][1]-dp[anc][0]+fdp[u][i-1][0][0],fdp[anc][i-1][1][1]-dp[anc][1]+fdp[u][i-1][0][1]);
fdp[u][i][1][1]=min(fdp[anc][i-1][0][1]-dp[anc][0]+fdp[u][i-1][1][0],fdp[anc][i-1][1][1]-dp[anc][1]+fdp[u][i-1][1][1]);
}
for(int it=grp.adj[u];it!=-1;it=grp.edg[it].nxt){
int v=grp.edg[it].to;
if(v==pre) continue;
DFS2(v,u);
}
}
ll LCA(int u,int v,int U,int V){
if(dep[u]<dep[v]) swap(u,v),swap(U,V);
ll val[5]={0,(U? dp[u][1]:INF),(U? INF:dp[u][0]),(V? dp[v][1]:INF),(V? INF:dp[v][0])};
ll fval[5];
for(int i=19;i>=0;i--)
if(dep[fa[u][i]]>=dep[v]){
fval[1]=min(fdp[u][i][0][1]-dp[u][0]+val[2],fdp[u][i][1][1]-dp[u][1]+val[1]);
fval[2]=min(fdp[u][i][0][0]-dp[u][0]+val[2],fdp[u][i][1][0]-dp[u][1]+val[1]);
val[1]=fval[1];val[2]=fval[2];
u=fa[u][i];
}
int lca;
if(u==v){
if(V) val[2]=INF;
else val[1]=INF;
lca=v;
}
else{
for(int i=19;i>=0;i--)
if(fa[u][i]!=fa[v][i]){
fval[1]=min(fdp[u][i][0][1]-dp[u][0]+val[2],fdp[u][i][1][1]-dp[u][1]+val[1]);
fval[2]=min(fdp[u][i][0][0]-dp[u][0]+val[2],fdp[u][i][1][0]-dp[u][1]+val[1]);
fval[3]=min(fdp[v][i][0][1]-dp[v][0]+val[4],fdp[v][i][1][1]-dp[v][1]+val[3]);
fval[4]=min(fdp[v][i][0][0]-dp[v][0]+val[4],fdp[v][i][1][0]-dp[v][1]+val[3]);
val[1]=fval[1];val[2]=fval[2];val[3]=fval[3];val[4]=fval[4];
u=fa[u][i];v=fa[v][i];
}
lca=fa[u][0];
fval[1]=dp[lca][1]-min(dp[u][1],dp[u][0])-min(dp[v][1],dp[v][0])+min(val[1],val[2])+min(val[3],val[4]);
fval[2]=dp[lca][0]-dp[u][1]-dp[v][1]+val[1]+val[3];
val[1]=fval[1];val[2]=fval[2];
}
for(int i=19;i>=0;i--)
if(dep[fa[lca][i]]>=dep[1]){
fval[1]=min(fdp[lca][i][0][1]-dp[lca][0]+val[2],fdp[lca][i][1][1]-dp[lca][1]+val[1]);
fval[2]=min(fdp[lca][i][0][0]-dp[lca][0]+val[2],fdp[lca][i][1][0]-dp[lca][1]+val[1]);
val[1]=fval[1];val[2]=fval[2];
lca=fa[lca][i];
}
return min(val[1],val[2]);
}
int main(){
grp.Init();
scanf("%d%d%*s",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&cst[i]);
for(int i=1;i<n;i++){
int u,v;scanf("%d%d",&u,&v);
grp.AddEdge(u,v);grp.AddEdge(v,u);
}
DFS1(1,0);
DFS2(1,0);
while(m--){
int u,v,U,V;scanf("%d%d%d%d",&u,&U,&v,&V);
if((fa[u][0]==v || fa[v][0]==u) && (!U && !V)){
printf("-1\n");
continue;
}
printf("%lld\n",LCA(u,v,U,V));
}
return 0;
}

\(\mathcal{The\ End}\)

\(\mathcal{Thanks\ For\ Reading!}\)

有什么没讲清楚的就在邮箱 \(lucky\_glass@qq.com\) 里面问嘛~

竞赛题解 - NOIP2018 保卫王国的更多相关文章

  1. 竞赛题解 - NOIP2018 旅行

    \(\mathcal {NOIP2018} 旅行 - 竞赛题解\) 坑还得一层一层的填 填到Day2T1了 洛谷 P5022 题目 (以下copy自洛谷,有删减/修改 (●ˇ∀ˇ●)) 题目描述 小 ...

  2. 竞赛题解 - NOIP2018 赛道修建

    \(\mathcal {NOIP2018}\) 赛道修建 - 竞赛题解 额--考试的时候大概猜到正解,但是时间不够了,不敢写,就写了骗分QwQ 现在把坑填好了~ 题目 (Copy from 洛谷) 题 ...

  3. [NOIP2018]保卫王国 题解

    NOIP2018提高组D2T3 ddp虽然好想,但是码量有点大(其实是我不会),因此本文用倍增优化树形DP来解决本题. 题意分析 给一棵树染色,每个节点染色需要一定的花费,要求相邻两个节点至少要有一个 ...

  4. NOIP2018保卫王国

    题目大意:给一颗有点权的树,每次规定两个点选还是不选,求这棵树的最小权点覆盖. 题解 ZZ码农题. 要用动态dp做,这题就是板子,然鹅并不会,留坑代填. 因为没有修改,所以可以静态倍增. 我们先做一遍 ...

  5. [NOIP2018]保卫王国

    嘟嘟嘟 由于一些知道的人所知道的,不知道的人所不知道的原因,我来发NOIP2018day2T3的题解了. (好像我只是个搬运工--) 这题真可以叫做NOIplus了,跟其他几道比较水的题果然不一样,无 ...

  6. NOIP2018 保卫王国(动态DP)

    题意 求最小权值点覆盖. mmm次询问,每次给出两个点,分别要求每个点必须选或必须不选,输出每次的最小权值覆盖或者无解输出−1-1−1 题解 强制选或者不选可以看做修改权值为±∞\pm\infin±∞ ...

  7. 【比赛】NOIP2018 保卫王国

    DDP模板题 #include<bits/stdc++.h> #define ui unsigned int #define ll long long #define db double ...

  8. luogu5024 [NOIp2018]保卫王国 (动态dp)

    可以直接套动态dp,但因为它询问之间相互独立,所以可以直接倍增记x转移到fa[x]的矩阵 #include<bits/stdc++.h> #define CLR(a,x) memset(a ...

  9. 2019.02.16 bzoj5466: [Noip2018]保卫王国(链分治+ddp)

    传送门 题意简述: mmm次询问,每次规定两个点必须选或者不选,求树上的带权最小覆盖. 思路: 考虑链分治+ddpddpddp 仍然是熟悉的套路,先考虑没有修改的状态和转移: 令fi,0/1f_{i, ...

随机推荐

  1. vue中全局引入bootstrap.css

    1.首先在官网上下载bootstrap的压缩包(必须是官网上下载的) 将压缩包解压后引入在项目文件夹下面.如下图所示: 2.在main.js中引入 import './assets/bootstrap ...

  2. 动态注册broadcast的安全考虑

    一.android service通知activity更新方式有1. service 通过广播的形式发送broadcast,向这个activity的内部类发广播的消息来更新界面2. service直接 ...

  3. js前台实现上传图片的预览

    网上这样的插件一大堆,不过还是谈下js下代码的实现,加深这方面的理解. 当然也没有一种方式就可以完事的情形,主要就两种方面来处理: 1.file API的filereader接口完成(支持的浏览器:I ...

  4. 【Python机器学习及实践】笔记

  5. 【Z】段错误Segment Fault定位,即core dump文件与gdb定位

    使用C++开发系统有时会出现段错误,即Segment Fault.此类错误程序直接崩溃,通常没有任何有用信息输出,很难定位bug,因而无从解决问题.今天我们介绍core dump文件,并使用gdb进行 ...

  6. June 30th 2017 Week 26th Friday

    Love me little and love me long. 不求情意浓,但愿情意久. Some people say beautiful young people are the creatur ...

  7. Event Driven Architecture

    在微服务中使用领域事件   稍微回想一下计算机硬件的工作原理我们便不难发现,整个计算机的工作过程其实就是一个对事件的处理过程.当你点击鼠标.敲击键盘或者插上U盘时,计算机便以中断的形式处理各种外部事件 ...

  8. 解决Ubuntu启动错误——kernel panic not syncing vfs unable to mount root fs on unknown-block 0 0 – error

    最近在倒腾Ubuntu,然后想着怎么美化一下界面,于是照着网上的教程整了一下Flatabulous这个软件,然后好像/boot就满了.关机之后再开机就出现了如题所述的错误,无法开机,也无法进入reco ...

  9. 如何选择PHP项目的开发方案?

    我说的项目开发方案并不是谈论到底用不用PHP去开发的问题,而是当你遇到一个项目,已经决定了用PHP,然后才来看的问题:用PHP的什么开发方案. 基本上有这么几种方案.各有各的说法,良莠不齐,我就谈谈我 ...

  10. Kali-linux枚举服务

    枚举是一类程序,它允许用户从一个网络中收集某一类的所有相关信息.本节将介绍DNS枚举和SNMP枚举技术.DNS枚举可以收集本地所有DNS服务和相关条目.DNS枚举可以帮助用户收集目标组织的关键信息,如 ...