Virtual NOIP II Day 8 总结

Sweetlemon

2018-11-08 17:29:54

Personal

### T1 关联点 一道树上的题目,感觉难度比本卷$\text{T}2$大。 $60$分:对每个点,在以它为根的子树内进行$\text{dfs}$即可。$\text{O}(n^2)$。 $100$分:有两种做法。 1. 由于数据范围是$2\times 10^5$,因此可以考虑使用$\text{O}(n\log n)$的算法。考虑在$b$处进行统计,即找到$b$的$v_b$层祖先进行标记。为了找到$v_b$层祖先,我们可以存储每个点的$2^k$层祖先,进行倍增(二进制拆分)跳跃。 由于要记录这个点是左关联点还是右关联点,我们想到了更好的方法:在$a$的左儿子和右儿子处进行统计,即在$v_b-1$层祖先处统计。 2. 我们考虑加速找到$b$的$v_b$层祖先的过程。我们可以$\text{dfs}$遍历整棵树,并用一个栈储存当前在函数调用栈帧中的节点(类似$\text{Tarjan}$的`stack`)。遍历到点$x$时,栈中的节点都是$x$的祖先,我们只需计算出$v_b$层祖先的深度即可直接访问。时间复杂度$\text{O}(n)$。 其实$\text{O}(n)$的代码比$\text{O}(n\log n)$的代码还要短,但是可能我学数据结构学傻了吧…… 下面附上$\text{O}(n)$的代码: ```cpp #include <cstdio> #define MAXN 200005 using namespace std; typedef int io_t; io_t uread(void); void uwrite(io_t x,char spliter=' '); int n; int lch[MAXN],rch[MAXN]; int con[MAXN]; //Mark at the lc/rc int depth[MAXN]; int v[MAXN]; int st[MAXN]; int stop; void dfs(int x); int main(void){ n=uread(); for (int i=1;i<=n;i++) v[i]=uread(); for (int i=1;i<=n;i++){ int lc,rc; lc=uread(),rc=uread(); lch[i]=lc,rch[i]=rc; } stop=0; depth[1]=0; dfs(1); for (int i=1;i<=n;i++){ uwrite(con[lch[i]]); uwrite(con[rch[i]]); putchar('\n'); } return 0; } void dfs(int x){ st[stop++]=x; if (depth[x]>=v[x]) con[st[depth[x]-v[x]+1]]++; if (lch[x]){ depth[lch[x]]=depth[x]+1; dfs(lch[x]); } if (rch[x]){ depth[rch[x]]=depth[x]+1; dfs(rch[x]); } stop--; } ``` ### T2 小奇的日程表 一道$\text{T}1$水平的题目,不知道为什么放在这里? 直接暴力模拟即可。注意标记数组不要爆空间,因此不能用`int`,只能用`bool`或`bitset`。 ### T3 送分题 把分送掉的题目。一道比较好的树形$\text{dp}$,和[黑点](https://vjudge.net/problem/CodeChef-BLACKCOM)有一点类似(当然难度不一定是一个级别的),可以用来领悟树形$\text{dp}$的基本套路。 毕克老师说,树形$\text{dp}$题目的数据范围可以用来辅助状态设计。于是我们观察数据范围:$n\le 10000,k\le 100$。于是我们就可以考虑设计$\text{O}(nk)$的状态。 我们设$f[i][j]$为:以$i$为根的子树中,所选节点到$i$距离$\ge j$,且合法的选择方案中点权和的最大值。那么,我们要考虑$f[i][j]$的转移。 树形$\text{dp}$的转移有一个套路:遍历子节点前,预处理父节点信息;在遍历子节点的同时,先将子节点的信息和父节点的信息进行处理,再将子节点的信息合并到父节点的信息中;最后根据父节点的信息处理答案。 这题也遵循这样的套路。 设当前遍历到的父节点为$x$。 预处理父节点信息这一步在本题中不需要,我们直接遍历子节点。设遍历到的子节点为$v$。 接下来考虑用$f[v]$更新$f[x][j]$。在信息合并中,有以下两种情况: 1. 距离$x$等于$j$的节点在前面遍历(即信息已被合并)的子节点中,那么$f[x][j]$可以被更新为$f[x][j]+f[v][i]$,其中$i=\max (j-1,k-j)$。(对$i$的要求可以理解为,既要求$i+1+j>k$,又要求$i\ge j$。注意到$x$的距离等于到$v$的距离$+1$。) 2. 距离$x$等于$j$的节点在新的(即当前遍历到)的子节点中,那么$f[x][j]$可以被更新为$f[x][i]+f[v][j-1]$,其中$i=\max(j,k+1-j)$。 尤其注意上面$j$的范围应为$[1,k+1]$。另外,我们把$f$打造为一个后缀$\max$的数组,即$j$越小,$f[x][j]$越大。这一点在“所选节点到$i$距离$\ge j$“中就可以体现。还有一点是,由于$f[v][k+1]$的信息已经被合并到$f[v][k]$中(前缀$\max$),我们在转移时无需考虑$f[v][k+1]$。 还要注意的一点是,我们在更新$f[x][j]$的值的时候,要把更新的值存到临时数组中,更新完毕后再转移回$f$数组,否则可能导致同一个权值被重复累加(这是一种类似$01$背包倒序转移的思想)。 最后,要根据父节点的信息处理答案,即我们要考虑选择父节点的情形。要选择父节点,其余选择的点到它的距离就至少是$k+1$。因此$f[x][0]=\max(f[x][1],f[x][k+1]+w[x])$。其实在这一步我们已经把$f[x][k+1]$的值使用完了,在以后的转移中它就没有用了。 在考试中,我在书写这道题的代码时,把上面的坑几乎全部跳了一遍(逃)。然而到考试结束我都没有发现的坑是,我把临时数组转移和求前缀$\max$放在了同一个循环中,由于$f[x][k+1]$不需要求前缀$\max$,因此那个循环就没有到达$k+1$,从而$f[x][k+1]$没有从临时数组中转移出来,最终造成某些状态下答案偏小,导致$\text{WA}$了$2$个点。这个坑点确实难以发现(难以对撞),对拍了很多次都没能发现。因此,对拍通过、通过大样例并不意味着这个程序是正确的,只是意味着它也许能够得到**大部分**的分数。想要保证程序的执行过程和心里想的一样,还是要进行代码审核。 下面附上关键部分的代码: ```cpp void dfs(int x,int pa){ for (int ei=fst[x];ei;ei=nxt[ei]){ int v=g[ei]; if (v==pa) continue; dfs(v,x); for (int j=1;j<=kp1;j++){ //kp1 means k+1 //f[x][j]=back_max_f[v][max(j-1,k-j)] //Decide by father int considerj=max(j-1,k-j); tempf[j]=f[x][j]+f[v][considerj]; } for (int j=1;j<=kp1;j++){ //f[v][j-1] (dis==j) //Decide by child int considerj=max(j,kp1-j); tempf[j]=max(tempf[j],f[v][j-1]+f[x][considerj]); } //f[v][k+1]:ignore. tempf[0]=0; for (int j=kp1;j>=0;j--){ //Important! j=kp1 not j=k. //Otherwise tempf[k+1] will lose tempf[j]=max(tempf[j],tempf[j+1]); f[x][j]=tempf[j]; } } f[x][0]=max(f[x][0],f[x][k+1]+w[x]); ans=max(ans,f[x][0]); } ```