11月1日 2023CSP-S 复赛模测(“日记和×××”系列) - 模拟赛记录
Preface
这套题说实话挺水的,不仅仅是在数据上(实际得分比赛时估分高了 \(50+\) 分),而且正解也神奇得不像个正解(各种分类讨论卡子任务,感觉像是出题人水平不够一样)。
怪不得教练敢说没考到 \(300\) 分的都该多练。
总体来说,第一二四题的做题手感、做题策略都挺好的,只是第三题实在没有啥思路(背包没学好),这个时候就应该先把各种特殊性质打完的,不要老想着一开始就打正解或者半正解,有时候打一打特殊性质既可以有分保底,又可以加深题目理解。
日记和最短路(shortway
)
(话说最短路的英语不应该是 shortest path
吗?)
题目中给了一个 DAG,然后要求用两种方式跑最短路。
看到 DAG,又要求最短路,第一时间想到先拓扑排序再做 DP,似乎只需要 \(O(N+M)\) 的时间复杂度。
然而这里有一个大问题:因为边权是一个字符串,而在组成最短路和比较路径大小的时候最坏时间复杂度是 \(O(\sum \lvert w_i \rvert)\),所以按照以上方法的实际时间复杂度应当是 \(O(N+M\sum \lvert w_i \rvert)\),虽然不可能卡得满,但是硬要算的话,期望得分应当只有 \(16\) 分才对。
然而,用这样的方法可以卡到 \(96\) 分,可见数据几乎完全没有卡字符串比较和合并的时间复杂度。
正解似乎是要使用拆点什么的,但是我不会,所以就只放上面说的 \(96\) 分代码了:
赛时超常发挥的神奇 $96$ 分代码
#include<queue>
#include<string>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1e5+5,M=5e5+5;
int n,m;
struct Allan{
int to,nxt;
string val;
}edge[M];
int head[N],idx;
inline void add(int x,int y,string &z)
{
edge[++idx]={y,head[x],z};
head[x]=idx;
return;
}
int indeg[N];
queue<int> q;
int top_order[N],top_idx;
void TopSort(int src)
{
q.push(src);
indeg[src]--;
while(!q.empty())
{
int x=q.front(); q.pop();
top_order[++top_idx]=x;
for(int i=head[x];i;i=edge[i].nxt)
{
int y=edge[i].to;
indeg[y]--;
if(!indeg[y])
q.push(y);
}
}
return;
}
string sp1[N];
bool cmp1(const string &x,const string &y,const string &z)
{
if(z=="-") return true;
else if(x.length()+y.length()!=z.length())
return x.length()+y.length()<z.length();
else return x+y<z;
}
void Solve1()
{
for(int p=1;p<=top_idx;p++)
{
int x=top_order[p];
for(int i=head[x];i;i=edge[i].nxt)
{
int y=edge[i].to; string z=edge[i].val;
if(cmp1(sp1[x],z,sp1[y])) //sp[x]+z<sp[y]
sp1[y]=sp1[x]+z;
}
if(x!=n) sp1[x].clear();
}
return;
}
string sp2[N];
bool cmp2(const string &x,const string &y,const string &z)
{
if(z=="-") return true;
else return x+y<z;
}
void Solve2()
{
for(int p=1;p<=top_idx;p++)
{
int x=top_order[p];
for(int i=head[x];i;i=edge[i].nxt)
{
int y=edge[i].to; string z=edge[i].val;
if(cmp2(sp2[x],z,sp2[y])) //sp[x]+z<sp[y]
sp2[y]=sp2[x]+z;
}
if(x!=n) sp2[x].clear();
}
return;
}
int main()
{
freopen("shortway.in","r",stdin);
freopen("shortway.out","w",stdout);
ios::sync_with_stdio(false);
cin.tie(nullptr),cout.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int x,y; string z;
cin>>x>>y>>z;
add(x,y,z);
indeg[y]++;
}
TopSort(1);
for(int i=1;i<=n;i++)
sp1[i]=sp2[i]="-";
sp1[1]=sp2[1]="";
Solve1();
Solve2();
cout<<sp1[n]<<' '<<sp2[n]<<endl;
return 0;
}
赛后补的 $100$ 分代码
#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int N=2e6+5;
int n,m;
struct ALLAN{
struct Allan{
int to,nxt;
char val;
}edge[N];
int head[N],idx;
void add(int x,int y,char z)
{
edge[++idx]={y,head[x],z};
head[x]=idx;
}
int sp[N];
queue<int> q;
void find_sp(int src)
{
memset(sp,-1,sizeof(sp));
sp[src]=0;
q.push(src);
while(!q.empty())
{
int x=q.front(); q.pop();
for(int i=head[x];i;i=edge[i].nxt)
{
int y=edge[i].to;
if(sp[y]==-1) sp[y]=sp[x]+1,q.push(y);
}
}
}
}g,rg;
int work[N],work_len;
int tmp[N],tmp_len;
char ans[N]; int ans_len;
void solve(bool op)
{
work_len=1; work[work_len]=1;
ans_len=tmp_len=0;
while(work_len)
{
tmp_len=0;
char now='|';
bool reach_n=false;
for(int p=1;p<=work_len;p++)
{
int x=work[p];
for(int i=g.head[x];i;i=g.edge[i].nxt)
{
int y=g.edge[i].to;
if(rg.sp[y]==-1) continue;
if(op && g.sp[x]+1+rg.sp[y]>g.sp[n]) continue;
if(g.edge[i].val<now)
{
reach_n=(y==n);
tmp_len=1;
tmp[tmp_len]=y;
now=g.edge[i].val;
}
else if(g.edge[i].val==now)
{
if(y==n) reach_n=true;
tmp[++tmp_len]=y;
}
}
}
ans[ans_len++]=now;
if(reach_n) break;
for(int i=1;i<=tmp_len;i++)
work[i]=tmp[i];
work_len=tmp_len;
}
ans[ans_len++]='\0';
printf("%s ",ans);
return;
}
char str[N];
int main()
{
freopen("shortway.in","r",stdin);
freopen("shortway.out","w",stdout);
scanf("%d%d",&n,&m);
int pos=n;
for(int i=1;i<=m;i++)
{
int x,y; scanf("%d%d%s",&x,&y,str);
int slen=strlen(str)-1;
if(!slen) g.add(x,y,str[0]),rg.add(y,x,str[0]);
else
{
pos++; g.add(x,pos,str[0]),rg.add(pos,x,str[0]);
for(int j=1;j<slen;j++,pos++)
g.add(pos,pos+1,str[j]),rg.add(pos+1,pos,str[j]);
g.add(pos,y,str[slen]),rg.add(y,pos,str[slen]);
}
}
g.find_sp(1),rg.find_sp(n);
solve(1),solve(0);
return 0;
}
日记和欧拉函数(euler
)
这道题倒是和赛时估分一分不差。
所有的欧拉函数值很明显可以预处理出来,因为不会写线筛求欧拉函数(现在会了),所以我把每个欧拉函数都直接算了出来,时间复杂度 \(O(R \sqrt{R})\)。
然后题目中要求的求和可以很容易想到用前缀和的方式来处理,这样可以 \(O(1)\) 查询。只是没想到赛时为了卡一下常写的 if(res==1) return 1;
用来判断 \(\varphi^{(k)}1=1\) 居然一下子把 \(O(R^2)\) 卡到了 \(O(R \log R)\),真是意外收获。
这样总时间复杂度就是 \(O(R \sqrt{R} + T)\),拿了 \(52\) 分。
赛后去复习补充了线筛求欧拉函数的知识,时间复杂度成功降至 \(O(R \log R + T)\),得到 \(72\) 分
赛时乱打的 $52$ 分代码
#include<cstdio>
#include<cmath>
#include<algorithm>
#define LL long long
using namespace std;
const int T=1e5+5,R=1e6+5;
int t,b;
pair<int,int> q[T];
int Phi(int x) //O(sqrt(R))
{
int res=x,sx=sqrt(x);
for(int i=2;i<=sx;i++)
{
if(x%i==0)
{
while(x%i==0)
x/=i;
res=res/i*(i-1);
}
}
if(x>1) res=res/x*(x-1);
return res;
}
int phi[R];
void Init_phi(int maxr) //O(R*sqrt(R))
{
for(int i=1;i<=maxr;i++)
phi[i]=Phi(i);
return;
}
int ExPhi(int x,int y) //O(R)
{
int res=x;
for(int i=1;i<=y;i++)
{
res=phi[res];
if(res==1) return 1;
}
return res;
}
int exphi[R];
LL sum[R];
void Init_sum(int maxr) //O(R^2)
{
int mx=0;
for(int i=1;i<=maxr;i++)
{
mx=max(mx,phi[i]);
exphi[i]=ExPhi(i,mx-b);
sum[i]=sum[i-1]+exphi[i];
}
return;
}
int main()
{
freopen("euler.in","r",stdin);
freopen("euler.out","w",stdout);
scanf("%d%d",&t,&b);
int maxr=0;
for(int i=1;i<=t;i++)
{
int l,r; scanf("%d%d",&l,&r);
maxr=max(maxr,r);
q[i]={l,r};
}
Init_phi(maxr);
Init_sum(maxr);
for(int i=1;i<=t;i++)
{
int l=q[i].first,r=q[i].second;
printf("%lld\n",sum[r]-sum[l-1]);
}
return 0;
}
赛后乱打的 $72$ 分代码
#include<cstdio>
#include<cmath>
#include<bitset>
#include<algorithm>
#define LL long long
using namespace std;
const int T=1e5+5,R=1e6+5,PR=8e4+5;
int t,b;
pair<int,int> q[T];
int phi[R];
bitset<R> not_prime;
int prime[PR],prime_idx;
void Init_phi(int maxr)
{
//Euler_sieve / Linear_sieve
not_prime[1]=true;
phi[1]=1;
for(int i=2;i<=maxr;i++)
{
if(!not_prime[i])
{
prime[++prime_idx]=i;
phi[i]=i-1;
}
for(int j=1;j<=prime_idx && 1ll*i*prime[j]<=maxr;j++)
{
not_prime[i*prime[j]]=true;
if(i%prime[j]==0)
{
phi[i*prime[j]]=phi[i]*prime[j];
break;
}
else phi[i*prime[j]]=phi[i]*phi[prime[j]];
}
}
return;
}
int ExPhi(int x,int y) //O(R)
{
int res=x;
for(int i=1;i<=y;i++)
{
res=phi[res];
if(res==1) return 1;
}
return res;
}
int exphi[R];
LL sum[R];
void Init_sum(int maxr) //O(R^2)
{
int mx=0;
for(int i=1;i<=maxr;i++)
{
mx=max(mx,phi[i]);
exphi[i]=ExPhi(i,mx-b);
sum[i]=sum[i-1]+exphi[i];
}
return;
}
int main()
{
freopen("euler.in","r",stdin);
freopen("euler.out","w",stdout);
scanf("%d%d",&t,&b);
int maxr=0;
for(int i=1;i<=t;i++)
{
int l,r; scanf("%d%d",&l,&r);
maxr=max(maxr,r);
q[i]={l,r};
}
Init_phi(maxr);
Init_sum(maxr);
for(int i=1;i<=t;i++)
{
int l=q[i].first,r=q[i].second;
printf("%lld\n",sum[r]-sum[l-1]);
}
return 0;
}
为什么说正解不像个正解呢?这个正解其实是在找规律,规律就是只有从 \(B\) 到后面某一截是需要单独计算的,之前的都以等差数列形式排布,而之后的全部都是 \(1\),然后就做出来了。
然而有一个测试点(测试数据 #5)一直在 \(900\) ms+,其它数据都只用几十毫秒,只有这个一枝独秀,鹤立鸡群,我加了记忆化卡常后保持 \(800\) ms 左右的稳定发挥,完全不符合理论时间复杂度,搞得我头疼。
玄之又玄的 $100$ 分代码
#include<cstdio>
#include<cmath>
#include<bitset>
#include<algorithm>
#define LL long long
using namespace std;
const int L=50;
int t,b;
int Phi(int x) //O(sqrt(R))
{
int res=x,sx=sqrt(x);
for(int i=2;i<=sx;i++)
{
if(x%i==0)
{
while(x%i==0)
x/=i;
res=res/i*(i-1);
}
}
if(x>1) res=res/x*(x-1);
return res;
}
int ExPhi(int x,int y) //O(R)
{
int res=x;
for(int i=1;i<=y;i++)
{
res=Phi(res);
if(res==1) return 1;
}
return res;
}
int prime_less_b;
LL sum_bL;
bool is_prime(int x)
{
for(int i=2;i*i<=x;i++)
if(x%i==0) return false;
return true;
}
LL arith(int x)
{
return 1ll*(1+x)*x/2;
}
LL getsum(int x)
{
if(x<=b) return arith(x);
else if(x<=b+L)
{
LL res=arith(b);
int mxphi=prime_less_b-1;
for(int i=b+1;i<=x;i++)
{
mxphi=max(mxphi,Phi(i));
res+=ExPhi(i,mxphi-b);
}
return res;
}
else
{
LL res=arith(b)+sum_bL;
res+=x-(b+L);
return res;
}
}
int main()
{
freopen("euler.in","r",stdin);
freopen("euler.out","w",stdout);
scanf("%d%d",&t,&b);
for(int i=b;i>=2;i--)
if(is_prime(i))
{
prime_less_b=i;
break;
}
int tmxphi=prime_less_b-1;
for(int i=b+1;i<=b+L;i++)
{
tmxphi=max(tmxphi,Phi(i));
sum_bL+=ExPhi(i,tmxphi-b);
}
for(int i=1;i<=t;i++)
{
int l,r; scanf("%d%d",&l,&r);
printf("%lld\n",getsum(r)-getsum(l-1));
}
return 0;
}
玄之又玄的 $100$ 分代码 · 记忆化卡常
#include<map>
#include<cmath>
#include<cstdio>
#include<algorithm>
#include<unordered_map>
#define LL long long
using namespace std;
const int L=50;
int t,b;
unordered_map<int,int> phi;
int Phi(int x) //O(sqrt(R))
{
if(phi[x]) return phi[x];
int savex=x,res=x,sx=sqrt(x);
for(int i=2;i<=sx;i++)
{
if(x%i==0)
{
while(x%i==0)
x/=i;
res=res/i*(i-1);
}
}
if(x>1) res=res/x*(x-1);
return phi[savex]=res; //xÒѾ¸Ä±ä¹ýÁË£¡
}
map<pair<int,int>,int> exphi;
int ExPhi(int x,int y) //O(R)
{
if(exphi[{x,y}])
return exphi[{x,y}];
int res=x;
for(int i=1;i<=y;i++)
{
res=Phi(res);
if(res==1) return 1;
}
return exphi[{x,y}]=res;
}
int prime_less_b;
LL sum_bL;
bool is_prime(int x)
{
for(int i=2;i*i<=x;i++)
if(x%i==0) return false;
return true;
}
LL arith(int x)
{
return 1ll*(1+x)*x/2;
}
LL getsum(int x)
{
if(x<=b) return arith(x);
else if(x<=b+L)
{
LL res=arith(b);
int mxphi=prime_less_b-1;
for(int i=b+1;i<=x;i++)
{
mxphi=max(mxphi,Phi(i));
res+=ExPhi(i,mxphi-b);
}
return res;
}
else
{
LL res=arith(b)+sum_bL;
res+=x-(b+L);
return res;
}
}
int main()
{
freopen("euler.in","r",stdin);
freopen("euler.out","w",stdout);
scanf("%d%d",&t,&b);
for(int i=b;i>=2;i--)
if(is_prime(i))
{
prime_less_b=i;
break;
}
int tmxphi=prime_less_b-1;
for(int i=b+1;i<=b+L;i++)
{
tmxphi=max(tmxphi,Phi(i));
sum_bL+=ExPhi(i,tmxphi-b);
}
for(int i=1;i<=t;i++)
{
int l,r; scanf("%d%d",&l,&r);
printf("%lld\n",getsum(r)-getsum(l-1));
}
return 0;
}
日记和二叉搜索树(tree
)
这道题赛时做的一脸懵逼,到头来只打了个 \(8\) 分暴力。
其实分析大样例可以很容易发现特殊性质 \(s(1)\) 的答案就是 \(0\),还能再赚 \(16\) 分,但是我当时被这题搞得有点懵,所以就没注意到。
而且特殊性质 \(s(2)\) 时最优排列就是这棵树的中序遍历序列,这个也想到了的,没去打真是不该,又少 \(8\) 分,唉。
(注:如无特殊说明,下面所说的“子树”都是指以当前节点的子节点为根的子树)
赛时思路和题解核心思想已经大差不差了,即对于每一个点,以它作为 LCA 的贡献就是权值比它小的子树大小和乘以权值比它大的子树大小和。而且赛时已经证出来了当这两部分大小最接近时贡献最大,只是没想出来如何让这两部分大小最接近,且它到底是什么编号无关紧要的这一点没有想到,所以没打出来。
赛后补充了 01 背包求这一部分的相关知识:具体来说,就是将每一棵子树的体积和价值都设为它的大小,然后在总体积不超过 \(\frac{sum-1}{2}\) 的前提下找最大价值,这个最大价值 \(t\) 乘以 \((sum-1-t)\)(减一是因为减去当前节点自己)就是当前节点的最大贡献。
因为每一次背包的时间复杂度都是 \(O(son_x \times size_x)\),其中 \(s_x\) 表示 \(x\) 的子节点数量,而 \(\sum son_x = n-1\),所以总时间复杂度为 \(O(N^2)\),且一般卡不满,故期望得分 \(48\) 分。
但是!(在加了特判链的情况下)它就是能跑到 \(96\) 分(还是在某些老年机上),包括一大堆 \(N \le 10^6\) 的点都卡过了(\(N\) 方过百万是吧),所以我说这个数据水呢~
赛后莫名其妙的 $96$ 分代码
#include<cstdio>
#include<vector>
#include<algorithm>
using namespace std;
const int N=1e6+5,M=2e6+5;
int n;
long long ans;
struct Allan{
int to,nxt;
}edge[M];
int head[N],idx;
inline void add(int x,int y)
{
edge[++idx]={y,head[x]};
head[x]=idx;
return;
}
int sz[N];
vector<int> sonsz[N];
int sonf[N];
void DFS(int x,int fa=0)
{
sz[x]=1;
for(int i=head[x];i;i=edge[i].nxt)
{
int y=edge[i].to;
if(y==fa) continue;
DFS(y,x);
sz[x]+=sz[y];
sonsz[x].push_back(sz[y]);
}
int mxsz=(sz[x]-1)>>1;
for(int i=1;i<=mxsz;i++)
sonf[i]=0;
for(int i=0;i<(int)sonsz[x].size();i++)
for(int j=mxsz;j>=(int)sonsz[x][i];j--)
sonf[j]=max(sonf[j],sonf[j-(int)sonsz[x][i]]+sonsz[x][i]);
ans+=1ll*sonf[mxsz]*(sz[x]-1-sonf[mxsz]);
return;
}
int deg[N];
int main()
{
freopen("tree.in","r",stdin);
freopen("tree.out","w",stdout);
scanf("%d",&n);
bool is_s1=true;
for(int i=1;i<n;i++)
{
int x,y; scanf("%d%d",&x,&y);
add(x,y),add(y,x);
deg[x]++,deg[y]++;
if(deg[x]>2||deg[y]>2) is_s1=false;
}
if(is_s1)
{
printf("0\n");
return 0;
}
DFS(1);
printf("%lld\n",ans);
return 0;
}
日记和编辑器(edit
)
一眼看过去:数据结构。
然后发现前四种操作暴力都是 \(O(\sum \lvert s \rvert)\) 的,而第五种操作用 KMP 优化一下也是 \(O(\sum \lvert s \rvert)\) 的。
加上一共 \(n\) 次操作,而 \(\sum \lvert s \rvert \le 10n\),所以这样暴力的时间复杂度是 \(O(N^2)\),期望得分 \(40 \sim 60\) 分,实际得分 \(60\) 分。
(据说不用 KMP 的 \(O(N^3)\) 暴力也是 \(60\) 分,这不公平!)
正解似乎要用平衡树,但是我还是不会。
终于正常一点的 $60$ 分代码
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+5,LEN=1e6+5,PLEN=25;
int n;
char S[LEN]; int len;
void Insert(int x,char str[])
{
int slen=strlen(str+1);
for(int i=len+slen;i>x+slen;i--)
S[i]=S[i-slen];
for(int i=x+1;i<=x+slen;i++)
S[i]=str[i-x];
len+=slen;
return;
}
void Delete(int l,int r)
{
int slen=r-l+1;
for(int i=r+1;i<=len;i++)
S[i-slen]=S[i];
len-=slen;
return;
}
void Replace(int l,int r,char str[])
{
Delete(l,r);
Insert(l-1,str);
return;
}
int Count(int l,int r,char ch)
{
int res=0;
for(int i=l;i<=r;i++)
if(S[i]==ch) res++;
return res;
}
char p[PLEN];
int plen,nxt[LEN];
void KMP_Init()
{
plen=strlen(p+1);
nxt[1]=0;
for(int i=2,j=0;i<=plen;i++)
{
while(j && p[j+1]!=p[i]) j=nxt[j];
if(p[j+1]==p[i]) j++;
nxt[i]=j;
}
return;
}
int Search(int l,int r)
{
int res=0;
for(int i=l,j=0;i<=r;i++)
{
while(j && p[j+1]!=S[i]) j=nxt[j];
if(p[j+1]==S[i]) j++;
if(j==plen) res++,j=nxt[j];
}
return res;
}
char tmp[LEN];
int main()
{
freopen("edit.in","r",stdin);
freopen("edit.out","w",stdout);
scanf("%d%s",&n,p+1);
KMP_Init();
for(int i=1;i<=n;i++)
{
char op[15]; scanf("%s",op);
if(op[0]=='I') //Insert
{
int x; scanf("%d%s",&x,tmp+1);
Insert(x,tmp);
}
if(op[0]=='D') //Delete
{
int l,r; scanf("%d%d",&l,&r);
Delete(l,r);
}
if(op[0]=='R') //Replace
{
int l,r; scanf("%d%d%s",&l,&r,tmp+1);
Replace(l,r,tmp);
}
if(op[0]=='C') //Count
{
int l,r; char c[5];
scanf("%d%d%s",&l,&r,c);
int ans=Count(l,r,c[0]);
printf("%d\n",ans);
}
if(op[0]=='S') //Search
{
int l,r; scanf("%d%d",&l,&r);
int ans=Search(l,r);
printf("%d\n",ans);
}
}
return 0;
}
本文采用 「CC-BY-NC 4.0」 创作共享协议,转载请注明作者及出处,禁止商业使用。
作者:Jerrycyx,原文链接:https://www.cnblogs.com/jerrycyx/p/18520827