题目

还是懒得把题目放上来了。

大意:给你一棵带点权的树,你要花费一些代价选择一些点使得相邻的两个点至少有一个被选。

然后有很多个询问,每个询问强制两个点的状态,问强制了这两个点的状态后的方案。


比赛思路

没时间了,没时间了……

匆匆打个44分的暴力就好了。

结果混淆了概念,打出来的DP是求一个点自己或周围至少有一个选的方案,和题目就不是一个样子。

比赛结束了,我还没有调处来,然后就爆0了。


解法

先说说暴力。

这是一个非常典型的问题,设fi,0/1f_{i,0/1}fi,0/1​表示以iii为根的子树中,不选或选iii的最优解。

(比赛时设的根本就不是同一道题)

这个DP的方程应该没有人不懂的吧:

fi,0=∑fson,1fi,1=∑min⁡(fson,0,fson,1)f_{i,0}=\sum f_{son,1} \\ f_{i,1}=\sum \min\left(f_{son,0},f_{son,1}\right)fi,0​=∑fson,1​fi,1​=∑min(fson,0​,fson,1​)

所以,暴力做法就是,将某一个值赋值为无限大,然后暴力地重新DP(当然,其实只需要更新它自己到根的这条路径就好了)。

考虑正解。

首先说一下,我的解法是在JZOJ、洛谷排名第一的并查集解法,时间复杂度几乎是线性的。倍增解法或许和并查集解法有很大相似之处,而对于那些打树链剖分的动态DP的方法,个人认为我的方法和他们的方法存在着太大的差别。

首先我们可以抽象地思考一下:

对于这个询问,其实就是将询问的两个点提起来,答案为限制了它们之后,链上的子树的贡献。

首先我们处理另一个DP,设gi,0/1g_{i,0/1}gi,0/1​表示除以iii为根的子树外,不选或选iii的最优解。(将它提起来之后,之前的父亲也可以看成一个儿子。)

这个方程也是挺好想的(为了方便表达,设fi,2=min⁡(fi,0,fi,1)f_{i,2}=\min\left(f_{i,0},f_{i,1}\right)fi,2​=min(fi,0​,fi,1​)):

gson,0=gi,1+(fi,1−fson,2)gson,1=min⁡(gi,0+(fi,0−fson,1),gi,1+(fi,1−fson,2))g_{son,0}=g_{i,1}+(f_{i,1}-f_{son,2}) \\
g_{son,1}=\min\left(g_{i,0}+(f_{i,0}-f_{son,1}),g_{i,1}+(f_{i,1}-f_{son,2})\right)gson,0​=gi,1​+(fi,1​−fson,2​)gson,1​=min(gi,0​+(fi,0​−fson,1​),gi,1​+(fi,1​−fson,2​))

这个方程是从上往下转移的。

