[关闭]
@xiaoziyao 2020-12-02T08:10:55.000000Z 字数 10407 阅读 1118

YLOJ 11月30日考试官方solution

考试总结


这是一场CCF性质浓厚的比赛,起码从题目名称就可以看出来。

四道题都是原题)。

预计平均分:分。

预计最高分:分。

实际平均分:分。

实际最高分:分。

为什么没有人写暴力呀/fad。

T1 数的重心(

原题P6563 [SBCOI2020] 一直在你身旁,本题是加强版(但是本题AC代码交上去会MLE)

题意:
给定序列a,每次可以选定一个位置,获取这个位置的值,并把原区间分成两部分,任意选择一个区间进行相同的操作,使得获得的值之和最小。

T1应该是本场最难的题。(在我心中和CSP2020儒略日差不多)

分析部分分:

然后是一个比较麻烦的部分分:对于另外的数据,

我们发现这个里面的非常讨厌,我们考虑怎么把它去掉。

可以发现一定有一个分界点,使得在这个分界点及之前,都满足,在这个分界点之后,满足

这个性质很显然,因为我们消除掉一个区间一定比消除掉它的子区间更劣。

很显然我们只要固定住右端点,随着左端点的增加,一定也会增加。

倒序枚举右端点,正序枚举左端点,那么我们就可以同时处理出每个区间的分界点,这样我们的转移式就变为了:

我们发现前面的无关,因此我们可以保存一个单调队列来记录。

对于,我们发现一定有,因为

有了这个结论,很显然最小,我们直接用代替就好了。

时间复杂度:

  1. #include<stdio.h>
  2. #define int long long
  3. #define inf 100000000000000000
  4. const int maxn=3005;
  5. int T,n;
  6. int a[maxn],f[maxn][maxn];
  7. inline int min(int a,int b){
  8. return a<b? a:b;
  9. }
  10. struct Monotonic_Queue{
  11. int l,r;
  12. int v[maxn],q[maxn];
  13. void init(){
  14. l=1,r=0;
  15. }
  16. void check(int k){
  17. while(l<=r&&q[l]>k)
  18. l++;
  19. }
  20. void push(int a,int b){
  21. while(l<=r&&a<v[r])
  22. r--;
  23. v[++r]=a,q[r]=b;
  24. }
  25. int top(){
  26. return l>r? inf:v[l];
  27. }
  28. }MQ;
  29. signed main(){
  30. scanf("%lld",&T);
  31. while(T--){
  32. scanf("%lld",&n);
  33. for(int i=1;i<=n;i++)
  34. scanf("%lld",&a[i]);
  35. for(int r=2;r<=n;r++){
  36. int pos=r-1;
  37. MQ.init();
  38. for(int l=r-1;l>=1;l--){
  39. while(pos>=l&&f[l][pos]>=f[pos+1][r])
  40. pos--;
  41. MQ.check(pos);
  42. f[l][r]=min(f[l][pos+1]+a[pos+1],MQ.top());
  43. if(l>1)
  44. MQ.push(f[l][r]+a[l-1],l-1);
  45. }
  46. }
  47. printf("%lld\n",f[1][n]);
  48. }
  49. return 0;
  50. }

考虑不满足的数据。

瞄一眼空间范围,发现有,因此可以知道可以多开一些数组。

首先,上面关于区间分界点的结论仍然是正确的,也就是说我们仍然可以使用这个转移方程:

我们换一种枚举方式:倒序枚举,正序枚举,这样我们仍然可以维护出分界点,并可以用单调队列维护

然后考虑怎么求,我们发现这个东西与无关,我们可以尝试使用单调队列。

具体地说,我们可以对于每个点保留一个保存当前所有的单调队列,在每一次处理完一个区间的数据后在单调队列中插入

然后,我们遍历到区间时,可以把的单调队列去除掉位置大于等于分界点的所有值。我们发现这样的去除一定是正确的,因为我们倒序枚举左端点,所以我们每次遇到都会减小,这样我们每一次对队首的判定去除掉的所有状态以后也用不到。

这样,就让我们用的队首表示出来了。

时间复杂度:

  1. #include<stdio.h>
  2. #define int long long
  3. #define inf 100000000000000000
  4. const int maxn=3005;
  5. int n,L;
  6. int a[maxn],f[maxn][maxn];
  7. inline int min(int a,int b){
  8. return a<b? a:b;
  9. }
  10. struct Monotonic_Queue{
  11. int l,r;
  12. int v[maxn],q[maxn];
  13. void init(){
  14. l=1,r=0;
  15. }
  16. void check1(int k){
  17. while(l<=r&&q[l]<=k)
  18. l++;
  19. }
  20. void check2(int k){
  21. while(l<=r&&q[l]>=k)
  22. l++;
  23. }
  24. void push(int a,int b){
  25. while(l<=r&&a<=v[r])
  26. r--;
  27. v[++r]=a,q[r]=b;
  28. }
  29. int front(){
  30. return l>r? inf:v[l];
  31. }
  32. }MQ,q[maxn];
  33. signed main(){
  34. scanf("%lld%lld",&n,&L);
  35. for(int i=1;i<=n;i++){
  36. scanf("%lld",&a[i]);
  37. q[i].init();
  38. }
  39. for(int l=n-1;l>=1;l--){
  40. int pos=l;
  41. MQ.init(),MQ.push(0+a[l],l);
  42. q[l+1].push(0+a[l],l);
  43. for(int r=l+1;r<=n;r++){
  44. while(pos<r&&f[l][pos]<=f[pos+1][r])
  45. pos++;
  46. MQ.check1(pos-1);
  47. q[r].check2(pos);
  48. f[l][r]=min(MQ.front(),q[r].front());
  49. MQ.push(f[l][r]+a[r],r);
  50. if(l>1)
  51. q[r].push(f[l][r]+a[l-1],l-1);
  52. }
  53. }
  54. if(f[1][n]>L)
  55. puts("wykrank1ingenerals!");
  56. else printf("%lld\n",f[1][n]);
  57. return 0;
  58. }

T2 赛道系统(

原题:P7100 [w3R1] 团

题意:
给定个集合,每个集合有若干个元素对,表示其中所有元素两两之间连一条边,边权为两个元素相对应的权值,求单源最短路径。

T2是这场比赛的签到题,应该大家都AC了吧。(在我心中和简单的CSPT2差不多)

对于每个集合,我们可以建一个虚点,其中每一个点向虚点连一条它权值的边

考虑这为什么对:因为我们的虚点之间没有边,因此每个点想要到其他的点一定会经过虚点,因此这个路径的长度是等于这两个点在集合中的点权之和的。

然后我们直接跑一个就可以了。

时间复杂度:

  1. #include<stdio.h>
  2. #include<queue>
  3. #define inf 0x3f3f3f3f3f3f3f3fll
  4. using namespace std;
  5. const int maxn=600005,maxm=800005;
  6. int n,k,e;
  7. int start[maxn],to[maxm],then[maxm],vis[maxn];
  8. long long worth[maxm],dis[maxn];
  9. priority_queue< pair<long long,int> >q;
  10. inline void add(int x,int y,long long z){
  11. then[++e]=start[x],start[x]=e,to[e]=y,worth[e]=z;
  12. }
  13. void dijkstra(int s){
  14. for(int i=1;i<=n+k;i++)
  15. dis[i]=inf,vis[i]=0;
  16. dis[s]=0,q.push(make_pair(0,s));
  17. while(!q.empty()){
  18. int x=q.top().second;
  19. q.pop();
  20. if(vis[x])
  21. continue;
  22. vis[x]=1;
  23. for(int i=start[x];i;i=then[i]){
  24. int y=to[i];
  25. if(dis[y]>dis[x]+worth[i])
  26. dis[y]=dis[x]+worth[i],q.push(make_pair(-dis[y],y));
  27. }
  28. }
  29. }
  30. int read(){
  31. int x=0;
  32. char c=getchar();
  33. for(;c<'0'||c>'9';c=getchar());
  34. for(;c>='0'&&c<='9';c=getchar())
  35. x=x*10+c-48;
  36. return x;
  37. }
  38. int main(){
  39. n=read(),k=read();
  40. for(int i=1;i<=k;i++){
  41. int s,x,y;
  42. s=read();
  43. for(int j=1;j<=s;j++){
  44. x=read(),y=read();
  45. add(x,n+i,y),add(n+i,x,y);
  46. }
  47. }
  48. dijkstra(1);
  49. for(int i=1;i<=n;i++)
  50. printf("%lld%c",dis[i],i==n? '\n':' ');
  51. return 0;
  52. }

T3 加法调用(

原题CF520E Pluses everywhere,本题套上了一个期望。

题意:
给定一个正整数,在的所有数位中随机放置个加号,求表达式值的期望。

较简单的一道题,预测会有巨佬推完式子。(在我心中和CSPT3差不多)

给定一个长度为的数,在的数位中放入个加号,求这个值的期望。

转化:求所有的值之和,除以,即个数位,放入个加号。

考虑每一位与这一位后面第一个加号:若考虑到第位,它后面第一个加号位置在后面。

一个一个考虑,如果第一个加号在后面,那么作为个位,贡献为,这种情况会出现次,即还剩个位置,放置个加号。

同理,如果第一个加号在后面,那么作为十位,贡献为,这种情况会出现次。

一般的,如果第一个加号在后面,那么作为第位(从低到高数),贡献为,出现次数为

还有一种情况,就是后面没有加号,那么作为第位(从低到高数),贡献为,出现次数为

统计一下和:

乍一看这个求和式是二维的,但是它可以转化一下:

我们考虑交换求和式,枚举位数

这样,因为无关,所以可以拆出来:

处理一下前缀和就可以做到了。

  1. #include<stdio.h>
  2. #include<iostream>
  3. #define int long long
  4. using namespace std;
  5. const int maxn=2000005,mod=1777771;
  6. int i,j,k,m,n,ans;
  7. int fac[maxn],nfac[maxn],sum[maxn],pow10[maxn];
  8. string s;
  9. inline int c(int n,int m){
  10. return fac[n]*nfac[m]%mod*nfac[n-m]%mod;
  11. }
  12. int ksm(int a,int b){
  13. int res=1;
  14. while(b){
  15. if(b&1)
  16. res=res*a%mod;
  17. a=a*a%mod,b>>=1;
  18. }
  19. return res;
  20. }
  21. signed main(){
  22. cin>>s>>m;
  23. n=s.size(),s=" "+s;
  24. fac[0]=1;
  25. for(i=1;i<=n;i++)
  26. fac[i]=fac[i-1]*i%mod;
  27. nfac[n]=ksm(fac[n],mod-2);
  28. for(i=n;i>=1;i--)
  29. nfac[i-1]=nfac[i]*i%mod;
  30. pow10[0]=1;
  31. for(i=1;i<=n;i++){
  32. sum[i]=sum[i-1]+(s[i]-48);
  33. pow10[i]=pow10[i-1]*10ll%mod;
  34. }
  35. for(i=1;i<=n-m;i++)
  36. ans=(ans+pow10[i-1]*sum[n-i]%mod*c(n-i-1,m-1)%mod)%mod;
  37. for(i=m+1;i<=n;i++)
  38. ans=(ans+pow10[n-i]*(s[i]-48)*c(i-1,m)%mod)%mod;
  39. printf("%lld\n",ans*ksm(c(n-1,m)%mod,mod-2)%mod);
  40. return 0;
  41. }

T4 树上的树(

原题CF1179D Fedor Runs for President

题意:
定义简单路径为任意一个点仅经过一次的路径。给定一颗个点的树,求添加一条边后最多有多少无向简单路径。

本场比赛最套路的一道题。(在我心中和CSPT4差不多)

先讲一下定义:的子树大小,的儿子数量,所有儿子形成的集合,路径上所有点形成的集合。

因为树上每两个点有且仅有一条路径,所以原本树上的路径数量为,然后考虑添加一条边会增加多少条无向简单路径:

假如我们将要添加,那么我们先提出的路径,假如在路径上,我们称为以为根,不向路径上扩展的子树大小。那么我们可以知道每一颗这样的子树之间都多了一条路径,根据乘法原理有这条边对答案贡献为(除是为了去重)。

变一下型:

那么我们的目的找到一条路径,让最小。

我们运用点分治的思想,考虑枚举每一个点,计算子树中,每一条经过的路径的答案。

为从子树中任意结点,且满足让上式最小的路径,那么很容易列出转移方程:

解释一下,相当于(因为的儿子在路径上)。

这个转移方程很显然,也很容易在的复杂度中求出。

  1. size[x]=1;
  2. for(int i=start[x];i;i=then[i]){
  3. int y=to[i];
  4. if(y==last)
  5. continue;
  6. dfs(y,x);
  7. size[x]+=size[y];
  8. }
  9. f[x]=size[x]*size[x];
  10. for(int i=start[x];i;i=then[i]){
  11. int y=to[i];
  12. if(y==last)
  13. continue;
  14. f[x]=min(f[x],f[y]+(size[x]-size[y])*(size[x]-size[y]));
  15. p[++ps]=y;
  16. }

然后考虑子树中,每一条经过的路径,设这条路径经过两个的儿子,那么我们可以把代表的路径,代表的路径和拼起来计算答案。

我们枚举两个儿子:

同样解释一下,是除了子树和子树中的结点外所有的结点,因为在均路径上,所以其他的点都必须分配到子树中,即

枚举它的复杂度就是了,加上,复杂度肯定无法通过本题。

考虑树上斜率优化,我们枚举的儿子,然后在斜率优化中求出决策点

套路性地枚举两个决策点,且更优,即

拆开平方,消掉相同的项就可以得到

套路性变形:

因为,那么除过来不变号,化成斜率式:

注意如果需要特判一下:

  1. inline int x(int p){
  2. return size[p];
  3. }
  4. inline int y(int p){
  5. return f[p]+size[p]*size[p];
  6. }
  7. inline double slope(int a,int b){
  8. if(x(a)==x(b))
  9. return y(a)>y(b)? inf:-inf;
  10. return 1.0*(y(a)-y(b))/(x(a)-x(b));
  11. }

我们在斜率优化之前给所有儿子按照排一下序,就可以上斜率优化板子了。


我们分析一下时间复杂度:对于每个点,复杂度的瓶颈就是遍历到后面的所有儿子和给它所有儿子一遍。

先证明一个引理:

不妨设,那么

对于排序,它的复杂度是的,因此我们可以直接看做每个点都需要的处理。

然后,我们处理递归:

可以按照深度归纳,我们证明对于,处理它的复杂度一定是的。

首先对于叶子结点一定成立,因为处理它的复杂度就是的。

对于非叶子结点,假如它的儿子是,且它每个儿子都满足这个复杂度约束,那么处理它儿子的总复杂度为,按照上面的引理一直处理下去,就可以得到上式小于等于,因为,所以递归所有儿子的复杂度是的。

再加上排序的复杂度,便可以得出处理的复杂度是的。

我们从开始,那么由上面的证明可以知道处理的时间复杂度为

即总复杂度为

  1. #include<stdio.h>
  2. #include<algorithm>
  3. #define int long long
  4. #define inf 1000000000000000000
  5. using namespace std;
  6. const int maxn=500005,maxm=1000005;
  7. int i,j,k,m,n,e,ans=inf;
  8. int start[maxn],to[maxm],then[maxm],f[maxn],size[maxn],p[maxn],q[maxn];
  9. inline void add(int x,int y){
  10. then[++e]=start[x],start[x]=e,to[e]=y;
  11. }
  12. inline bool cmp(int a,int b){
  13. return size[a]<size[b];
  14. }
  15. inline int x(int p){
  16. return size[p];
  17. }
  18. inline int y(int p){
  19. return f[p]+size[p]*size[p];
  20. }
  21. inline double slope(int a,int b){
  22. if(x(a)==x(b))
  23. return y(a)>y(b)? inf:-inf;
  24. return 1.0*(y(a)-y(b))/(x(a)-x(b));
  25. }
  26. void dfs(int x,int last){
  27. int ps=0,l=1,r=0;
  28. size[x]=1;
  29. for(int i=start[x];i;i=then[i]){
  30. int y=to[i];
  31. if(y==last)
  32. continue;
  33. dfs(y,x);
  34. size[x]+=size[y];
  35. }
  36. f[x]=size[x]*size[x];
  37. if(size[x]==1)
  38. return ;
  39. for(int i=start[x];i;i=then[i]){
  40. int y=to[i];
  41. if(y==last)
  42. continue;
  43. f[x]=min(f[x],f[y]+(size[x]-size[y])*(size[x]-size[y]));
  44. p[++ps]=y;
  45. }
  46. if(x==1)
  47. ans=min(ans,f[x]);
  48. sort(p+1,p+1+ps,cmp);
  49. q[++r]=p[1];
  50. for(int i=2;i<=ps;i++){
  51. while(l<r&&slope(q[l+1],q[l])<=2*(n-size[p[i]]))
  52. l++;
  53. ans=min(ans,f[p[i]]+f[q[l]]+(n-size[p[i]]-size[q[l]])*(n-size[p[i]]-size[q[l]]));
  54. while(l<r&&slope(p[i],q[r-1])<=slope(q[r],q[r-1]))
  55. r--;
  56. q[++r]=p[i];
  57. }
  58. }
  59. signed main(){
  60. scanf("%lld",&n);
  61. for(i=1;i<n;i++){
  62. int x,y;
  63. scanf("%lld%lld",&x,&y);
  64. add(x,y),add(y,x);
  65. }
  66. dfs(1,0);
  67. printf("%lld\n",n*(n-1)/2+(n*n-ans)/2);
  68. return 0;
  69. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注