CSP-S前总复习
里面大概有一两个星期吧,挑一些有价值的写。
注:后面的题目因为想省事,就不放代码了
CSP-S 目标是 6 级勾,所以蓝题以下需要稳定做到每题至少70pts+,一些紫题的部分分,树上数据结构什么的需要过过板子。
基础算法需要熟练,思维能力需要提高。可以把历年的真题做做练练综合能力,以及需要看看ARC的题目提高思维。
结合这几次的模拟赛写点心得:
-
T1 应该做到稳过,而且时间要快。这几次要不就是 T1 没过要不就是过了以后时间所剩无几。
-
T2 只要有分就应该去写,没准写着写着就搞出来高分做法了。
-
T3 T4 记得看题,能简单推的性质就应该先推,一看上去能写暴力的一定写,否则去找特殊性质,把能写的都写了。
-
个人感觉比赛最后10分钟有用。
-
保持好心情
-
不要听一些洗脑的歌,放空大脑。
下面放点最近几天挑的题,最后可以览一下过过脑子。
[ABC369F] Gather Coins
来补的题目。
先考虑不输出方案的写法。排序过后可以用一个 DP 实现。
注意到 DP 的转移方程只和 max
有关,所以可以用数据结构优化。
排序过后保证横坐标不降,所以只需要对纵坐标开一个树状数组,维护最大值,能做到 \(\mathcal{O}(k\log n)\) 的转移。
现在要求输出方案,我们可以记录每一个 DP 数组中的元素从哪里转移过来,最后倒序查找输出。
这样的话我们同时需要对树状数组多维护点东西。除了基本的记录 max
数组,我们再开一个记录编号的数组,记录树状数组中每一个值对应的编号。
这样转移的时候更新最大值时可以同时更新编号数组,实现维护。
点击查看代码
#include <bits/stdc++.h>
// #define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int maxn=5e5+10;
const int mod=998244353;
const int inf=1e9+7;
int n,m,k;
struct no
{
int x,y;
}a[maxn];
int dp[maxn],pre[maxn];
int c[maxn<<1],d[maxn<<1];
int lb(int x){return x&-x;}
void add(int x,int y,int id){for(;x<=maxn;x+=lb(x)){if(c[x]<y)c[x]=y,d[x]=id;}}
no ask(int x){int ans=0,id=0;for(;x;x-=lb(x)){if(c[x]>ans){ans=c[x];id=d[x];}}return {ans,id};}
vector<char> v;
void work(no s,no t)
{
for(int i=1;i<=abs(s.x-t.x);i++)v.push_back('D');
for(int i=1;i<=abs(s.y-t.y);i++)v.push_back('R');
}
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
// #else
// freopen("Stone.in","r",stdin);
// freopen("Stone.out","w",stdout);
#endif
cin>>n>>m>>k;
for(int i=1;i<=k;i++)a[i]={read(),read()};
sort(a+1,a+k+1,[](no x,no y){return x.x<y.x||x.x==y.x&&x.y<y.y;});
for(int i=1;i<=k;i++)dp[i]=1;
add(a[1].y,1,1);
for(int i=2;i<=k;i++)
{
no now=ask(a[i].y);
dp[i]=now.x+1;
pre[i]=now.y;
add(a[i].y,dp[i],i);
// cout<<i<<' '<<pre[i]<<endl;
}
int ans=0,id=0;
for(int i=1;i<=k;i++)
{
if(dp[i]>ans)
{
ans=dp[i];
id=i;
}
}
cout<<ans<<endl;
// cout<<id<<endl;
// cout<<a[id].x<<' '<<a[id].y<<endl;
work(no{n,m},a[id]);
// for(auto i : v)cout<<i;puts("");
while(1)
{
if(!pre[id]){work(no{1,1},a[id]);break;}
work(a[id],a[pre[id]]);
id=pre[id];
}
reverse(v.begin(),v.end());
for(auto i : v)cout<<i;puts("");
return 0;
}
[ABC127F] Absolute Minima
看着很数论。但是我们把函数的形式画一画可以发现最终会形成一个类似 \(f(x)=\sum_{i=1}^{n}|x-a_i|+\sum_{i=1}^{n}b_i\) 的形式。后面的随便维护,也对答案没有影响。
前面的题目要求的最小值,此时可以发现 \(x\) 是 \(a_i\) 中位数。因此我们需要一个能够维护单点插入和区间查询中位数的数据结构。这个显然可以平衡树,但是我只会写 pbds 的不重平衡树。但是这东西也可以用对顶堆维护。
对顶堆我之前只是知道原理,但是从没写过。思考一会设定让 \(\text{小根堆}-\text{大根堆}\in [0,1]\),然后照着思路写。
由于中位数候选答案可能有两个,所以需要分点情况,还需要考虑最开始的插入和查询时 RE 的可能性。(感谢CSR大佬的提醒
过了样例以后交上去一遍过。
点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int maxn=3e5+10;
const int inf=998244353;
const int mod=1e9+7;
int Q;
priority_queue<int> q1;//小的
priority_queue<int,vector<int>,greater<int> > q2;//大的
int sum1,sum2;
int sum=0;
signed main()
{
#ifdef Lydic
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
// #else
// freopen("Stone.in","r",stdin);
// freopen("Stone.out","w",stdout);
#endif
cin>>Q;
while(Q--)
{
int opt=read();
if(opt==1)
{
int a=read(),b=read();
sum+=b;
if(q1.empty())
{
q1.push(a);
sum1+=a;
continue;
}
if(a<=q1.top())q1.push(a),sum1+=a;
else q2.push(a),sum2+=a;
if(q1.size()<=q2.size())
{
if((q1.size()+q2.size())%2==0&&q1.size()==q2.size())continue;
int x=q2.top();
sum2-=x;
q2.pop();
sum1+=x;
q1.push(x);
}
else if(q1.size()>q2.size()+1)
{
int x=q1.top();
sum1-=x;
q1.pop();
sum2+=x;
q2.push(x);
}
}
else if(opt==2)
{
int x=0;
if((q1.size()+q2.size())&1)
{
x=q1.top();
printf("%lld %lld\n",x,x*q1.size()-sum1+sum2-x*q2.size()+sum);
}
else
{
x=q1.top();
int y=q2.top();
if(x*q1.size()-sum1+sum2-x*q2.size()<y*q1.size()-sum1+sum2-y*q2.size())
{
printf("%lld %lld\n",x,x*q1.size()-sum1+sum2-x*q2.size()+sum);
}
else if(x*q1.size()-sum1+sum2-x*q2.size()>y*q1.size()-sum1+sum2-y*q2.size())
{
printf("%lld %lld\n",y,y*q1.size()-sum1+sum2-y*q2.size()+sum);
}
else
{
printf("%lld %lld\n",x,x*q1.size()-sum1+sum2-x*q2.size()+sum);
}
}
}
}
return 0;
}
[ABC211C] chokudai
虽然就这道题来说,橙很合理,但是这种 DP 套路我记得很多都有,所以写到这里。
简单转化以后就是求特定子序列数量。
设 \(dp_{i,j}(i\in n,j\in 8)\) 表示枚举到 \(i\),且当前匹配到的字符是 \(j\) 的方案数。
转移的时候分别枚举,满足条件的话就让 \(dp_{i,j}\leftarrow dp_{i,j}+dp_{i-1,j-1}\) 即可。
本着能省空间就剩空间的原则,我们滚动数组,既可以减少码量,也可以避免挂分。
点击查看代码
#include <bits/stdc++.h>
// #define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int maxn=3e5+10;
const int inf=998244353;
const int mod=1e9+7;
char s[maxn];
int n,ans;
int dp[10]={1};
int dk[10]={'s','c','h','o','k','u','d','a','i'};
signed main()
{
#ifdef Lydic
freopen(".in", "r", stdin);
freopen(".out", "w", stdout);
// #else
// freopen("Stone.in","r",stdin);
// freopen("Stone.out","w",stdout);
#endif
scanf("%s",s+1);
n=strlen(s+1);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=8;j++)
{
if(s[i]==dk[j])(dp[j]+=dp[j-1])%=mod;
}
}
cout<<dp[8];
return 0;
}
[ABC365G] AtCoder Office
这个记忆化很好啊。
题目要求两个区间线段交的长度,看了大鸡子的题解感觉很对,递归区间的时候如果一段完全覆盖,那么取交的话这一段的贡献显然就是另一个区间的长度。
因为题目设置,所以不用懒标记。
但这样显然会卡你,所以伟大的 HDS 证明这样的情况不会很多并使用 map
进行记忆化,成功通过。
本地 OJ 上面 TLE 了,需要删 long long
。
关于这种线段树的数组空间计算一直不太会,所以现在无论如何都会开个 2e7
防炸。
点击查看代码
#include <bits/stdc++.h>
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int maxn=3e5+100;
const int mod=998244353;
const int inf=1e9+7;
int n,m,Q;
vector<int> v[maxn];
map<pair<int,int>,int> mp;
int rt[maxn],tot;
struct Seg
{
int l,r,d;
#define lc(p) t[p].l
#define rc(p) t[p].r
}t[maxn<<7];
void upd(int p){t[p].d=t[lc(p)].d+t[rc(p)].d;}
void change(int &p,int tl,int tr,int l,int r)
{
if(!p)p=++tot;
if(tl>=l&&tr<=r)
{
t[p].d=tr-tl+1;
return ;
}
int mid=(tl+tr)>>1;
if(mid>=l)change(lc(p),tl,mid,l,r);
if(mid<r)change(rc(p),mid+1,tr,l,r);
upd(p);
}
int ask(int x,int y,int l,int r)
{
if(!x||!y)return 0;
if(t[x].d==r-l+1)return t[y].d;
if(t[y].d==r-l+1)return t[x].d;
int mid=(l+r)>>1;
return ask(lc(x),lc(y),l,mid)+ask(rc(x),rc(y),mid+1,r);
}
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
// #else
// freopen("Stone.in","r",stdin);
// freopen("Stone.out","w",stdout);
#endif
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int t=read(),p=read();
v[p].push_back(t);
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<v[i].size();j+=2)
{
change(rt[i],1,inf,v[i][j],v[i][j+1]-1);
// cout<<v[i][j]<<' '<<v[i][j+1]<<endl;
}
}
Q=read();
while(Q--)
{
int x=read(),y=read();
if(x>y)swap(x,y);
if(mp.find({x,y})!=mp.end())printf("%lld\n",mp[{x,y}]);
else printf("%lld\n",mp[{x,y}]=ask(rt[x],rt[y],1,inf));
}
return 0;
}
[ZJOI2007] 时态同步
考虑最后的修改结果一定是根到叶节点深度的最大值,那么对每个子树分别考虑,不用去考虑哪个节点连接的路径才是最大值,直接记录子树中所有叶节点到这棵子树的根节点的距离最大值,累计答案的时候的时候就用 \(dis_x-(dis_y+E(x,y))\) 即可。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int mod=998244353;
const int maxn=7e5+10;
const int inf=1e17;
const double eps=1e-10;
int n,s;
struct no{int y,v;};
vector<no> G[maxn];
int ans=0,dep[maxn],dp[maxn];
void fid(int x,int fa)
{
for(auto i : G[x])
{
int y=i.y,v=i.v;
if(y==fa)continue;
fid(y,x);
dep[x]=max(dep[x],dep[y]+v);
}
for(auto i : G[x])
{
int y=i.y,v=i.v;
if(y==fa)continue;
ans+=dep[x]-dep[y]-v;
}
}
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
cin>>n>>s;
for(int i=1;i<n;i++)
{
int x=read(),y=read(),v=read();
G[x].push_back({y,v});
G[y].push_back({x,v});
}
fid(s,0);
// dfs(1,0);
cout<<ans;
return 0;
}
[ARC172A] Chocolate
思维很巧妙。
贪心的思路很好想,关键在于怎么维护。
细想可以发现一个矩形每次从中挖去一个正方形过后会产生两个新矩形,根据题目要求容易发现先沿着从上到下的顺序切再沿着从左到右的顺序切是最优的。这样我们可以用一个容器存储矩形的长和宽,每次取出一个合法矩形,把它从容器里面删除,再按照上文方法计算出两个新矩形的长和宽重新插进容器里面,就可以方便地维护当前矩形的所有信息。
这种维护矩阵填充的策略方法很实用。
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int mod=998244353;
const int maxn=7e5+10;
const int inf=1e17;
const double eps=1e-10;
int n,m,k;
int a[maxn];
struct Mar{int x,y;inline friend bool operator < (Mar a,Mar b){return (a.x<b.x)||(a.x==b.x&&a.y<b.y);}};
priority_queue<Mar> v;
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
cin>>n>>m>>k;
for(int i=1;i<=k;i++)a[i]=read();
for(int i=1;i<=k;i++)a[i]=(1<<a[i]);
sort(a+1,a+k+1);
reverse(a+1,a+k+1);
v.push({min(n,m),max(n,m)});
for(int i=1;i<=k;i++)
{
auto j=v.top();
if(j.x>=a[i])
{
v.push({min(j.x,j.y-a[i]),max(j.x,j.y-a[i])});
v.push({min(a[i],j.x-a[i]),max(a[i],j.x-a[i])});
v.pop();
}
else return 0 * puts("No");
}
puts("Yes");
return 0;
}
[USACO10HOL] Cow Politics G
题目相当于多次问你一棵树上的某些点距离的最大值,首先利用 \(lca\) 能够很快地求出任意两点的距离,然后我们发现对于每一组节点忽略其他节点后可以看做一个树的结构,我们可以利用求树的直径的方法求出最大距离。
这样最开始存储每个政党的奶牛集合,然后分别跑一遍树的直径就可以了。
点击查看代码
#include<bits/stdc++.h>
#define ll long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int mod=998244353;
const int maxn=5e5+10;
const int inf=2e9+10;
const double eps=1e-10;
int n,k;
vector<int> G[maxn];
int dep[maxn],f[maxn][22];
void dfs(int x,int fa)
{
dep[x]=dep[fa]+1;
f[x][0]=fa;
for(int i=1;i<=20;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(auto y : G[x])
{
if(y==fa)continue;
dfs(y,x);
}
}
int lca(int x,int y)
{
if(dep[y]<dep[x])swap(x,y);
int d=dep[y]-dep[x],i=0;
while(d)
{
if(d&1)y=f[y][i];
i++;d>>=1;
}
if(x==y)return y;
for(int i=20;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
int id[maxn];
int dis(int x,int y)
{
return dep[x]+dep[y]-dep[lca(x,y)]*2;
}
int ans[maxn];
vector<int> dev[maxn];
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
cin>>n>>k;int root=1;
for(int i=1;i<=n;i++)
{
int x=read(),y=read();
id[i]=x;dev[x].push_back(i);
if(!y){root=i;continue;}
G[i].push_back(y);
G[y].push_back(i);
}
dfs(root,0);
for(int i=1;i<=k;i++)
{
int idd=1,ma=0;
for(int j=0;j<dev[i].size();j++)
{
if(dis(dev[i][0],dev[i][j])>ma)
{
ma=dis(dev[i][0],dev[i][j]);
idd=dev[i][j];
}
}
int st=idd;
idd=1;ma=0;
for(int j=0;j<dev[i].size();j++)
{
if(dis(st,dev[i][j])>ma)
{
ma=dis(st,dev[i][j]);
idd=dev[i][j];
}
}
int en=idd;
ans[i]=dis(st,en);
// cout<<st<<' '<<en<<endl;
}
for(int i=1;i<=k;i++)cout<<ans[i]<<endl;
return 0;
}
「StOI-1」树上询问
大力分讨+码力题。
首先每次都做一遍肯定是不行的,所以我们不妨随便建一棵树,然后再这棵树上对每次询问考虑答案。
感觉实际上要分的情况不算太多,但是我当时写了挺多的。
- \(x=y=z\) 直接输出
- \(x=y||y=z\) 深度小的倍增跳 \(k\) 祖先计算,深度大的输出 \(siz\)
- \(z\) 在 \(x,y\) 路径上但不是LCA 分情况分别倍增跑 \(k\) 祖先。
- \(z\) 是LCA 求 LCA 的时候记录 LCA 向下的两个节点,分别跑出来以后计算。
反正就是分了一堆情况,最后也是过了。
好在我的 LCA 之类的预处理函数都是一遍过。
CSR大佬太强力。
点击查看代码
#include<bits/stdc++.h>
//#define int long long
using namespace std;
inline int read()
{
int w=1,s=0;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')w=-1;ch=getchar();}
while(isdigit(ch)){s=s*10+(ch-'0');ch=getchar();}
return w*s;
}
const int mod=1e9+7;
const int maxn=5e5+10;
const int inf=2e17+10;
const double eps=1e-10;
int n,Q;
int f[maxn][23],dep[maxn],siz[maxn];
vector<int> G[maxn];
void dfs(int x,int fa)
{
siz[x]=1;
f[x][0]=fa;dep[x]=dep[fa]+1;
for(int i=1;i<=20;i++)
f[x][i]=f[f[x][i-1]][i-1];
for(auto y : G[x])
{
if(y==fa)continue;
dfs(y,x);
siz[x]+=siz[y];
}
}
int lca(int x,int y)
{
if(dep[x]>dep[y])swap(x,y);
int d=dep[y]-dep[x],j=0;
while(d)
{
if(d&1)y=f[y][j];
j++;d>>=1;
}
if(x==y)return x;
for(int i=20;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
return f[x][0];
}
pair<int,int> fid(int x,int y)
{
bool ff=0;
if(dep[x]>dep[y])swap(x,y),ff=1;
int d=dep[y]-dep[x],j=0;
while(d)
{
if(d&1)y=f[y][j];
j++;d>>=1;
}
for(int i=20;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
x=f[x][i];
y=f[y][i];
}
}
if(ff==0)return {x,y};
else return {y,x};
}
int ispath(int a,int b,int c)
{
int ab=lca(a,b),bc=lca(c,b),ac=lca(a,c);
if(c==ab)return 2;
if(ac==c&&bc==ab||bc==c&&ac==ab)return 1;
return 0;
}
int dis(int x,int y){return dep[x]+dep[y]-2*dep[lca(x,y)];}
signed main()
{
#ifdef Lydic
freopen(".in","r",stdin);
freopen(".out","w",stdout);
#endif
cin>>n>>Q;
for(int i=1;i<n;i++)
{
int x=read(),y=read();
G[x].push_back(y);
G[y].push_back(x);
}
dfs(1,0);
while(Q--)
{
int x=read(),y=read(),z=read(),ans=0;
if(z==x||z==y)
{
if(x==y){printf("%d\n",n);continue;}
if(dep[x]>dep[y])swap(x,y);
if(lca(x,y)==x){//在一条祖先链上
if(z==y) {
printf("%d\n",siz[y]);
}
else {
int ey=y;
for(int k=20;k>=0;k--)
if(dep[f[ey][k]]>dep[x]) ey=f[ey][k];
cerr<<ey<<'\n';
printf("%d\n",n-siz[ey]);
}
}
else {//不是祖先关系
printf("%d\n",siz[z]);
}
continue;
}
if(ispath(x,y,z)==1)
{
// cout<<"S"<<endl;
if(lca(x,z)==z)
{
int ex=x;
for(int k=20;k>=0;k--)
if(dep[f[ex][k]]>dep[z]) ex=f[ex][k];
ans=siz[z]-siz[ex];
}
else
{
int ey=y;
for(int k=20;k>=0;k--)
if(dep[f[ey][k]]>dep[z]) ey=f[ey][k];
ans=siz[z]-siz[ey];
}
printf("%d\n",ans);
continue;
}
else if(ispath(x,y,z)==2)
{
auto xy=fid(x,y);
int fx=xy.first,fy=xy.second;
ans=n-siz[fx]-siz[fy];
printf("%d\n",ans);
}
else puts("0");
}
return 0;
}
Range Deleting
考虑到区间可以任意向外拓展,具有单调性。
所以我们可以钦定一个值为区间左端点,右端点显然可以通过移动指针找到。这样根据题目性质,如果向右移动 \(l\),\(r\) 一定不降,所以我们可以用双指针实现这件事情。
每次对于当前左端点累加答案即可。
Pair of Numbers
我们不妨依次钦定每个元素为答案,找到最长的区间。
这个区间的存在显然有单调性,所以我们此时需要在二分的时候快速 \(check\),由于公约数具有可重复贡献性,所以 \(check\) 可以用 st表实现。
这样我们每次求出当前元素的答案,比较区间大小,用一个 set
存储左端点,每次更新 \(set\) 或者往里面插入新值就可以方便处理出所有要求的东西。
Colorful Points
有个蛋的数据结构。
考虑对每个连续的相同字符集用一个结构体存储长度和颜色,然后每次我们暴力处理每一个块,更新一次操作以后这个块的长度信息,同时用一个新数组统计一次更新后的新块集合。
如果这个块处理好的长度直接 \(le 0\),说明它已经没用了,跳过。否则对于当前处理好的块,判断它与新块集最新元素的颜色是否相同,如果是把它加入集合,否则开一个新的块给它就行了。
由于每个元素最多操作一次,所以复杂度是严格线性的。