暑假集训CSP提高模拟7
暑假集训CSP提高模拟7
组题人: @KafuuChinocpp | @Chen_jr
\(T1\) P122. Permutations & Primes \(20pts\)
-
假的构造策略,拿到了 \(20pts\) 。
- 若 \(n\) 为奇数,则将 \(1\) 放在 \(\left\lceil \frac{n}{2} \right\rceil\) 的位置上,前一半质数降序放置,如果数量不够则合数降序放置,后一半降序放置。
- 若 \(n\) 为偶数,则将 \(1\) 放在 \(\frac{n}{2}\) 的位置上,前一半质数降序放置,如果数量不够则合数降序放置,后一半降序放置。
-
正解
- 观察到 \(1\) 对整个 \(\operatorname{mex}\) 的影响较大,故让包含 \(1\) 的区间尽量多,由 普及模拟1 T1 Past 的结论当取 \(i=n-i+1\) 时取到最大值,把 \(1\) 放在 \(\left\lceil \frac{n}{2} \right\rceil\) 的位置,然后将 \(2,3\) 分别放在首尾,剩下的随便填即可。
点击查看代码
int main() { ll t,n,i,j,k; cin>>t; for(j=1;j<=t;j++) { cin>>n; if(n==1) { cout<<"1"<<endl; } else { if(n==2) { cout<<"2 1"<<endl; } else { cout<<"2 "; for(i=2,k=4;i<=n/2;i++,k++) { cout<<k<<" "; } cout<<"1 "; for(i=n/2+2;i<=n-1;i++,k++) { cout<<k <<" "; } cout<<"3"<<endl; } } } return 0; }
\(T2\) P135. 树上游戏 \(29pts\)
-
原题: [ARC116E] Spread of Information | luogu P3523 [POI2011] DYN-Dynamite
-
部分分
- \(10pts\) :当 \(k=n-1\) 时输出 \(1\) 。
- \(10pts\) :当 \(k=1\) 时求出树的直径除以 \(2\) 并向上取整即可。
- 随机 \(pts\) :
rand()
大法好。
-
正解
- 答案显然具有单调性,考虑二分答案。
- 设当前二分出的答案为 \(mid\) ,则等价于覆盖距离为 \(mid\) 的情况下进行选点。
- 做法同 luogu P3942 将军令 ,考虑进行贪心,对于深度最深的叶节点将选择的点放在边界时,即取 \(mid\) 级祖先时,覆盖的范围一定最大。
- 设 \(f_{x}\) 表示 \(x\) 到以 \(x\) 为根的子树内最远的没被覆盖的点的距离, \(g_{x}\) 表示 \(x\) 到以 \(x\) 为根的子树内最近被选的点的距离,状态转移方程为 \(\begin{cases} f_{x}=\max\limits_{y \in Son(x)}\{ f_{y}+1 \} \\ g_{x}=\min\limits_{y \in Son(x)}\{ g_{y}+1 \}\end{cases}\) ,
- 当 \(g_{x}>mid\) 时以 \(x\) 为根的子树内所选的点覆盖不到自己,需要祖先节点进行覆盖,此时需要统计自己的贡献,即 \(f_{x}=\max(f_{x},0)\) ;当 \(f_{x}+g_{x} \le mid\) 时以 \(x\) 为根的子树内所选的点就能覆盖整棵子树,令 \(f_{x}=- \infty\) ;当 \(f_{x}=mid\) 说明 \(x\) 必须被选,令 \(f_{x}=- \infty,g_{x}=0\) ,选择点数加一。
- 特判下根节点没有被覆盖的情况。
点击查看代码
struct node { int nxt,to; }e[400010]; int head[400010],f[400010],g[400010],cnt=0,sum=0; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } void dfs(int x,int fa,int k) { f[x]=-0x3f3f3f3f; g[x]=0x3f3f3f3f; for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { dfs(e[i].to,x,k); f[x]=max(f[x],f[e[i].to]+1); g[x]=min(g[x],g[e[i].to]+1); } } if(g[x]>k) { f[x]=max(f[x],0);//等待祖先的覆盖 } if(f[x]+g[x]<=k)//已被覆盖 { f[x]=-0x3f3f3f3f; } if(f[x]==k)//强制覆盖 { f[x]=-0x3f3f3f3f; g[x]=0; sum++; } } bool check(int mid,int k) { sum=0; dfs(1,0,mid); sum+=(f[1]>=0);//自己到自己也得算 return sum<=k; } int main() { int n,k,u,v,l=0,r,mid,ans=0,i; cin>>n>>k; r=n; for(i=1;i<=n-1;i++) { cin>>u>>v; add(u,v); add(v,u); } while(l<=r) { mid=(l+r)/2; if(check(mid,k)==true) { ans=mid; r=mid-1; } else { l=mid+1; } } cout<<ans<<endl; return 0; }
\(T3\) P127. Ball Collector \(0pts\)
-
- 弱化版: [ARC111B] Reversible Cards | 種類数 β
-
部分分
- \(20pts\) :将选 \(a_{i}/b_{i}\) 进行状态压缩判断。
-
正解
- 对于每个点 \(i\) ,从 \(a_{i}\) 向 \(b_{i}\) 连一条无向边。
- 对于图中的每个连通块单独考虑。
- 设此时遍历的连通块 \(G=(V,E)\) ,则这个连通块对答案产生的贡献为 \(\min(V,E)\) 。
- 由于是连通块,所以 \(E \ge V-1\) 。
- 当 \(E=V-1\) 时这个连通块是一棵树,至多选 \(E=V-1\) 个点。
- 当 \(E \le V\) 时每个点一定能被至少分到一条边使其被选到,至多选 \(V\) 个点。
- 并查集维护连通块及内部点数、边数。
- 由于需要遍历每一个节点,所以在回溯过程中需要撤销不在路径上的点影响,使用可撤销并查集维护即可。
- 可撤销并查集只能按秩合并,路径压缩的话会改变树的形态导致无法撤销。
- 合并前栈记录下原值,撤销时按照栈内的原值更新回去。
点击查看代码
struct node { int nxt,to; }e[400010]; struct quality { int id,fa,siz,edge; }; int head[400010],a[400010],b[400010],fa[400010],siz[400010],edge[400010],ans[400010],cnt=0,sum=0; void add(int u,int v) { cnt++; e[cnt].nxt=head[u]; e[cnt].to=v; head[u]=cnt; } int find(int x) { return (fa[x]==x)?x:find(fa[x]); } void merge(int x,int y,stack<quality> &s,int &sum) { x=find(x); y=find(y); s.push((quality){x,fa[x],siz[x],edge[x]}); s.push((quality){y,fa[y],siz[y],edge[y]}); if(x==y)//自己到自己需要特判 { sum-=min(siz[x],edge[x]); edge[x]++; sum+=min(siz[x],edge[x]); } else { if(siz[x]<siz[y]) { swap(x,y); } sum-=min(siz[x],edge[x]); sum-=min(siz[y],edge[y]); fa[y]=x; siz[x]+=siz[y]; edge[x]+=edge[y]+1;//连一条边导致多了一条边 sum+=min(siz[x],edge[x]); } } void split(stack<quality>&s) { while(s.empty()==0)//撤销 { fa[s.top().id]=s.top().fa; siz[s.top().id]=s.top().siz; edge[s.top().id]=s.top().edge; s.pop(); } } void dfs(int x,int fa) { stack<quality>s; int last=sum; merge(a[x],b[x],s,sum); ans[x]=sum; for(int i=head[x];i!=0;i=e[i].nxt) { if(e[i].to!=fa) { dfs(e[i].to,x); } } split(s); sum=last;//同样也需要撤销 } int main() { int n,u,v,i; cin>>n; for(i=1;i<=n;i++) { cin>>a[i]>>b[i]; fa[i]=i; siz[i]=1; edge[i]=0; } for(i=1;i<=n-1;i++) { cin>>u>>v; add(u,v); add(v,u); } dfs(1,0); for(i=2;i<=n;i++) { cout<<ans[i]<<" "; } return 0; }
\(T4\) P159. 满穗 \(20pts\)
-
部分分
-
\(20pts\) :暴力修改, @wkh2008 有 \(hack\) 数据能卡精度,需要写
eps
。点击查看 hack 数据
in: 5 0 2 -3 2 1 -2 ans: 1
点击查看代码
const double eps=1e-10; double a[50010]; int main() { ll n,m,p=0,q=0,pos=0,x,y,i,j; double sum=0,ans=-0x7f7f7f7f; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; p+=(a[i]>=0)*a[i]; q+=(a[i]<0)*a[i]; } for(i=1;i<=n;i++) { sum+=a[i]/((a[i]>=0)?p:-q); if(sum-ans>eps) { ans=sum; pos=i; } } cout<<pos<<endl; for(j=1;j<=m;j++) { cin>>x>>y; p-=(a[x]>=0)*a[x]; q-=(a[x]<0)*a[x]; a[x]=y; p+=(a[x]>=0)*a[x]; q+=(a[x]<0)*a[x]; pos=sum=0; ans=-0x7f7f7f7f; for(i=1;i<=n;i++) { sum+=a[i]/((a[i]>=0)?p:-q); if(sum-ans>eps) { ans=sum; pos=i; } } cout<<pos<<endl; } return 0; }
-
-
正解
- 设 \(\begin{cases} p_{i}=\sum\limits_{j=1}^{i}[a_{i} \ge 0] \times a_{i} \\ q_{i}=\sum\limits_{j=1}^{i}[a_{i}<0] \times a_{i} \end{cases}\) ,此时 \(s_{i}=\dfrac{p_{i}}{P}-\dfrac{q_{i}}{Q}\) 。
- 若 \(i<j,s_{i}<s_{j}\) 则有 \(\dfrac{p_{i}}{P}-\dfrac{q_{i}}{Q}<\dfrac{p_{j}}{P}-\dfrac{q_{j}}{Q}\) ,移项有 \(\dfrac{p_{i}-p_{j}}{P}<\dfrac{q_{i}-q_{j}}{Q}\) ,又因为 \(p_{i}-p_{j} \le 0,Q<0\) 有 \(\dfrac{Q}{P} < \dfrac{q_{i}-q_{j}}{p_{i}-p_{j}}\) ,即 \(Q(p_{i}-p_{j})>P(q_{i}-q_{j})\) 。
- 发现很像斜率优化的式子,以 \(\{ p \}\) 为横坐标,以 \(\{ q \}\) 为纵坐标建立平面直角坐标系,其中横坐标 \(\{ p \}\) 是单调不降的,横坐标 \(\{ p \}\) 是单调不增的。
- 由于斜率 \(\dfrac{Q}{P}<0\) ,考虑维护上凸包相邻两点斜率 \(>\dfrac{Q}{P}\) 的部分,二分队列维护。
- 难点来自修改。设当前修改位置为 \(i\) ,则对于 \(j \ne i\) 的位置均有它们的凸包仅发生了平移,斜率不变(因为相互抵消了)。
- 考虑对序列分块,每个块内单独建出凸包,修改时将所属的块的凸包重构,其他凸包平移;查询时二分整个凸包取 \(\max\) 。
- 块长取 \(\sqrt{n \log{n}}\) 。
点击查看代码
const double eps=1e-15; ll a[50010],pos[50010],L[50010],R[50010],klen,ksum,P,Q; pair<ll,ll>p[50010]; deque<ll>q[50010]; ll x(ll i) { return p[i].first; } ll y(ll i) { return p[i].second; } void build(ll l,ll r,ll id) { q[id].clear(); ll sx=0,sy=0; for(ll i=l;i<=r;i++) { sx+=(a[i]>=0)*a[i]; sy+=(a[i]<0)*a[i]; p[i]=make_pair(sx,sy); while(q[id].size()>=2&&(y(q[id][q[id].size()-2])-y(q[id].back()))*(x(q[id].back())-x(i))<=(y(q[id].back())-y(i))*(x(q[id][q[id].size()-2])-x(q[id].back()))) { q[id].pop_back(); } q[id].push_back(i); } } bool check(ll mid,ll id) { return (y(q[id][mid])-y(q[id][mid+1]))*P<Q*(x(q[id][mid])-x(q[id][mid+1])); } ll divide_search(ll id) { ll l=0,r=q[id].size()-1,mid,ans=0; while(l<r) { mid=(l+r)/2; if(check(mid,id)==true) { ans=mid+1; l=mid+1; } else { r=mid;//mid 这个点可以被取到 } } return q[id][ans]; } void init(ll n) { klen=sqrt(n*log2(n)); ksum=n/klen; for(ll i=1;i<=ksum;i++) { L[i]=R[i-1]+1; R[i]=R[i-1]+klen; } if(R[ksum]<n) { ksum++; L[ksum]=R[ksum-1]+1; R[ksum]=n; } for(ll i=1;i<=ksum;i++) { for(ll j=L[i];j<=R[i];j++) { pos[j]=i; } build(L[i],R[i],i); } } void update(ll x,ll y) { P-=(a[x]>=0)*a[x]; Q-=(a[x]<0)*a[x]; a[x]=y; P+=(a[x]>=0)*a[x]; Q+=(a[x]<0)*a[x]; build(L[pos[x]],R[pos[x]],pos[x]); } ll query() { ll id=0,sum=0,sx=0,sy=0; double ans=-0x7f7f7f7f; for(ll i=1;i<=ksum;i++) { sum=divide_search(i); if(1.0*(p[sum].first+sx)/P-1.0*(p[sum].second+sy)/Q-ans>eps) { ans=1.0*(p[sum].first+sx)/P-1.0*(p[sum].second+sy)/Q; id=sum; } sx+=p[R[i]].first;//进行平移 sy+=p[R[i]].second; } return id; } int main() { ll n,m,x,y,i; cin>>n>>m; for(i=1;i<=n;i++) { cin>>a[i]; P+=(a[i]>=0)*a[i]; Q+=(a[i]<0)*a[i]; } init(n); cout<<query()<<endl; for(i=1;i<=m;i++) { cin>>x>>y; update(x,y); cout<<query()<<endl; } return 0; }
总结
- \(T1\) 赛时一直在想小区间怎么合并到大区间,猜结论时不会合理分析样例和性质导致结论猜假了。 @KafuuChinocpp 讲的
Special Judge
写法不是很符合预期,本以为会对 2024初三集训模拟测试3 T4 计蒜客 T3729 MEX 有所启发。 - \(T2\) 赛时和赛后转化题面转化得有点绕,很玄乎,没写完。
- \(T3\) 的暴力写假了。
后记
-
特殊奖励
-
\(T2\) 因为昨天 \(huge\) 看完题后,体活来找学长时说了需要 \(CDQ\) 分治所以就临时换了这道题,正常的话这道题应该和 暑假集训CSP提高模拟6 是一套题,还能和 \(T3\) 题面背景连上。
-
\(T4\) 夹带私货,剧透了《明末千里行》的部分情节。
本文来自博客园,作者:hzoi_Shadow,原文链接:https://www.cnblogs.com/The-Shadow-Dragon/p/18323123,未经允许严禁转载。
版权声明:本作品采用 「署名-非商业性使用-相同方式共享 4.0 国际」许可协议(CC BY-NC-SA 4.0) 进行许可。