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
",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(); }

总结:一定要提高自己的乱搞技巧,加强码力,不要放过任何一个得分点。会写的就不要写错,学会猜结论。

原文地址:https://www.cnblogs.com/HocRiser/p/8559817.html