Virtual NOIP 2018 Day5 总结

Sweetlemon

2018-10-29 11:57:28

Personal

## 上午 ### T1 购买板凳 $100$分:使用差分数组进行区间加法、单点查询。 ### T2 马拉松路径 $30$分:对每一个$C$点计算最短路即可。 $40$分:考虑$C=n$的情况,直接输出最短的边长即可。 $70$分:考虑树的情况,进行树上$\text{dp}$。 $100$分: 这道题要求的是图上多起点、多终点的最短路,考虑添加超级源点和超级汇点。 但是一个$C$点不能回到它自己,因此要避免出现$s\rightarrow c_1\rightarrow x \rightarrow c_1 \rightarrow e$这样的路径。解决办法有以下两种: 1. 考虑将所有$C$点分为$A$和$B$两组,源点只向$A$连边,只有$B$中的点才向汇点连边,求一次最短路,就能求出所有$A$中的$C$点到$B$中$C$点的最短路。 我们如何分组,才能计算完所有$C$点对之间的最短路呢? 考虑二进制拆分。如果两个数不同,那么它们至少有一个二进制位不同。因此我们考虑枚举二进制位,将某一位为$0$的点分到$A$组,为$1$的点分到$B$组,这样就能枚举完所有的可能$C$点对了。 2. 在计算$s$到$e$的最短路时,保留最短路和次短路,即保证最短路的“来源”不同,最后选择来源正确的最短路值即可。 ### T3 起飞 $40$分:暴力$\text{dp}$。 $100$分:考虑使用数据结构优化。因为是在$a-k$到$a-1$的范围内选择状态进行转移,因此这道题是不是可以用单调队列解决呢?但是树高不同,怎么办呢? 考虑到树高不同,因此我们可以试着使用单调栈套单调队列。 单调栈套单调队列的代码如下: ```cpp #include <cstdio> #include <cctype> #include <algorithm> #define MAXN 1000005 #define INF 1000005 using namespace std; typedef int io_t; io_t uread(void); //读入优化 int a[MAXN]; //树的高度 int h[MAXN]; //单调不减数组,记录每一个体力值对应的范围内最高的树高 int fst[MAXN]={0},nxt[MAXN],fore[MAXN],tl[MAXN]={0}; //记录每一个点单调队列的队头、下一个元素、上一个元素和队尾 int f[MAXN]; //跳到每一棵数上的最小体力值花费 bool cmp(const int lhs,const int rhs); //离散化 int min(int a,int b); int max(int a,int b); int main(void){ int n; n=uread(); for (int i=1;i<=n;i++){ a[i]=uread(),nxt[i]=i; //这里使用nxt数组是为了节省空间 } //离散化 sort(nxt+1,nxt+n+1,cmp); for (int i=1;i<=n;i++){ a[nxt[i]]=i;//离散化(逆映射) } int q; q=uread(); while (q--){ int k; int head=0,tail=0; //单调栈栈底、栈顶(超尾)指针 //实际上这个单调栈也会出现弹出栈底的现象(如果栈底的所有元素都过期了,就需要弹出栈底) k=uread(); for (int i=0;i<=n;i++) tl[i]=fst[i]=0,h[i]=INF; //初始化每个栈节点的队头和队尾 fst[0]=tl[0]=1; //将1加入单调栈 h[0]=a[1]; fore[1]=nxt[1]=0; f[1]=0; tail=1; for (int i=2;i<=n;i++){ int tf=INF; //tf计算当前元素的f值 //删除已经无效的元素(i-k-1) if (i-k-1>0){ int deletep=i-k-1; if (f[deletep]<tail && fst[f[deletep]]==deletep){ //这个节点还在数据结构中,需要删除 fst[f[deletep]]=nxt[deletep]; //队头由下一个继承 if (!fst[f[deletep]]){ //如果这个队已经空了 tl[f[deletep]]=0; //注意一定要把tail也清空 if (f[deletep]>head) //如果这个元素之前还有元素,即不是栈底 h[f[deletep]]=h[f[deletep]-1]; //为了不影响二分搜索,我们把这个位置的h值设为体力值稍小的一个位置的h值,这样这个位置不会被使用 else h[f[deletep]]=0,head++; //栈底被弹出,head++ } else { fore[fst[f[deletep]]]=0; //否则把这个栈位置的队头的fore信息维护好 h[f[deletep]]=a[fst[f[deletep]]]; //更新这个栈位置的h值 } } } //在h数组中二分,计算f[i] int lst=lower_bound(h+head,h+tail,a[i])-h; //找到比i高的、范围内的、消耗体力值最小的树 if (lst<tail) //lst==tail是找不到的标志 tf=min(tf,lst); tf=min(tf,head+1); //也可以考虑从当前体力值最小的树(比i矮)消耗1体力值跳到i上 f[i]=tf; //更新f[i] //将i插入到数据结构中 if (tf>=tail){ //要压入栈顶 tail=tf+1; //tail是超尾 h[tf]=a[i]; //压入 fst[tf]=tl[tf]=i; //更新fst和tl fore[i]=nxt[i]=0; //没有相邻的元素 } else { while (tl[tf] && a[tl[tf]]<a[i]) //单调队列基本操作 tl[tf]=fore[tl[tf]]; //弹掉比i早、又比i矮的树 if (tl[tf]){ //如果还剩有元素 nxt[tl[tf]]=i; fore[i]=tl[tf],nxt[i]=0; tl[tf]=i; } else { //不剩元素了,i作为队头 tl[tf]=fst[tf]=i; h[tf]=a[i]; fore[i]=nxt[i]=0; } } } printf("%d\n",f[n]); } return 0; } inline int min(int a,int b){ return a<b?a:b; } inline int max(int a,int b){ return a>b?a:b; } bool cmp(const int lhs,const int rhs){ if (a[lhs]==a[rhs]) //如果高度相同,按出现顺序正序排列 return lhs<rhs; else return a[lhs]<a[rhs]; } io_t uread(void){ char ch; io_t ans=0; ch=getchar(); while (!isdigit(ch)) ch=getchar(); while (isdigit(ch)){ ans=ans*10+(ch-'0'); ch=getchar(); } return ans; } ``` 这个办法很麻烦,因为要手动维护每个栈节点的队列,而且必须用链表模拟队列。 有什么简单的方法吗? 事实上,我们只需要维护一个单调队列,单调队列中记录范围内的体力值的最小值,每次都从队头元素转移即可。 为什么可以这样呢? 如果用队头元素转移,设队头体力值为$x$那么转移出来的体力值为$x$或$x+1$。现在假设有另一个不在队头的状态,使得可以不消耗体力值转移,设这个状态的体力值为$y$,那么转移出来的体力值也为$y$。 然而,由于$y$不在队头,一定有$x<y$,由此知$y\ge x+1$,因此从队头转移不会变劣,证明完毕。 ## 下午 ### T1 鳕鱼 $100$分:解方程$x+y=n,x-y=k$,解得$x=\frac{n+k}{2},y=\frac{n-k}{2}$,因此当$n>k$且$n\equiv k\pmod{2}$时,这一群鳕鱼还可以被分成$\frac{n+k}{2}$和$\frac{n-k}{2}$两组,递归计算即可。 ### T2 一样远 $30$分:对于每一次询问,都分别以起点和终点为根计算每一个点的深度,最后计算深度相同的点的个数。 $40$分:对于$a=b$的情况,直接输出$n$即可。 $100$分:树上的题目要抓住$\text{LCA}$这个关键点来解。我们讨论这两个点与$\text{LCA}$的距离。 如果这两个点到$\text{LCA}$的距离相等,那么$\text{LCA}$及$\text{LCA}$以上的部分都是合法的。 如果这两个点到$\text{LCA}$的距离不相等,且这两个点之间的距离为偶数,那么以这两个点的路径的中点为根的子树中的点都满足要求。 如果这两个点间的距离为奇数,那么答案为$0$。 注意,如果树的形态是一条链,不能通过递归$\text{dfs}$,否则会导致$\text{Stack Overflow}$;必须手写栈。 $\text{dfs}$遍历树时,要递推计算点的深度和直接父亲;遍历后要递推计算$2^x$层祖先和以$i$为根的子树大小(可以按照深度从大到小的顺序计算)。 ### T3 配对方案 $30$分:$\text{O}(2^{2n})=\text{O}(4^n)$直接记忆化搜索即可。 $100$分:我们考虑队列的形态。最后一位一定是女生,否则那一位男生就没有办法匹配女生了。如此我们发现,每一位男生身后的女生数量一定至少比男生数量多$1$。 接着我们考虑一位男生可以和哪一位女生配对。如果他身后只有一位女生(这样他当然就是最后一位男生),那么他就只能和这一位女生配对;如果他身后有多位女生,那么他可以选择其中的一位女生配对,这样会产生不同的状态,即有后效性,不便$\text{dp}$,且由于$n$的范围很大,几乎不可能用撞鸭$\text{dp}$。 还记得[小奇挖矿](https://www.luogu.org/problemnew/show/P1412)吗?在那道题中,是否采矿和是否维修对接下来的状态有很大影响,于是我们采用逆向$\text{dp}$,从后往前计算。 这道题同样可以逆向$\text{dp}$。由于每位男生都只能选择和自己身后的女生配对,因此对于最后一位男生(序号为$i$),他身后的女生数目$f[i]$即为他能选择的方案数。 对于$i$前面的一位男生,设他身后的女生数量为$x$,那么他能选择的方案数是$x-1$(其中一位已经和男生$i$配对)。以此类推,如果某一位男生身后有$x$位女生和$y$位男生,那么他能够配对的选择数是$x-y$。 于是我们可以计算将上述$x-y$的值用$f[i]$数组递推计算,最后把所有男生的$f$值乘起来取模即得答案。