其中fi,1−fson,2f_{i,1}-f_{son,2}fi,1​−fson,2​中,由fff的转移方程得fi,1−fson,2=(∑fson′,2)−fson,2=∑son′≠sonfson′,2f_{i,1}-f_{son,2}=\left(\sum f_{son',2}\right)-f_{son,2}=\sum_{son'\neq son} f_{son',2}fi,1​−fson,2​=(∑fson′,2​)−fson,2​=∑son′̸​=son​fson′,2​

下面的那个类似。

这样子DP部分就搞完了,剩下的东西就是维护。

我们可以参考一下Tarjan求LCA的过程(这个名字有毒,和强联通分量的那个完全不是一个东西,不要被名字震撼到),其实也就是用并查集求LCA的过程。

简要地说一下过程:

对于一个节点uuu,首先fau=ufa_u=ufau​=u,然后dfs它的儿子。当从它的儿子那里回溯上来的时候,fason=ufa_{son}=ufason​=u,然后枚举和uuu有关联的询问,设另一个点为vvv,如果fav≠0fa_v \neq 0fav​̸​=0,则它还未被访问过,先不理它;否则,LCALCALCA就是getfather(v)getfather(v)getfather(v)。

至于这个算法是为什么,其实随便想一想就可以了。在dfs的时候,先到LCALCALCA,再到vvv,回溯上去,在LCALCALCA处转弯,再走到uuu。由此可见,自vvv到uuu,深度最小的地方就是它们的LCALCALCA(莫名其妙地想起了ST表求LCA),深度最小的地方也就是vvv在并查集上的最远祖先。

对于每个点,我们不只是记录一下它在并查集上的父亲,还要记录一下它和他父亲之间的答案。

每个节点的答案记录444条信息,表示父亲选或不选和它选或不选。

这个答案表示,在这个状态下,这条链上挂着的子树的最小答案。

我们可以在求LCALCALCA的递归中,在从儿子回来的时候,对儿子的答案信息进行初始化:

hson,00=∞hson,01=fu,0−fson,1+fson,1hson,10=fu,1−fson,2+fson,0hson,11=fu,1−fson,2+fson,1h_{son,00}=\infty \\ h_{son,01}=f_{u,0}-f_{son,1}+f_{son,1} \\ h_{son,10}=f_{u,1}-f_{son,2}+f_{son,0}\\ h_{son,11}=f_{u,1}-f_{son,2}+f_{son,1}hson,00​=∞hson,01​=fu,0​−fson,1​+fson,1​hson,10​=fu,1​−fson,2​+fson,0​hson,11​=fu,1​−fson,2​+fson,1​

首先,由于相邻的两个不能都不选,所以设为无限大。

其它的东西,都是它父亲的贡献,减掉儿子对父亲的贡献,再加上儿子自己的贡献。

对于这个东西,我们可以在getfathergetfathergetfather的过程中顺便维护它们的值。

维护它的值就是要将两条链(其中有一个交点)的答案合并起来。

合并其实很简答,只需要枚举一下交点的状态,然后接起来并且减去重复的贡献就好了。

newij=min⁡(hup,i0−fup,0+hdown,0j,hup,i1−fup,1+hdown,1j)new_{ij}=\min\left(h_{up,i0}-f_{up,0}+h_{down,0j},h_{up,i1}-f_{up,1}+h_{down,1j}\right)newij​=min(hup,i0​−fup,0​+hdown,0j​,hup,i1​−fup,1​+hdown,1j​)

其中downdowndown表示下面的点,upupup表示上面的点(其实也就是fadownfa_{down}fadown​,同时也是两条链的交点)

这个东西其实就是核心操作了。

当你在uuu求出(u,v)(u,v)(u,v)的LCALCALCA的时候,不要急着求答案,因为getfather(v)getfather(v)getfather(v)已经上去了,而getfather(u)getfather(u)getfather(u)还没有上去。所以,求出LCALCALCA之后,我们可以将询问再挂到LCALCALCA上,在LCALCALCA处计算答案。

在计算的时候,自然是分成两种情况:

当一个是另一个的祖先时,设aaa为bbb的祖先,那么ans=hb,xy+ga,xans=h_{b,xy}+g_{a,x}ans=hb,xy​+ga,x​

否则,枚举LCA的状态,ans=min⁡(ha,0x+hb,0y−flca,0+glca,0,ha,1x+hb,1y−flca,1+glca,1)ans=\min(h_{a,0x}+h_{b,0y}-f_{lca,0}+g_{lca,0},h_{a,1x}+h_{b,1y}-f_{lca,1}+g_{lca,1})ans=min(ha,0x​+hb,0y​−flca,0​+glca,0​,ha,1x​+hb,1y​−flca,1​+glca,1​)

时间复杂度O(nα(n))O(n \alpha (n))O(nα(n))


代码

  1. using namespace std;
  2. #include <cstdio>
  3. #include <cstring>
  4. #include <algorithm>
  5. #define N 100000
  6. #define M 100000
  7. #define INF 1000000000000ll
  8. int n,m;
  9. int a[N+1];
  10. struct EDGE{
  11. int to;
  12. EDGE *las;
  13. } e[N*2+1];
  14. int ne;
  15. EDGE *last[N+1];
  16. long long f[N+1][3],g[N+1][2];
  17. void init1(int x,int fa){
  18. f[x][0]=0,f[x][1]=a[x];
  19. for (EDGE *ei=last[x];ei;ei=ei->las)
  20. if (ei->to!=fa){
  21. init1(ei->to,x);
  22. f[x][0]+=f[ei->to][1];
  23. f[x][1]+=f[ei->to][2];
  24. }
  25. f[x][2]=min(f[x][0],f[x][1]);
  26. }
  27. void init2(int x,int fa){
  28. for (EDGE *ei=last[x];ei;ei=ei->las)
  29. if (ei->to!=fa){
  30. g[ei->to][0]=g[x][1]+(f[x][1]-f[ei->to][2]);
  31. g[ei->to][1]=min(g[x][0]+(f[x][0]-f[ei->to][1]),g[ei->to][0]);
  32. init2(ei->to,x);
  33. }
  34. }
  35. struct Query{
  36. int a,x,b,y;
  37. int lca;
  38. } q[M+1];
  39. struct list{
  40. int v;
  41. list *lst;
  42. } d[M*3+1];//这是链表开的内存池
  43. int cnt;
  44. list *qv[N+1],*ql[N+1];//qv[u]表示与u有关的询问 ql[u]表示lca为u的询问
  45. inline void insert(list * &end,int v){
  46. ++cnt;
  47. d[cnt].v=v,d[cnt].lst=end;
  48. end=d+cnt;
  49. }
  50. int top[N+1];//表示并查集上的父亲(我才不喜欢打fa)
  51. long long h[N+1][4];//表示的时候直接压位了……自认为好打一些
  52. inline void merge(int down,int up){//合并操作,将h[down]变为h[down]+h[up]
  53. static long long res[4];
  54. res[0]=min(h[up][0]-f[up][0]+h[down][0],h[up][1]-f[up][1]+h[down][2]);
  55. res[1]=min(h[up][0]-f[up][0]+h[down][1],h[up][1]-f[up][1]+h[down][3]);
  56. res[2]=min(h[up][2]-f[up][0]+h[down][0],h[up][3]-f[up][1]+h[down][2]);
  57. res[3]=min(h[up][2]-f[up][0]+h[down][1],h[up][3]-f[up][1]+h[down][3]);
  58. memcpy(&h[down],res,sizeof res);
  59. }
  60. void gettop(int x){
  61. if (top[top[x]]==top[x])
  62. return;
  63. gettop(top[x]);
  64. merge(x,top[x]);//在gettop过程中,合并两个答案信息
  65. top[x]=top[top[x]];
  66. }
  67. void dfs(int,int);
  68. long long ans[N+1];
  69. int main(){
  70. freopen("defense.in","r",stdin);
  71. freopen("defense.out","w",stdout);
  72. scanf("%d%d%*s",&n,&m);
  73. for (int i=1;i<=n;++i)
  74. scanf("%d",&a[i]);
  75. for (int i=1;i<n;++i){
  76. int u,v;
  77. scanf("%d%d",&u,&v);
  78. ++ne;
  79. e[ne].to=v,e[ne].las=last[u],last[u]=e+ne;
  80. ++ne;
  81. e[ne].to=u,e[ne].las=last[v],last[v]=e+ne;
  82. }
  83. init1(1,0);
  84. init2(1,0);
  85. for (int i=1;i<=m;++i){
  86. scanf("%d%d%d%d",&q[i].a,&q[i].x,&q[i].b,&q[i].y);
  87. insert(qv[q[i].a],i);
  88. insert(qv[q[i].b],i);
  89. }
  90. dfs(1,0);
  91. for (int i=1;i<=m;++i)
  92. printf("%lld\n",ans[i]>=INF?-1:ans[i]);
  93. return 0;
  94. }
  95. void dfs(int x,int fa){
  96. top[x]=x;
  97. for (list *i=qv[x];i;i=i->lst){
  98. int u=q[i->v].a^q[i->v].b^x;//表示a,b中除了x以外的另一个数
  99. if (!top[u])
  100. continue;
  101. gettop(u);
  102. q[i->v].lca=top[u];//求出LCA
  103. insert(ql[q[i->v].lca],i->v);//将询问挂在LCA上
  104. }
  105. for (EDGE *ei=last[x];ei;ei=ei->las)
  106. if (ei->to!=fa){
  107. dfs(ei->to,x);
  108. //一坨初始化,具体解释见上
  109. h[ei->to][0]=INF;
  110. h[ei->to][1]=f[x][0]-f[ei->to][1]+f[ei->to][1];
  111. h[ei->to][2]=f[x][1]-f[ei->to][2]+f[ei->to][0];
  112. h[ei->to][3]=f[x][1]-f[ei->to][2]+f[ei->to][1];
  113. top[ei->to]=x;
  114. }
  115. for (list *i=ql[x];i;i=i->lst){
  116. int a=q[i->v].a,b=q[i->v].b;
  117. gettop(a),gettop(b);
  118. //具体解释见上
  119. if (x==a)
  120. ans[i->v]=h[b][q[i->v].x<<1|q[i->v].y]+g[a][q[i->v].x];
  121. else if (x==b)
  122. ans[i->v]=h[a][q[i->v].y<<1|q[i->v].x]+g[b][q[i->v].y];
  123. else
  124. ans[i->v]=min(h[a][0<<1|q[i->v].x]+h[b][0<<1|q[i->v].y]-f[x][0]+g[x][0],h[a][1<<1|q[i->v].x]+h[b][1<<1|q[i->v].y]-f[x][1]+g[x][1]);
  125. }
  126. }

总结

只要不修改,离线之后,并查集方法有时可以代替倍增。

举个例子,询问树上两点之间的最大值……

用并查集维护它到fafafa这条链上的信息,然后在LCALCALCA处计算就好了。

JZOJ5966【NOIP2018提高组D2T3】保卫王国(并查集)的更多相关文章

  1. 【数据结构】运输计划 NOIP2015提高组D2T3

    [数据结构]运输计划 NOIP2015提高组D2T3 >>>>题目 [题目描述] 公元 2044 年,人类进入了宇宙纪元.L 国有 n 个星球,还有 n−1 条双向航道,每条航 ...

  2. [NOIp2018提高组]旅行

    [NOIp2018提高组]旅行: 题目大意: 一个\(n(n\le5000)\)个点,\(m(m\le n)\)条边的连通图.可以从任意一个点出发,前往任意一个相邻的未访问的结点,或沿着第一次来这个点 ...

  3. [NOIp2018提高组]赛道修建

    [NOIp2018提高组]赛道修建 题目大意: 给你一棵\(n(n\le5\times10^4)\)个结点的树,从中找出\(m\)个没有公共边的路径,使得第\(m\)长的路径最长.问第\(m\)长的路 ...

  4. [NOIp2018提高组]货币系统

    [NOIp2018提高组]货币系统 题目大意: 有\(n(n\le100)\)种不同的货币,每种货币的面额为\([1,25000]\)之间的一个整数.若两种货币系统能够组合出来的数是相同的的,那我们就 ...

  5. [NOIp2013提高组]积木大赛/[NOIp2018提高组]铺设道路

    [NOIp2013提高组]积木大赛/[NOIp2018提高组]铺设道路 题目大意: 对于长度为\(n(n\le10^5)\)的非负数列\(A\),每次可以选取一个区间\(-1\).问将数列清零至少需要 ...

  6. NOIP2018提高组题解

    D1T1:铺设道路 回忆NOIP2013D2T1 积木大赛,发现这两题唯一的区别就是一个是造山一个是填坑,而把填坑的操作反序就是造山,所以可以直接使用那道题的方法. 具体方法是,从左到右每次考虑新的一 ...

  7. NOIP2018提高组省一冲奖班模测训练(六)

    NOIP2018提高组省一冲奖班模测训练(六) https://www.51nod.com/Contest/ContestDescription.html#!#contestId=80 20分钟AC掉 ...

  8. NOIP2018提高组省一冲奖班模测训练(五)

    NOIP2018提高组省一冲奖班模测训练(五) http://www.51nod.com/Contest/ContestDescription.html#!#contestId=79 今天有点浪…… ...

  9. NOIP2018提高组金牌训练营——动态规划专题

    NOIP2018提高组金牌训练营——动态规划专题 https://www.51nod.com/Live/LiveDescription.html#!#liveId=19 多重背包 二进制优化转化成01 ...

随机推荐

  1. C/C++实现单向循环链表(尾指针,带头尾节点)

    C语言实现单向循环链表,主要功能为空链表创建,链表初始化(头插法,尾插法),链表元素读取,按位置插入,(有序链表)按值插入,按位置删除,按值删除,清空链表,销毁链表. 单向循环链表和单向链表的区别:( ...

  2. 使用OxyPlot在WPF中创建图表

    目录(?)[+] Using Nuget 包括OxyPlot在你的应用程序的最简单方法是使用NuGet包管理器在Visual Studio 运行 Visual Studio并开始创建一个新的WPF项目 ...

  3. AtCoder ABC 129E Sum Equals Xor

    题目链接:https://atcoder.jp/contests/abc129/tasks/abc129_e 题目大意 给定一个二进制表示的数 L,问有多少对自然数 (a, b) 满足 $a + b ...

  4. C# EF去除重复列DistinctBy

    在网上看了LinQ有DistinctBy方法,实际在用的时候并没有找到,后来参照了该网站才发现写的是拓展方法 https://blog.csdn.net/c1113072394/article/det ...

  5. 一个很笨的方法,写脚本来实现自动调dmp,找有用的数据

    很久很久以前用到的方法, 方法挺笨的,但是算是比较实用吧. 全自动的调试dmp,最后只要结果. 谁用得着就给谁看吧. 这里需要两个脚本 1:启动脚本,是一个批处理文件,用来启动调试器,来加载dmp和调 ...

  6. 最全Linux常用命令大全

    查看系统系统信息 arch 显示机器的处理器架构(1) uname -m 显示机器的处理器架构(2) uname -r 显示正在使用的内核版本 dmidecode -q 显示硬件系统部件 - (SMB ...

  7. Hadoop 2.x 版本中的依赖 jar

  8. AWS云管理平台

  9. JS规则 多样化的我(变量赋值)我们使用"="号给变量存储内容,你可以把任何东西存储在变量里,如数值、字符串、布尔值等,

    多样化的我(变量赋值) 我们可以把变量看做一个盒子,盒子用来存放物品,那如何在变量中存储内容呢? 我们使用"="号给变量存储内容,看下面的语句: var mynum = 5 ; / ...

  10. mysql之备份表和备份数据库

    备份表 1.首先创建一个与原来一样的表 create table score2 like score; ###like就是将score表的结构拷贝过来,但是它并不执行数据:也就是说执行完上面的语句之后 ...