WC2018伪题解
NOIP分数过低的场外选手,一个月之后才有幸膜到这套卷子。感觉题目质量很不错啊,可惜了T1乱搞可过,T2题目出锅非集训队员没有通知到,导致风评大幅被害。
感觉Cu的话随手写两个暴力就稳了,Ag的话T3稍微搞出点性质就稳了,Au的话T1乱搞和T3中搞出一个就比较稳了。
可以发现T1的28分和T3的16分只要读懂题目(会用交互)就能拿到。下面看下这两题的各部分分解法。
T1
Subtask 1(28 pts):直接暴力,倍增LCA即可。$O(n^2 \log n)$
Subtask 2(16 pts):特殊性质0,实际上就是一棵树,直接跑树的直径即可。 $O(n)$
Subtask 3(12 pts):特殊性质12,一棵树加一条链。枚举LCA,问题变成求$d_a+d_b-2*d_{lca}+v_b-v_a$其中v是链上前缀和。
后面还有很多档部分分,但都较复杂,正解则使用了边分治。但是有一种有理有据的乱搞方法可以通过所有官方数据。
重复多次以下操作:
- 从一个点a出发,找另一个点b使得答案最大
- 从b出发,找到一个点c使得答案最大,如此反复
先将序列random_shuffle()一下,然后用爬山法,取20次左右的初始值,每次按上面的方法更新,发现答案不更优则停止。
1 #include<cstdio> 2 #include<algorithm> 3 #define rg register int 4 #define rep(i,l,r) for (rg i=l; i<=r; i++) 5 typedef long long ll; 6 using namespace std; 7 8 const int N=100100; 9 int pos[N],vis[N],q[N],u,v,n; 10 ll dis[3][N],ans,w; 11 12 struct Graph{ 13 struct edge{ int nt,to; ll dis; }g[N<<1]; 14 int head[N],num; 15 void insert(rg from,rg to,ll dis){ g[++num]=(edge){head[from],to,dis},head[from]=num; return; } 16 }G[3]; 17 18 void bfs(rg op,rg S){ 19 rep(i,1,n) vis[i]=0; 20 rg h=0,t=1; q[t]=S,vis[S]=1,dis[op][S]=0; 21 while (h<t){ 22 rg x=q[++h],v; 23 for (rg i=G[op].head[x];i;i=G[op].g[i].nt){ 24 v=G[op].g[i].to; if (vis[v]) continue; 25 vis[v]=1,q[++t]=v,dis[op][v]=dis[op][x]+G[op].g[i].dis; 26 } 27 } 28 return; 29 } 30 31 int main(){ 32 freopen("tunnel.in","r",stdin); 33 freopen("tunnel.out","w",stdout); 34 srand(20020223); scanf("%d",&n); 35 rep(j,0,2) 36 for (rg i=1,u,v;i<n;++i){ 37 ll w; scanf("%d%d%lld",&u,&v,&w); 38 G[j].insert(u,v,w); G[j].insert(v,u,w); 39 } 40 rep(i,1,n) pos[i]=i; 41 random_shuffle(pos+1,pos+n+1); 42 for (int T=1,rt; T<=20; ++T){ 43 rt=pos[T]; ll ret=0; 44 while (1){ 45 bfs(0,rt); bfs(1,rt); bfs(2,rt); 46 ll res=0; rg id=0; 47 rep(i,1,n) 48 if (res<dis[0][i]+dis[1][i]+dis[2][i]) 49 res=dis[0][i]+dis[1][i]+dis[2][i],id=i; 50 if (ret<res) ret=res,rt=id; else break; 51 } 52 ans=max(ans,ret); 53 } 54 printf("%lld\n",ans); 55 return 0; 56 }
T3
Subtask 1:(20 pts) $O(n^2)$以上的限制跟没有限制没什么区别,直接$O(n^2)$跑暴力即可。
Subtask 2:(15 pts) 由于是完全二叉树,我们每次随机选一个未知的点,然后径直走过去即可。探寻路径的次数近似于每个点的深度,当然不超过log n,所以可以$O(n \log n)$过。
Subtask 3:(30 pts) 一条链,每次找一个未知点径直走过去。可以证明期望次数是ln n级别的。
设E(n)表示当只有左端点未知时,需要尝试的次数。则有
$$E(n)=1+\frac{1}{n}\sum_{i=0}^{n-1} E(i)$$ $$nE(n)+1+E(n)=(n+1)E(n+1)$$ $$ E(n+1)-E(n)=\frac{1}{n+1}$$ $$E(n)=\sum_{i=1}^{n} \frac{1}{i}\approx \ln n +0.5$$
如果每次从1扩展可能不能过最后一个点,错误次数是2 ln n + 1的,用左右端点尝试的话可以做到ln n +0.5,只有$\frac{144}{100000}$的概率无法通过。
满分做法就非常有趣了。既然已经确定是随机化了,每次随机的点又不能控制,我们只能用最小的代价找到离这个点最近的已知点扩展过去。
可以发现每次扩展都新形成一条链,这就变成了动态树链剖分问题,可以用LCT($O(n \log n)$)或动态点分治($O(n \log^2 n)$)解决。
这样直接把LCT模板拖过来就好了,LCT查询的是左子树和右子树中离这个点最近的点分别是哪个,记得每次新加入链的时候要access()以保证复杂度。
1 #include "rts.h" 2 #include <cstdio> 3 #include <algorithm> 4 #define ls ch[x][0] 5 #define rs ch[x][1] 6 #define rep(i,l,r) for (int i=l; i<=r; i++) 7 using namespace std; 8 typedef long long ll; 9 typedef pair<int,int> pii; 10 11 const int N=300100; 12 int n,b[N],t,ch[N][2],f[N],l[N],r[N]; 13 14 int sj(int l,int r){ return rand()%(r-l+1)+l; } 15 16 int isroot(int x) { return (!f[x])||(ch[f[x]][0]!=x && ch[f[x]][1]!=x);} 17 void upd(int x){ l[x]=r[x]=x; if (ls) l[x]=l[ls]; if (rs) r[x]=r[rs]; } 18 19 void rot(int x){ 20 int y=f[x],z=f[y],w=ch[y][1]==x; 21 ch[y][w]=ch[x][w^1]; f[ch[x][w^1]]=y; 22 if (!isroot(y)) ch[z][ch[z][1]==y]=x; 23 f[x]=z; f[y]=x; ch[x][w^1]=y; upd(y); 24 } 25 26 void splay(int x){ 27 while (!isroot(x)){ 28 int y=f[x]; 29 if (!isroot(y)) ((ch[f[y]][1]==y)^(ch[y][1]==x)) ? rot(x) :rot(y); 30 rot(x); 31 } 32 upd(x); 33 } 34 35 void access(int x){ for (int y=0; x; y=x,x=f[x]) splay(x),ch[x][1]=y,upd(x); } 36 37 void work(int x){ 38 int now=1,v; splay(now); 39 while (!b[x]){ 40 v=explore(now,x); 41 if (v==r[ch[now][0]]) now=ch[now][0]; 42 else if (v==l[ch[now][1]]) now=ch[now][1]; 43 else if (b[v]) splay(v),now=v; 44 else b[v]=1,f[v]=now,now=v; 45 } 46 access(x); 47 } 48 49 void work1(){ rep(i,2,n) if (!b[i]) work(i); } 50 51 void work2(){ 52 int l=1,r=1; int tot=1; b[1]=1; 53 while (tot<n){ 54 int k=sj(2,n); while (b[k]) k=sj(2,n); 55 int p=explore(l,k); 56 if (b[p]) 57 while (r!=k) r=explore(r,k),b[r]=1,tot++; 58 else{ 59 l=p; b[p]=1; tot++; 60 while (l!=k) l=explore(l,k),b[l]=1,tot++; 61 } 62 } 63 } 64 65 void play(int _n,int _t,int type){ n=_n; t=_t; if(type==3) work2(); else work1(); }
总结:一定要提高自己的乱搞技巧,加强码力,不要放过任何一个得分点。会写的就不要写错,学会猜结论。