\(\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. C语言--清理getchar缓存

    getchar()采用了缓冲区,而getch()才是立即获取,所以要想再用getchar()获取正确的值必须先清空缓冲区,如果是windows操作系统,用fflush(stdin)函数或rewind( ...

  2. 10分钟学会Less开发环境搭建与初体验

    Less 是一门 CSS 预处理语言,它扩充了 CSS 语言,增加了诸如变量.混合(mixin).函数等功能,让 CSS 更易维护.方便制作主题.扩充. 今天看一下,10分钟能不能手把手快速教会你Le ...

  3. 微信小程序支付返回信息为空

    1.昨天公司说要实现微信小程序的支付,于是看了下微信小程序的开发api文档,和之前的app  端以及pc端基本相似:于是让他们把参数改了下,把之前的trade_type 由 app 改成 小程序要求的 ...

  4. Android已上线应用开源分享中(第二季)

    昨天和大家分享了我Android上线的第一个应用,大家还是挺支持的,很高兴,虽然作品没有那么高大上,但是很是有了一点小小的成就感,所以打算继续开源我上线的一些应用,和大家一起交流一下. 我这个作品是一 ...

  5. redis持久化方法

    1.redis持久化,来自官方说明 如何选择使用哪种持久化方式? 一般来说, 如果想达到足以媲美 PostgreSQL 的数据安全性, 你应该同时使用两种持久化功能. 如果你非常关心你的数据, 但仍然 ...

  6. ESXi中的虚拟机如何使用U盘和加密Key

    最近想在虚拟机中的主机使用通讯加密key来部署专门用于转发和加密的通讯前置机.故查询测试一下是不是能识别. 第一步:在虚拟宿主机上添加USB控制器,右键单击目标虚拟机,选中"编辑设置&quo ...

  7. Python学习---重点模块之re

    正则表达式是用来操作字符串,但是字符串提供的正则是完全匹配,有时候我们需要进行模糊匹配,这个时候就需要正则表达式了.通过re模块来实现,由C语言来执行底层的匹配 字符匹配(普通字符,元字符): 1 普 ...

  8. ignore_user_abort函数制定计划任务

    PHP中的ignore_user_abort函数是当用户关掉终端后脚本不停止仍然在执行,可以用它来实现计划任务与持续进程,下面会通过实例讨论ignore_user_abort()函数的作用与用法.   ...

  9. zt 我们要找出毒素的藏身处,尽快把它赶出身体

    在中医看来,我们体内有很多毒素,凡是不能及时排出体外.对我们的身体和精神会产生不良作用的物质都可以称为“毒”,例如瘀血.痰湿.寒气.食积.气郁. 上火.这些毒素堆积在五脏之内,就会加速五脏的衰老,然后 ...

  10. good pics