[NOIP2018]保卫王国 题解
NOIP2018提高组D2T3
ddp虽然好想,但是码量有点大(其实是我不会),因此本文用倍增优化树形DP来解决本题。
题意分析
给一棵树染色,每个节点染色需要一定的花费,要求相邻两个节点至少要有一个被染色,给出一些限制条件,求满足每个限制条件的最小花费为多少。
思路分析
首先判断无解的情况。显然,只有当$a,b$互为父子关系(这里的父子关系指的是严格相邻),且$x,y$都为0时才无解,其它情况都可以通过多染色来解。
很容易想到树形DP,那么具体状态如何设置呢?对于每个限制条件给出的两个点$a,b$,答案可以由四个部分构成:以$a$为根的子树,以$b$为根的子树,以$lca(a,b)$为根的子树减去以$a$为根的子树和以$b$为根的子树,整棵树减去以$lca(a,b)$为根的子树。因为对$a,b$的限制会影响到从$a$到$b$的链上的染色状态,因此拆成这四部分,这四部分互相不影响。
显然,对$a,b$的限制只会影响到以$lca(a,b)$为根的子树减去以$a$为根的子树和以$b$为根的子树的答案,因此,我们可以将其它三个部分的答案预处理出来,然后DP处理剩下的这一部分。
因此状态可以设定为:$f1[i][1/0]$表示以$i$为根的子树当$i$染色/不染色时的最小花费,$f2[i][1/0]$表示整棵树减去以$i$为根的子树当$i$染色/不染色时的最小花费,$dp[i][1/0][1/0]$表示以$lca(a,b)$为根的子树减去以$i$为根的子树当$i$染色/不染色及$i$的父节点染色/不染色(按顺序对应第二维和第三维)时的最小花费。这样,我们就可以从$a,b$一直DP到$lca(a,b)$处计算答案了。特别地,当当前状态无解时,值设定为INF。
但是这样的时间复杂度显然太大。类似这样在树上多次向上的DP,可以想到利用树上倍增进行优化。更改一下$dp$数组的定义,$dp[i][j][0/1][0/1]$表示以$i$的$2^j$辈祖先为根的子树减去以$i$为根的子树当$i$染色/不染色及$i$的$2^j$辈祖先染色/不染色时的最小花费。
接下来的问题就是如何进行DP了。利用上面的三个数组,我们可以倍增地DP到$lca(a,b)$处。首先,我们先将$a,b$中深度较大的一个向上倍增DP,直到$a,b$处于同一深度;此时若$a=b$,说明它们原本具有祖先与后代的关系,直接输出答案;否则就将$a,b$同时向上倍增DP,直到$lca(a,b)$的子节点处,然后进行最后的处理,输出答案。DP的时候,枚举状态转移即可。
整理一下思路:
1. 预处理出$f1,f2,dp$三个数组
1. 将$a,b$中深度较大的一个向上倍增直到$a,b$处于同一深度
1. 判断此时$a,b$是否相等,如果是,直接输出答案;否则继续向上倍增
1. 将$a,b$倍增到$lca(a,b)$的子节点处,进行最后的处理,输出答案
接下来将对具体的实现步骤进行讲解。
具体实现
1. 预处理$f1$数组
很基础的一个树形DP,类似于没有上司的舞会,就不再赘述了。同时也处理出深度数组$d$和倍增祖先数组$f$。
- void dfs1(int fa,int x)
- {
- f[x][]=fa,d[x]=d[fa]+,f1[x][]=p[x];
- for(int i=head[x],y;i;i=Next[i])
- if(fa!=(y=ver[i]))
- {
- dfs1(x,y);
- f1[x][]+=f1[y][];
- f1[x][]+=min(f1[y][],f1[y][]);
- }
- }
2. 预处理$f2$数组
当$i$不染色时,$i$的父亲节点必须染色,因此答案就是当$i$的父亲染色时,整棵树减去以$i$的父亲为根的子树的答案,加上以$i$的兄弟(即父亲相同的节点)为根的子树为根的答案。回顾递推$f1$数组的过程,以$i$的兄弟为根的子树为根的答案相当于统计时少了$i$的统计,即$f1[fa][1]-min(f1[i][0],f1[i][1])$,其中$fa$表示$i$的父亲。
当$i$染色时,$i$的父亲染不染色都可以,$i$的父亲染色时和$i$不染色时的答案是一样的,不染色时同理统计,相当于统计$f1$时撤销了对$i$的统计。
- void dfs2(int x)
- {
- for(int i=head[x],y;i;i=Next[i])
- if(f[x][]!=(y=ver[i]))
- {
- f2[y][]=f2[x][]+f1[x][]-min(f1[y][],f1[y][]);
- f2[y][]=min(f2[y][],f2[x][]+f1[x][]-f1[y][]);
- dfs2(y);
- }
- }
3. 预处理$dp$数组
先将$dp$数组初始化为无限大。首先先处理dp初值,即$j=0$的情况。
显然,当$i$和$i$的父节点都不染色时,是无解的,不动它;当$i$的父亲染色时,$i$染不染色都可以,因此答案和上面$f2[i][0]$的转移类似,只是不用加上剩下的部分;当$i$的父亲不染色时,$i$必须染色,转移类似,就不再赘述了。
然后就是倍增地处理,对于第$2^j$辈祖先,枚举第$2^{j-1}$辈祖先及$i$和第$2^j$辈祖先的状态进行转移。转移思想也基本类似。
- void pre()
- {
- memset(dp,0x3f3f,sizeof(dp));
- dfs1(,),dfs2();//先处理f1,f2数组
- for(int i=;i<=n;i++)
- {
- dp[i][][][]=dp[i][][][]=f1[f[i][]][]-min(f1[i][],f1[i][]);//父节点染色
- dp[i][][][]=f1[f[i][]][]-f1[i][];//父节点不染色
- }
- for(int j=;j<=;j++)
- for(int i=;i<=n;i++)
- {
- int fa=f[i][j-];
- f[i][j]=f[fa][j-];//先计算祖先
- for(int x=;x<;x++)//枚举当前点的状态
- for(int y=;y<;y++)//枚举第2^j辈祖先的状态
- for(int z=;z<;z++)//枚举第2^(j-1)辈祖先的状态
- dp[i][j][x][y]=min(dp[i][j][x][y],dp[i][j-][x][z]+dp[fa][j-][z][y]);
- }
- }
接下来对询问的处理进行讲解。
4. 将$a,b$中深度较大的一个上移,直到$a,b$处于同一深度
我们可以先令深度较大的一个为$a$,若不是,则交换。
定义四个数组$ansa[0/1],ansb[0/1],nowa[0/1],nowb[0/1]$,分别代表$a,b$不染色/染色时最终的答案和当前的答案。具体地说,这个答案就是以当前倍增到的这个祖先为根的子树的答案。
处理之前,因为限制条件,将$a,b$点的另一个状态初始化为INF。
转移的时候,一样地,枚举中间点的状态进行转移。每次转移前,将$nowa$数组初始化为INF;每次转移后,将$nowa$数组的值赋给$ansa$数组,然后将$a$上移。
上移之后,若$a=b$,则直接返回答案。因为当前点是$b$,而限制条件对$b$起作用,因此答案就是$ansa[y]+f2[a][y]$,即以$b$为根的子树的答案加上剩下部分的答案。
- if(d[a]<d[b])
- swap(a,b),swap(x,y);//令x为深度较大的点
- ansa[-x]=ansb[-y]=INF;
- ansa[x]=f1[a][x],ansb[y]=f1[b][y];//初值
- for(int j=;j>=;j--)//倍增
- if(d[f[a][j]]>=d[b])//上移到同一深度
- {
- nowa[]=nowa[]=INF;//初始化
- for(int u=;u<;u++)//枚举2^j辈祖先的状态
- for(int v=;v<;v++)//枚举当前点的状态
- nowa[u]=min(nowa[u],ansa[v]+dp[a][j][v][u]);
- ansa[]=nowa[],ansa[]=nowa[],a=f[a][j];//数组值转移
- }
- if(a==b)
- return ansa[y]+f2[a][y];//相等直接返回
5. 将$a,b$同时上移到$lca(a,b)$的子节点处
与上一步类似,只是同时上移,就不赘述了。
- for(int j=;j>=;j--)
- if(f[a][j]!=f[b][j])
- {
- nowa[]=nowa[]=nowb[]=nowb[]=INF;
- for(int u=;u<;u++)
- for(int v=;v<;v++)
- {
- nowa[u]=min(nowa[u],ansa[v]+dp[a][j][v][u]);
- nowb[u]=min(nowb[u],ansb[v]+dp[b][j][v][u]);
- }
- ansa[]=nowa[],ansa[]=nowa[],ansb[]=nowb[],ansb[]=nowb[],a=f[a][j],b=f[b][j];
- }
6. 对$a,b,lca(a,b)$的状态进行枚举,进行最后的处理
还是一样的思想,若$lca(a,b)$染色,则$a,b$染不染色都行;否则$a,b$必须染色。式子看起来很长,实际上只是之前的式子多了一个节点罢了。
- int fa=f[a][];
- ans0=f1[fa][]-f1[a][]-f1[b][]+f2[fa][]+ansa[]+ansb[];//lca(a,b)不染色
- ans1=f1[fa][]-min(f1[a][],f1[a][])-min(f1[b][],f1[b][])+f2[fa][]+min(ansa[],ansa[])+min(ansb[],ansb[]);//lca(a,b)染色
- return min(ans0,ans1);
完结撒花~
(所以对于正解来说type并没有什么用,不过在考场上打不出正解的时候确实是拿部分分的一个好助手)
------------
最后奉上完整代码:
- #include<iostream>
- #include<cstdio>
- #include<cstring>
- #include<algorithm>
- #define ll long long
- using namespace std;
- const int N=2e5,M=3e5,L=;
- const ll INF=1e17;
- int n,m,tot;
- int p[N],d[N],f[N][L];
- int head[N],ver[*M],Next[*M];
- ll ans0,ans1;
- ll nowa[],nowb[],ansa[],ansb[],f1[N][],f2[N][],dp[N][L][][];
- string tp;
- void add(int x,int y)
- {
- ver[++tot]=y,Next[tot]=head[x],head[x]=tot;
- ver[++tot]=x,Next[tot]=head[y],head[y]=tot;
- }
- void dfs1(int fa,int x)
- {
- f[x][]=fa,d[x]=d[fa]+,f1[x][]=p[x];
- for(int i=head[x],y;i;i=Next[i])
- if(fa!=(y=ver[i]))
- {
- dfs1(x,y);
- f1[x][]+=f1[y][];
- f1[x][]+=min(f1[y][],f1[y][]);
- }
- }//1. 预处理 $f1$ 数组
- void dfs2(int x)
- {
- for(int i=head[x],y;i;i=Next[i])
- if(f[x][]!=(y=ver[i]))
- {
- f2[y][]=f2[x][]+f1[x][]-min(f1[y][],f1[y][]);
- f2[y][]=min(f2[y][],f2[x][]+f1[x][]-f1[y][]);
- dfs2(y);
- }
- }//2. 预处理 $f2$ 数组
- void pre()
- {
- memset(dp,0x3f3f,sizeof(dp));
- dfs1(,),dfs2();
- for(int i=;i<=n;i++)
- {
- dp[i][][][]=dp[i][][][]=f1[f[i][]][]-min(f1[i][],f1[i][]);
- dp[i][][][]=f1[f[i][]][]-f1[i][];
- }
- for(int j=;j<=;j++)
- for(int i=;i<=n;i++)
- {
- int fa=f[i][j-];
- f[i][j]=f[fa][j-];
- for(int x=;x<;x++)
- for(int y=;y<;y++)
- for(int z=;z<;z++)
- dp[i][j][x][y]=min(dp[i][j][x][y],dp[i][j-][x][z]+dp[fa][j-][z][y]);
- }
- }//3. 预处理 $dp$ 数组
- bool check(int a,int x,int b,int y)
- {
- return !x && !y &&(f[a][]==b || f[b][]==a);
- }
- ll ask(int a,int x,int b,int y)
- {
- if(d[a]<d[b])
- swap(a,b),swap(x,y);
- ansa[-x]=ansb[-y]=INF;
- ansa[x]=f1[a][x],ansb[y]=f1[b][y];
- for(int j=;j>=;j--)
- if(d[f[a][j]]>=d[b])
- {
- nowa[]=nowa[]=INF;
- for(int u=;u<;u++)
- for(int v=;v<;v++)
- nowa[u]=min(nowa[u],ansa[v]+dp[a][j][v][u]);
- ansa[]=nowa[],ansa[]=nowa[],a=f[a][j];
- }
- if(a==b)
- return ansa[y]+f2[a][y];//4. 将 $a,b$ 中深度较大的一个上移,直到 $a,b$ 处于同一深度
- for(int j=;j>=;j--)
- if(f[a][j]!=f[b][j])
- {
- nowa[]=nowa[]=nowb[]=nowb[]=INF;
- for(int u=;u<;u++)
- for(int v=;v<;v++)
- {
- nowa[u]=min(nowa[u],ansa[v]+dp[a][j][v][u]);
- nowb[u]=min(nowb[u],ansb[v]+dp[b][j][v][u]);
- }
- ansa[]=nowa[],ansa[]=nowa[],ansb[]=nowb[],ansb[]=nowb[],a=f[a][j],b=f[b][j];
- }//5. 将 $a,b$ 同时上移到 $lca(a,b)$ 的子节点处
- int fa=f[a][];
- ans0=f1[fa][]-f1[a][]-f1[b][]+f2[fa][]+ansa[]+ansb[];
- ans1=f1[fa][]-min(f1[a][],f1[a][])-min(f1[b][],f1[b][])+f2[fa][]+min(ansa[],ansa[])+min(ansb[],ansb[]);
- return min(ans0,ans1);//6. 对 $a,b,lca(a,b)$ 的状态进行枚举,进行最后的处理
- }
- int main()
- {
- scanf("%d%d",&n,&m);cin>>tp;
- for(int i=;i<=n;i++)
- scanf("%d",&p[i]);
- for(int i=;i<n;i++)
- {
- int x,y;
- scanf("%d%d",&x,&y);
- add(x,y);
- }
- pre();
- for(int i=,a,x,b,y;i<=m;i++)
- {
- scanf("%d%d%d%d",&a,&x,&b,&y);
- if(check(a,x,b,y))
- {
- puts("-1");
- continue;
- }
- printf("%lld\n",ask(a,x,b,y));
- }
- return ;
- }
[NOIP2018]保卫王国 题解的更多相关文章
- 竞赛题解 - NOIP2018 保卫王国
\(\mathcal{NOIP2018}\) 保卫王国 - 竞赛题解 按某一个炒鸡dalao名曰 taotao 的话说: \(\ \ \ \ \ \ \ \ \ "一道sb倍增题" ...
- NOIP2018保卫王国
题目大意:给一颗有点权的树,每次规定两个点选还是不选,求这棵树的最小权点覆盖. 题解 ZZ码农题. 要用动态dp做,这题就是板子,然鹅并不会,留坑代填. 因为没有修改,所以可以静态倍增. 我们先做一遍 ...
- [NOIP2018]保卫王国
嘟嘟嘟 由于一些知道的人所知道的,不知道的人所不知道的原因,我来发NOIP2018day2T3的题解了. (好像我只是个搬运工--) 这题真可以叫做NOIplus了,跟其他几道比较水的题果然不一样,无 ...
- NOIP2018 保卫王国(动态DP)
题意 求最小权值点覆盖. mmm次询问,每次给出两个点,分别要求每个点必须选或必须不选,输出每次的最小权值覆盖或者无解输出−1-1−1 题解 强制选或者不选可以看做修改权值为±∞\pm\infin±∞ ...
- 【比赛】NOIP2018 保卫王国
DDP模板题 #include<bits/stdc++.h> #define ui unsigned int #define ll long long #define db double ...
- luogu5024 [NOIp2018]保卫王国 (动态dp)
可以直接套动态dp,但因为它询问之间相互独立,所以可以直接倍增记x转移到fa[x]的矩阵 #include<bits/stdc++.h> #define CLR(a,x) memset(a ...
- 2019.02.16 bzoj5466: [Noip2018]保卫王国(链分治+ddp)
传送门 题意简述: mmm次询问,每次规定两个点必须选或者不选,求树上的带权最小覆盖. 思路: 考虑链分治+ddpddpddp 仍然是熟悉的套路,先考虑没有修改的状态和转移: 令fi,0/1f_{i, ...
- [NOIP2018]保卫王国(树形dp+倍增)
我的倍增解法吊打动态 \(dp\) 全局平衡二叉树没学过 先讲 \(NOIP\) 范围内的倍增解法. 我们先考虑只有一个点取/不取怎么做. \(f[x][0/1]\) 表示取/不取 \(x\) 后,\ ...
- 「NOIP2018 保卫王国」
题目 强制选点我们可以把那个点权搞成\(-inf\),强制不选我们搞成\(inf\),之后就真的成为动态\(dp\)的板子题了 由于不想像板子那样再写一个最大独立集的方程,之后利用最小点覆盖=总点权- ...
随机推荐
- PHP date_parse_from_format() 函数
------------恢复内容开始------------ 实例 根据指定的格式返回一个包含指定日期信息的关联数组: <?phpprint_r(date_parse_from_format(& ...
- 线性DP 学习笔记
前言:线性DP是DP中最基础的.趁着这次复习认真学一下,打好基础. ------------------ 一·几点建议 1.明确状态的定义 比如:$f[i]$的意义是已经处理了前$i个元素,还是处理第 ...
- 012_go语言中的Functions 函数
代码演示 package main import "fmt" func plus(a int, b int) int { return a + b } func plusPlus( ...
- 用 Python 可以实现侧脸转正脸?我也要试一下!
作者 | 李秋键 责编 | Carol 封图 | CSDN 下载自视觉中国 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻找案例上手.很多已经做案例 ...
- Python分析「我们为什么这么穷」
- Paper English
论文中的英语 单词 a arange 整理 ambiguity 含糊的 aggregate 总量 auxiliary 辅助的 alleviate 缓解 aberrant 异常的 akin 类似的 Ac ...
- java_线程、同步、线程池
线程 Java使用 java.lang.Thread 类代表线程,所有的线程对象都必须是Thread类或其子类的实例 Thread类常用方法 构造方法 public Thread():分配一个新的线程 ...
- 微信小程序通过二维码获取参数运行
小程序开发过程中会遇到参数id会通过二维码获取,然后执行接口获取数据,但是难免会遇到带过来的参数出现乱码,这样就需要解码,多个参数时就需要进行处理取我们需要的字段值:小程序开发过程中会遇到参数id会通 ...
- 11 Linux(CentOS)用户与权限
用户管理 root 在root用户下可以进行以下操作 useradd 用户名 新增用户 userdel 用户名 删除用户,同时应该删除家目录下的用户文件,与邮箱目录下的用户文件 ...
- java 多态二
一 多态-转型 多态的转型分为向上转型与向下转型两种: 向上转型:当有子类对象赋值给一个父类引用时,便是向上转型,多态本身就是向上转型的过程. 使用格式: 父类类型 变量名 = new 子类类型() ...