CSP-S 备战所有题题解(week 1)
Day1
主要复习:线段树、树状数组(?)、分块、主席树
AT_abc339_e [ABC339E] Smooth Subsequence
一道线段树板子题。首先可以写出下面的 DP 代码:
for(int i=1;i<=n;i++)
for(int j=1;j<i;j++)
if(abs(a[i]-a[j])<=d)
dp[i]=max(dp[i],dp[j]+1);
初始化:。因为每个数可以单独成一段。
由于能转移到 的 一定满足 。我们不妨设 表示最后一个选 的最优解。那么就有如下 DP 方程:
显然中间这一段可以用线段树维护。
核心代码:
for(int i=1;i<=N;i++)
{
int num=dp.query(max(1ll,a[i]-d),min(500000ll,a[i]+d))+1;
dp.update(a[i],a[i],num);
ans=max(num,ans);
}
知识点:线段树,DP
AT_abc339_g [ABC339G] Smaller Sum
分块板板题。
看到这道题,就知道这是一道板板分块了。先分成每块长 。分下列几种情况讨论:
- 在一块:暴力!此时时间复杂度 。
- 不在一块:
- 散块:暴力!此时时间复杂度 。
- 中间的块:可以考虑先将每一块排序,二分+前缀和出答案。此时时间复杂度
预处理:要将这 块排序,时间复杂度 。
可以发现,其实重头在查询整块,如果令 ,直接 ,很容易就炸掉了(别问我怎么知道的)。
因为 ,考虑 就能很轻松过掉了。
signed main()
{
int n=read(),tot=0;
int kc=2000;
for(int i=1;i<=n;i++)
{
a[i]=read();
id[i]=(i-1+kc)/kc;
if(id[i]!=id[i-1]) L[i]=i,LLL[++tot]=i;
else L[i]=L[i-1];
}
L[n+1]=n+1;LLL[tot+1]=n+1;
for(int i=1;i<=tot;i++)
{
for(int j=LLL[i];j<LLL[i+1];j++)
w[j]=a[j];
sort(w+LLL[i],w+LLL[i+1]);
for(int j=LLL[i];j<LLL[i+1];j++) sum[j]=sum[j-1]+w[j];
}
int q;
long long lastans=0;
for(cin>>q;q--;)
{
long long l=read(),r=read(),k=read();
l^=lastans,r^=lastans,k^=lastans;
lastans=0;
if(id[l]==id[r])
{
for(int i=l;i<=r;i++) lastans+=(a[i]<=k)*a[i];
write(lastans);puts("");
continue;
}
for(int i=l;id[i]==id[l];i++) lastans+=(a[i]<=k)*a[i];
for(int i=r;id[i]==id[r];i--) lastans+=(a[i]<=k)*a[i];
for(int i=id[l]+1;i<id[r];i++)
{
auto K=upper_bound(w+LLL[i],w+LLL[i+1],k)-w;
lastans+=sum[K-1]-sum[LLL[i]-1];
}
write(lastans);puts("");
}
return 0;
}
知识点:分块(其实也可以主席树)
AT_abc350_f [ABC350F] Transpose
一眼板板题!
先大小写转化、括号匹配,无需多盐。
接着 dfs。显然每次碰到括号就 dfs 一次即可。
void dfs(int l,int r,bool pf)
{
if(pf)
{
for(int i=l;i<=r;i++)
{
if(s[i]=='(')
{
dfs(i+1,dy[i]-1,pf^1);
i=dy[i];
}
else
cout<<s[i];
}
}
else
{
for(int i=r;i>=l;i--)
{
if(s[i]==')')
{
dfs(dy[i]+1,i-1,pf^1);
i=dy[i];
}
else
cout<<s[i];
}
}
}
知识点:搜索,栈。
后记:经过 lihongqian__int128 神犇提醒,我才知道这题正解应该是线段树。对于一次转换,位置 会被转换到 ,其中 是左右括号的坐标。这个操作相当于一个先乘 ,再加上 的操作。线段树维护。
AT_abc350_g [ABC350G] Mediator
看起来是一道很难的题,但是暴力过了。
但思考一下会发现也不太好卡。
- 预处理:预处理 , 表示 是否有边。这样的时间复杂度其实是 度数,接近 。
- 修改: 一次。这个操作估计会执行 次。(这样才能让查询效果最大化)
- 查询: 一次。
发现了一件可悲的事情:时间复杂度接近 。但是暴力跑不满。如果要跑满,可以用记忆化水掉。这样最多跑到 。加上三秒时限,加上 ATC 神机...
Day 2 刷题
P5903 【模板】树上 K 级祖先
顾名思义,求树上 K 级祖先的算法叫做树上 K 级祖先。
考虑每一次查询等价于 往上跳 次,正常时间复杂度是 的。怎么优化这个过程呢?一看就可以用倍增。预处理 的 祖先,二进制拆分即可。
树上 K 级祖先的时间复杂度:预处理 ,每次查询 。
void logg()
{
Log2[0]=Log2[1]=0;
for(int i=2;i<=500000;i++)
Log2[i]=Log2[i/2]+1;
}
void dfs(int x,int father)
{
depth[x]=depth[father]+1;
for(int i=1;i<=Log2[depth[x]];i++)
fa[x][i]=fa[fa[x][i-1]][i-1];//2^i=2^i-1+2^i-1
for(int i=0;i<G[x].size();i++)
if(G[x][i]!=father) dfs(G[x][i],x);
}
int KZX(int x,int k)
{
int f=0;
while(k)
{
if(k%2) x=fa[x][f];
f++,k/=2;
}
return x;
}
P2396 yyy loves Maths VII
备注:记 。lowbit(i) <=> (i&(-i));将 写作 。
话说那个让 yyy 写程序的那个人怎么能算,怎么没见他去算呢。手捏 24!
虽然 做不了,但是 还是绰绰有余的。定义状态 的第 位等于 是,表示使用这张卡片。否则不使用。
那 的距离怎么算呢?显然等于 。
如果 是一个厄运数字,那么就不需要计算了,否则考虑进行 DP。
方程很显然:对于每一位为 的卡片,考虑不拿这张卡片后再拿这张卡片。即 一定可以从 转移过来。也就是:
其中 为位数。
初始化?,(什么都不选也是一种方案)。
void dp(int x)
{
for(int i=x,j;i;i^=j)
j=lowbit(i),(f[x]+=f[x^j])%=mod;
}
Day 2 考试
T1
简述题意:求一个数 ,使得 不是 的倍数, 是 的倍数。
不愧是签到题。容易想到如果 ,则 。验证一下即可。
T2
简述题意:有两种操作,操作 1 使得 加上 ,代价 ;操作 2 使得 减去 ,代价 。对于所有的 ,使 全部相等的最小代价是多少。
容易想到,最后所有 变成的值一定会 的一个值(这个值成为阈值)。再一想,按 排序, 的阈值最多与 的阈值差一位。bound 一下即可。
T3
简述题意:把一个数加上 的代价为 。求把 这 个数进行若干次操作后,使所有数大于等于 的最小代价。,数据随机生成。
定义 表示将一个数加上 的最优解。打表后发现, 有下面性质。
- 是递增的。
- 在 个数后,会以 的长度循环
性质 1 告诉我们, 变为 的最小价值一定是加 ;性质 2 告诉我们,对于前 个数,可以暴力算,后面的数,按循环节来算。
由于数据随机生成,这种算法几乎卡不掉。
T4
简述题意:定义一条路径的价值为 ,其中 是路径上边的数量,求经过 的所有路径价值和。
显然可以先预处理 的子树(y为根)、 的子树(x为根), 到 的路径的价值。相同的加在一起,最后相乘。前两个换根/预处理,最后一个 LCA。
Day 3
CF915E Physical Education Lessons
动态开点板子题。
如果你需要维护一个 的区间,你或许会掏出离散化。但如果它强制在线呢?动态开点!
动态开点的宗旨是:要用什么点就建立什么节点。以前来说, 的两个儿子是 和 ,但现在变成了 。
由于每次操作的时间复杂度是 ,每次操作就可能创建 级别的点。操作 次后,也只会创造 个节点。时间复杂度同样还是 一次。by sLMxf
知识点 get!
CF208E Blood Cousins
首先把这颗树拍平成一个序列。这里的 P 级表祖一定是 的 K 级祖先。所以先求出 的 级祖先。
那么现在的问题就变成了这样:给定一个序列,求区间 中有多少个值等于 。
好像没什么好用的数据结构?那就分块启动!
AT_abc180_f [ABC180F] Unbranched
构式数学题。一个一个条件分析。
- 没有自环:好像只要正经点,这条件其实没什么用。
- 点的度数不超过 :手玩一下就能发现最后这张图只会有一堆链和环构成。
- 连通块最大个数等于 :考虑 DP。因为等于 太难受了,所以考虑统计不超过 ,因为这样就能变成 了,非常爽!
考虑一下怎么 DP。定义状态:
表示选 个点, 条边。
问题是转移方程怎么写?由于每个连通块只能是链或环,考虑一下分类讨论。
在分讨之前,我们先讨论链、环的可能数。
- 链:假设这条链长度为
- 时,显然只有 种方案。
- 时,选第一个链头方案数 ,第二个结点方案数 ,,第 个结点方案数 。但是每条链一定会跟另一条对应的链重合(如 与 ),所以方案数为 。
- 环:假设这个环长度为 。
- 时,显然只有 种方案。
- 时,分析思路同链,但重复的个数为 (如 、、、、 和 ),方案数为 。
定义:
即链与环的方案数函数。
考虑除了枚举 ,再枚举一个 ,分讨 对答案的贡献。其中 相当于代表 那一位。
- 如果这 个结点构成的是链。
这 个节点使用了 条边,其他的 个节点使用了 条边,也就是状态 。选 个节点的方案数为剩下的 个点中选 个点,方案数为 ,贡献为 。所以总的贡献为: - 如果这 个结点构成的是环。 同链,总贡献为:
初始化 ,一个点、一条边都不选是一种方案。
然后这题套个逆元就做完了。
for(int i=1;i<=n;i++)
for(int j=0;j<=m;j++)
{
for(int k=1;k<=min(l,min(i,j+1));k++)
dp[i][j]=(dp[i][j]+dp[i-k][j-k+1]*C(n-i+k-1,k-1)%mod*l(k)%mod)%mod;
for(int k=2;k<=min(l,min(i,j));k++)
dp[i][j]=(dp[i][j]+dp[i-k][j-k]*C(n-i+k-1,k-1)%mod*h(k)%mod)%mod;
}
Day 4 考试
T1 扑克牌
如果能配出 副牌,则一定能配出 副牌;如果不能配出 副牌,则一定不能配出 副牌。所以答案具有单调性。
设答案为 ,考虑答案:
1 2 3 4 ... k ... n -- 1
1 2 3 4 ... k ... n -- 2
J 2 3 4 ... k ... n -- 3
J 2 3 4 ... k ... n -- 4
1 J 3 4 ... k ... n -- 5
1 2 J 4 ... k ... n -- 6
. . . . ... . ... n .. .
. . . . ... . ... n .. .
. . . . ... . ... n .. .
1 2 3 4 ... J ... n -- ans
则第 种牌要用 张 来配。记
则 。所以 且 。贪心+二分即可。
T2 T3
石堆没讲
T4 San
这么小的范围,折半搜索即可。
Day 5 考试
CSP-J 模拟,不属于 CSP-S 范围,不讲了。
Day 5 刷题
CF383C Propagating tree
定义 为节点 的深度。对于深度为奇数的节点,对权值的贡献为正。对于深度为偶数的节点,对权值的贡献为负。
最后统计答案时,乘上自己对权值的贡献即可。
至于操作:拍成 DFS 序,树状数组维护即可。
喵喵喵,没了。
while(m--)
{
int op,x,Val;
cin>>op>>x;
if(op==1)
{
cin>>Val;
add(dfn[x],dep[x]*Val);
add(r[x]+1,-dep[x]*Val);
}
else
{
cout<<dep[x]*sum(dfn[x])+val[x]<<endl;
}
}
Day 6 刷题
CF1439C Greedy Shopping
一道很水的题。
操作 1 水水就过去了。
操作 2 ,我们发现要卡暴力最好的办法是这样的:
但是这样会有一个性质:每一次 至多会变成 。所以这样的时间复杂度是 的。
考虑线段树二分,这样时间复杂度 ,就这样水过去了!!!
int query(int l,int u=1,int L=1,int R=n)
{
if(l<=L&&R<=n)
{
if(a.w[u]<=y)
{
y-=a.query(L,R);
return R-L+1;
}
else if(a.W[u]>y)
{
return 0;
}
}
if(a.lzy[u]) a.pushdown(u,L,R);
int ans=0;
int mid=(L+R)>>1;
if(l<=mid) ans+=query(l,u*2,L,mid);
if(mid+1<=n) ans+=query(l,u*2+1,mid+1,R);
return ans;
}
Week 1 到此结束

浙公网安备 33010602011771号