多校A层冲刺NOIP2024模拟赛19
\(T1\) A. 图书管理 (book) \(90pts/90pts\)
-
部分分
- \(90pts\) :平衡树/线段树、主席树上二分/对顶堆暴力维护中位数,同 luogu P3871 [TJOI2010] 中位数 | luogu P1168 中位数 ,时间复杂度为 \(O(n^{2} \log n)\) ,需要适当卡常。
点击查看代码
int p[10010]; int main() { freopen("book.in","r",stdin); freopen("book.out","w",stdout); int n,i,l,r; ll ans=0; scanf("%d",&n); for(i=1;i<=n;i++) { scanf("%d",&p[i]); } for(l=1;l<=n;l++) { priority_queue<int,vector<int>,greater<int> >q1; priority_queue<int,vector<int>,less<int> >q2; for(r=l;r<=n;r+=2) { if(q2.empty()==0&&p[r]>q2.top()) { q1.push(p[r]); } else { q2.push(p[r]); } if(abs((int)q1.size()-(int)q2.size())>=2) { if(q1.size()>q2.size()) { while(q1.size()-q2.size()>=2) { q2.push(q1.top()); q1.pop(); } } else { while(q2.size()-q1.size()>=2) { q1.push(q2.top()); q2.pop(); } } } ans+=1ll*l*r*(q1.size()>q2.size()?q1.top():q2.top()); if(r+2<=n) { if(q2.empty()==0&&p[r+1]>q2.top()) { q1.push(p[r+1]); } else { q2.push(p[r+1]); } if(abs((int)q1.size()-(int)q2.size())>=2) { if(q1.size()>q2.size()) { while(q1.size()-q2.size()>=2) { q2.push(q1.top()); q1.pop(); } } else { while(q2.size()-q1.size()>=2) { q1.push(q2.top()); q2.pop(); } } } } } } printf("%lld\n",ans); fclose(stdin); fclose(stdout); return 0; }
-
正解
- 考虑枚举中位数 \(p_{i}\) ,计算区间个数。
- 将 \([l,r]\) 内 \(>p_{i}\) 的数看做 \(1\) , \(<p_{i}\) 的数看做 \(-1\) ,中位数的约束条件等价于 \([l,r]\) 内数的和等于 \(0\) ,开个桶一并维护左右端点即可。
点击查看代码
ll p[10010],suml[20010],sumr[20010]; int main() { freopen("book.in","r",stdin); freopen("book.out","w",stdout); ll n,num,ans=0,i,j; cin>>n; for(i=1;i<=n;i++) { cin>>p[i]; } for(i=1;i<=n;i++) { memset(suml,0,sizeof(suml)); memset(sumr,0,sizeof(sumr)); num=0; suml[n]=sumr[n]=i; for(j=i-1;j>=1;j--) { num+=(p[j]>p[i])?1:-1; suml[num+n]+=j; } num=0; for(j=i+1;j<=n;j++) { num+=(p[j]>p[i])?-1:1; sumr[num+n]+=j; } for(j=-n;j<=n;j++) { ans+=suml[j+n]*sumr[j+n]*p[i]; } } cout<<ans<<endl; fclose(stdin); fclose(stdout); return 0; }
\(T2\) B. 两棵树 (tree) \(30pts/30pts\)
-
部分分
- \(30pts\) :爆搜出每种情况挨个计算。
点击查看代码
const ll p=998244353; ll ans=0; ll qpow(ll a,ll b,ll p) { ll ans=1; while(b) { if(b&1) { ans=ans*a%p; } b>>=1; a=a*a%p; } return ans; } struct Graph { struct node { ll nxt,to; }e[400010]; ll head[200010],vis[200010],check[200010],cnt=0,tot=0; void add(ll u,ll v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } void dfs(ll x) { vis[x]=tot; for(ll i=head[x];i!=0;i=e[i].nxt) { if(vis[e[i].to]!=tot&&check[e[i].to]==0) { dfs(e[i].to); } } } ll ask(ll n) { tot++; ll ans=0; for(ll i=1;i<=n;i++) { if(vis[i]!=tot&&check[i]==0) { dfs(i); ans++; } } return ans; } }T,U; void dfs(ll pos,ll n) { if(pos==n+1) { ans=(ans+T.ask(n)*U.ask(n))%p; } else { T.check[pos]=1; U.check[pos]=0; dfs(pos+1,n); T.check[pos]=0; U.check[pos]=1; dfs(pos+1,n); } } int main() { freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); ll n,u,v,i; cin>>n; for(i=1;i<=n-1;i++) { cin>>u>>v; T.add(u,v); T.add(v,u); } for(i=1;i<=n-1;i++) { cin>>u>>v; U.add(u,v); U.add(v,u); } dfs(1,n); cout<<ans*qpow(qpow(2,n,p),p-2,p)%p<<endl; fclose(stdin); fclose(stdout); return 0; }
-
正解
- 对于一个森林,连通块数(树数)等于点数 \(V\) 减边数 \(E\) 。此时等价于求 \((V_{T}-E_{T}) \times (V_{U}-E_{U})=V_{T}V_{U}-E_{T}V_{U}-V_{T}E_{U}+E_{T}E_{U}\) 的期望值,考虑分别统计这几项的贡献。
- \(V_{T}V_{U}\)
- 暴力推式子,有 \(\begin{aligned} &E(V_{T}V_{U}) \\ &=\frac{1}{2^{n}} \times \sum\limits_{i=0}^{n}\dbinom{n}{i}(n-i)i \\ &=\frac{1}{2^{n}} \times \sum\limits_{i=1}^{n}\frac{n!}{(i-1)!(n-i-1)!} \\ &=\frac{1}{2^{n}} \times \sum\limits_{i=0}^{n-2}\dbinom{n-2}{i}(n-1)n \\ &=\frac{n(n-1)}{4} \end{aligned}\) 。
- 考虑组合意义,对于 \(x \in T,y \in U\) ,当 \(x \ne y\) 时均被保留的概率为 \(\frac{1}{4}\) ,否则均被保留的概率为 \(0\) ,总期望为 \(\frac{n(n-1)}{2}\) 。
- \(E_{T}V_{U}\)
- 考虑组合意义,对于 \((x,y) \in T,u \in U\) ,当 \(x,y,u\) 互不相同时均被保留的概率为 \(\frac{1}{8}\) ,否则均被保留的概率为 \(0\) ,总期望为 \(\frac{(n-1)(n-2)}{8}\) 。
- \(V_{T}E_{U}\)
- 同 \(E_{T}V_{U}\) ,总期望为 \(\frac{(n-1)(n-2)}{8}\) 。
- \(E_{T}E_{U}\)
- 考虑组合意义,对于 \((x,y) \in T,(u,v) \in U\) ,当 \(x,y,u,v\) 互不相同时均被保留的概率为 \(\frac{1}{16}\) ,否则均被保留的概率为 \(0\) 。
- 枚举 \((x,y) \in T\) ,那么 \((u,v) \in U\) 满足 \(x,y,u,v\) 互不相同的数量等于 \(n-1-du_{U,x}-du_{U,y}+[(x,y) \in U]\) 。
- 总期望为 \(\sum\limits_{(x,y) \in T}\frac{n-1-du_{U,x}-du_{U,y}+[(x,y) \in U]}{16}\) 。
- 最终,有 \(\frac{n-1}{2}+\sum\limits_{(x,y) \in T}\frac{n-1-du_{U,x}-du_{U,y}+[(x,y) \in U]}{16}\) 即为所求。
点击查看代码
const ll p=998244353; map<pair<ll,ll>,ll>e; ll du[200010],u[400010],v[400010]; ll qpow(ll a,ll b,ll p) { ll ans=1; while(b) { if(b&1) { ans=ans*a%p; } b>>=1; a=a*a%p; } return ans; } int main() { freopen("tree.in","r",stdin); freopen("tree.out","w",stdout); ll n,ans,inv=qpow(16,p-2,p),i; cin>>n; ans=(n-1)*qpow(2,p-2,p)%p; for(i=1;i<=n-1;i++) { cin>>u[i]>>v[i]; if(u[i]>v[i]) { swap(u[i],v[i]); } } for(i=1;i<=n-1;i++) { cin>>u[i+n]>>v[i+n]; du[u[i+n]]++; du[v[i+n]]++; if(u[i+n]>v[i+n]) { swap(u[i+n],v[i+n]); } e[make_pair(u[i+n],v[i+n])]=1; } for(i=1;i<=n-1;i++) { ans=(ans+(n-1-du[u[i]]-du[v[i]]+(e.find(make_pair(u[i],v[i]))!=e.end()))*inv%p)%p; } cout<<ans<<endl; fclose(stdin); fclose(stdout); return 0; }
\(T3\) C. 函数(fun) \(60pts/60pts\)
-
部分分
-
\(30 \%\) :模拟。
点击查看代码
ll x[1000010]; int main() { freopen("fun.in","r",stdin); freopen("fun.out","w",stdout); ll n,q,a,b,ans,i,j; scanf("%lld%lld",&n,&q); for(i=1;i<=n;i++) { scanf("%lld",&x[i]); } for(j=1;j<=q;j++) { scanf("%lld%lld",&a,&b); ans=-1; for(i=1;i<=n-1;i++) { if(((x[i]^a)-b)*((x[i+1]^a)-b)<=0) { ans=i; break; } } cout<<ans<<endl; } fclose(stdin); fclose(stdout); return 0; }
-
-
正解
- 考虑手动建出 \(f(x)\) 的函数图象,设左右端点分别为 \(l,r\) ,使用 \(01Trie\) 可以轻松维护。
- 若 \(f(l)f(r)>0\) 则一定无解,否则一定存在 \(f(l)f(mid) \le 0 \lor f(mid)f(r) \le 0\) ,其中 \(mid=\frac{l+r}{2}\) 。二分处理即可。
- 正确性由二分法求函数零点可知。
- 把二分改成递归在 accoders NOI 上大数据平均快了 \(300ms\) 左右,但在学校 \(OJ\) 上变慢了,不是很理解为啥。
点击查看代码
int x[1000010]; struct Trie { int son[32000010][2],ed[32000010],rt_sum=0; void insert(int s,int id) { int x=0; for(int i=30;i>=0;i--) { if(son[x][(s>>i)&1]==0) { rt_sum++; son[x][(s>>i)&1]=rt_sum; } x=son[x][(s>>i)&1]; } ed[x]=id; } int query_min(int s) { int x=0; for(int i=30;i>=0;i--) { if(son[x][(s>>i)&1]!=0) { x=son[x][(s>>i)&1]; } else { x=son[x][((s>>i)&1)^1]; } } return ed[x]; } int query_max(int s) { int x=0; for(int i=30;i>=0;i--) { if(son[x][((s>>i)&1)^1]!=0) { x=son[x][((s>>i)&1)^1]; } else { x=son[x][(s>>i)&1]; } } return ed[x]; } }T; int f(int x,int a,int b) { return (x^a)-b; } int main() { freopen("fun.in","r",stdin); freopen("fun.out","w",stdout); int n,q,a,b,l,r,ans,mid,i; scanf("%d%d",&n,&q); for(i=1;i<=n;i++) { scanf("%d",&x[i]); T.insert(x[i],i); } for(i=1;i<=q;i++) { scanf("%d%d",&a,&b); l=T.query_min(a); r=T.query_max(a); if(l>r) { swap(l,r); } ans=-1; if(1ll*f(x[l],a,b)*f(x[r],a,b)<=0) { while(r-l>1) { mid=(l+r)/2; if(1ll*f(x[l],a,b)*f(x[mid],a,b)<=0) { r=mid; } else { l=mid; } } ans=l; } printf("%d\n",ans); } fclose(stdin); fclose(stdout); return 0; }
\(T4\) D. 编辑(edit) \(60pts/60pts\)
-
部分分
- \(30pts\) :枚举左右端点,然后 \(O(|S||T|)\) 计算编辑距离,同 luogu P2758 编辑距离 ,时间复杂度为 \(O(k|S||T|^{2})\) 。
- \(60pts\) :考虑固定一个端点,然后让另一个端点单调,时间复杂度为 \(O(|S||T|^{2})\) 。
点击查看代码
ll ans[50],f[2][50010]; char s[50010],t[50010]; int main() { freopen("edit.in","r",stdin); freopen("edit.out","w",stdout); ll k,lens,lent,l,r,i; cin>>k>>(s+1)>>(t+1); lens=strlen(s+1); lent=strlen(t+1); for(l=1;l<=lent;l++) { for(i=0;i<=lens;i++) { f[(l-1)&1][i]=i; } for(r=l;r<=lent;r++) { f[r&1][0]=r-l+1; for(i=1;i<=lens;i++) { f[r&1][i]=min(f[(r-1)&1][i-1]+(s[i]!=t[r]),min(f[r&1][i-1]+1,f[(r-1)&1][i]+1)); } if(f[r&1][lens]<=k) { ans[f[r&1][lens]]++; } } } for(i=0;i<=k;i++) { cout<<ans[i]<<endl; } fclose(stdin); fclose(stdout); return 0; }
-
正解
枚举每个后缀,\(f_{i,j}\) 表示最大的 \(x\),使得 \(S[1:x]\) 和 \(T[1:x+j]\) 可以在 \(i\) 次编辑内得到,显然若 \(x\) 可以,所有\(x_0 \leq x\), \(S[1:x_0]\) 和 \(T[1:x_0+j]\) 都可以在 \(i\) 次编辑内得到。
考虑如何转移,先考虑做完第 \(j\) 次编辑操作后往外延申,可以延申的即为 \(S\) 的一个后缀和 \(T\) 的一个后缀的最长公共前缀,即\(f_{i,j} = f_{i,j} + \text{LCP}(S[f_{i,j + 1}:|S|],T [f_{i,j} + j + 1 . .: |T|])\),随后我们可以通过对\(f_{i+1,j-1},f_{i+1,j},f_{i+1,j+1}\) 三项进行转移,即考虑下一次的编辑的具体操作是删除添加还是修改。
每次要算的两个后缀的前缀下标不会差超过 \(k\),因此一共至多求 \(O(nk)\) 次 LCP,可以利用二分+ hash 的方式解决。
记录每个后缀中 \(f_{i,j}=|S|\) 的那些状态,即可计算出最终答案,时间复杂度为 \(O(nk^2+nk \log n)\)。
总结
- \(T1\) 直接对着 \(2s\) 的时限冲了个 \(O(n^{2} \log n)\) 的做法,大样例在本地跑了 \(1.4s \sim 1.6s\) ,遂直接交了。
- \(T2\) 的 \(Trick\) 好像在之前见过,但想不起来是哪道题了。
- \(T4\) 想了一个小时的怎么求编辑距离,属于去年初赛白考了。
后记
-
\(T1\)
- 下发的 \(PDF,markdown\) 题面都给了 \(2s\) 的时限,且在一段时间内题目界面也给了 \(2s\) 的时限,但在比赛结束时发现改成了 \(1s\) 。
- 第一次下发的大样例假了,暴力都跑不过去。
-
\(T2\) 临结束时在学校 \(OJ\) 补充了额外的大样例。
-
\(T3\) 中途更换了大样例,并下发了
testlib
和checker.cpp
,但默认我们已经会使用checker.cpp
了, \(huge\) 还让我们别卡checker.cpp
。点击查看代码
#include"testlib.h" #include<bits/stdc++.h> const int N=1e6+7; int f[N]; int main(int argc,char *argv[]){ registerTestlibCmd(argc,argv); int n=inf.readInt(),q=inf.readInt(); for(int i=1;i<=n;i++) f[i]=inf.readInt(); while(q--){ int x=inf.readInt(),y=inf.readInt(); int a=ouf.readInt(),b=ans.readInt(); if((a==-1)^(b==-1)) quitf(_wa,"Wrong Answer."); if(a==-1) continue; if(1ll*((f[a]^x)-y)*((f[a+1]^x)-y)>0) quitf(_wa,"Wrong Answer."); std::swap(a,b); if(1ll*((f[a]^x)-y)*((f[a+1]^x)-y)>0) quitf(_wa,"Wrong Answer."); } quitf(_ok,"Correct. Participant found a valid solution."); return 0; }
-
\(T4\) 中途更换了大样例。
-
\(T3,T4\) 因为时间复杂度较大,考虑到各学校 \(OJ\) 评测机,所以
后两题没有时间,设置题目的时候用标程跑一下就行了
。