[关闭]
@xiaoziyao 2020-11-15T19:17:53.000000Z 字数 21231 阅读 1450

CSP提高组45题

其他


2020.10.14

第一天。

题意:个杯子,个回合,每一次等概率将中间的杯子与左/右杯子交换,求中间的杯子在中间的概率,需要约分。

数学题,推柿子+快速幂。

定义为有钥匙的杯子,为没有钥匙的杯子,那么共有三种状态,满足题意的状态为

画出树状图可以知道所有状态都可以变为种状态,且不能变为可以变为

设第次操作的情况数为,那么非的数量为,且只有非有贡献,故

次操作后的答案为,那么

那么有

,那么,且

那么有

然后有

为奇数时,,因为为偶数,所以,所以,所以可以进行一次约分,而且因为不是偶数,所以

为偶数时,,因为为奇数,所以,所以,所以还是可以约分,同样的约分到了最简。

因此答案就是:

可以用费马小定理解决输入时取模问题。

题意很清晰,搜索水题,不解释。

没判墙竟然有分,震惊了。

题意:求生成树中最大边与最小边最小的差值.

水题,把边按权值排序,每一条边往后取,做了,记得判。(加强版为P4234 最小差值生成树,用维护最小生成树配合尺取法做到

题意:给定天的股票价格,每天可以买进一股或卖出一股,也可以什么都不干,求赚的最多钱(可以赊账)。

反悔贪心,考虑这个转化,可以同时使用贪心策略(当前股价减最小股价),和反悔策略(当前股价减之前选择贪心过的最小股价),这样可以用堆维护之前的贪心操作进行反悔保证正确性,复杂度

  1. for(int i=1;i<=n;i++){
  2. int t1,t2;
  3. scanf("%lld",&a[i]);
  4. if(!q.empty()&&a[i]+q.top()>0){
  5. ans+=a[i]+q.top();
  6. q.pop();
  7. q.push(-a[i]);
  8. }
  9. q.push(-a[i]);
  10. }

2020.10.15

第二天。

题意:给一棵树染黑白,每个白点附近恰好有一个黑点,求最小的黑点数。

树形dp,第一眼看成了没有上司的舞会,最后越想越歪,只好看了题解。

为服务器,父亲为服务器的客户端,和父亲为客户端的客户端的最少服务器个数,然后直接dp。

优化一下,然后就能做了。

题意:把一堆硬币分成两堆让两堆硬币差最小。

背包水题,直接把硬币推出一个背包,然后取能组合出的一堆硬币总数最小的

题意:给定个物品,每个物品有两个权值,选定若干个物品,让他们之和等于之和,且让之和最大。

简单背包题,本来可以想出来的,却看了题解,艹

假设我们选的物品是集合,我们有,然后变一下型就是

因此我们考虑每个物品体积为,价值为,然后按照体积的正和负做两个背包,最后合并一下两个背包就好了(注意特判没有选任何物品的情况)。

题意:将一棵树分为若干条链,使链长度的最小值最大,求这个最大的最小值。

这道题就是P5021 赛道修建的改版,首先二分这个长度,设每个点可以向上延伸的长度,然后每个点将它所有儿子的值加一(加上儿子到父亲的边)放在一个里,使用贪心的思想,用双指针不断抵消相加大于最小的长度,然后特判一下就可以了,复杂度,细节有点多。

  1. int ok=1;
  2. while(!s.empty()){
  3. multiset<int>::iterator it=s.lower_bound(lim-(*s.begin()));
  4. if(it==s.begin())
  5. it++;
  6. if(it==s.end()){
  7. if(x==1||ok==0){
  8. flg=1;
  9. break;
  10. }
  11. ok=0,up[x]=(*s.begin());
  12. }
  13. else s.erase(it);
  14. s.erase(s.begin());
  15. }

2020.10.16

第三天。

题意:个蛋糕,个互不相同的区间,每个区间有权值,可以按照一定的顺序取若干个区间,取每个区间会取走里面所有的蛋糕,只有一个区间里有蛋糕才能取这个区间,求能取的区间之和最大值。

区间dp,有点神,只会做法/fad。

为取之和最大值,然后我们有转移

但是我们这种转移不能加入一只牛,我们考虑改进这个转移,其中代表还剩下蛋糕,选择范围在内的区间最大是多少。

然后就可以做到转移了,细节有亿点多。

  1. for(int i=1;i<=m;i++){
  2. int w,l,r;
  3. scanf("%d%d%d",&w,&l,&r);
  4. for(int j=l;j<=r;j++)
  5. maxx[l][r][j]=max(maxx[l][r][j],w);
  6. }
  7. for(int k=1;k<=n;k++)
  8. for(int i=k;i>=1;i--)
  9. for(int j=k;j<=n;j++){
  10. if(i>1)
  11. maxx[i-1][j][k]=max(maxx[i-1][j][k],maxx[i][j][k]);
  12. if(j<n)
  13. maxx[i][j+1][k]=max(maxx[i][j+1][k],maxx[i][j][k]);
  14. }
  15. for(int i=1;i<=n;i++)
  16. f[i][i]=maxx[i][i][i];
  17. for(int len=2;len<=n;len++)
  18. for(int i=1;i+len-1<=n;i++){
  19. int j=i+len-1;
  20. for(int k=i;k<=j;k++)
  21. f[i][j]=max(f[i][j],f[i][k-1]+maxx[i][j][k]+f[k+1][j]);
  22. }

题意:给定一棵以为根的带点权的树,对于每个点,计算它子树中值比它大的点数。

统计题,本来想线段树合并,却发现题解有更简单的做法。

因为一颗子树的访问一定是连续的,因此进时到出时一定包括且只包括子树中所有的贡献,这样我们减去不属于子树内的贡献(即在访问之前的贡献)就好了。

  1. void dfs(int x,int last){
  2. ans[x]-=(query(tot)-query(val[x]));
  3. for(int i=start[x];i;i=then[i]){
  4. int y=to[i];
  5. if(y==last)
  6. continue;
  7. dfs(y,x);
  8. }
  9. ans[x]+=(query(tot)-query(val[x]));
  10. update(val[x],1);
  11. }

题意:给定一个颜色矩阵,求里面是否有相同颜色的,长度大于等于的简单回路(即不经过相同结点的回路)。

简单搜索题,对于每个点搜索一遍与它颜色相同的其他点,如果搜回来了就判断长度是否大于等于,因为数据范围很小,所以很容易过。


2020.10.17

第四天。

题意:求连续矩阵(可跨越上下左右边界)的最大权值和。

这里举个例子,可以这样选:

  1. × × ×
  2. × × ×
  3. × × ×
  4. × × × × ×
  5. × × × × ×
  1. ×
  2. ×
  3. × × × × ×
  4. ×
  5. ×

首先有一个简化版UVA108 Maximum Sum

先做一遍前缀和,然后枚举所有的矩阵求和最大值。

这道题,我们只需要把矩阵复制份做一遍上面的操作就可以了,记得约束矩阵大小(宽度与高度不能大于),复杂度,有一个的大常数,但是可以卡过去。

当然,上面那道简化版有做法:设代表第行第个数一直往上扩展,最大的矩阵权值和是多少,然后转移也很方便:(设为第行第到第个数的和)。

但这道题要约束矩阵大小,因此不能适用上述做法。

其实这道题也有的算法,但是并不想写。

题意:给定一个长度为的序列,求有多少个子序列的异或和大于等于,序列的异或和为序列中所有数异或起来的结果。

自己想出了做法,鸡冻。

首先根据异或的性质,求出异或的前缀和,把异或和转化为有多少个数对满足异或起来大于等于(其中数对需要在异或前缀和数组中)。

然后我们对异或前缀和数组建一颗,并求出每个点子树中终止点的个数(终止点就是插入时最后一个点),查询的时候按位查询。

具体地,我们对于每一位分类讨论,设第,第),假如我们前位与都相同:

  1. long long calc(int x,int k){
  2. int now=root;
  3. long long res=0;
  4. for(int i=30;i>=0;i--){
  5. int y=((x>>i)&1),p=((k>>i)&1);
  6. if(p==0)
  7. res+=1ll*size[chd[now][y^1]];
  8. now=chd[now][y^p];
  9. }
  10. res+=size[now];
  11. return res;
  12. }

但是这样并不行,因为我们提前把建出来了,这样会两个异或起来不能成为一个区间的数的贡献加进答案。

可以边修改边查询,把直接改成经过的所有点都增加,在每一次查询前插入上一个前缀和,这样我们可以保证查询的所有贡献都是有效的了。

时间复杂度

题意:有一种生物只有一天的寿命,一天后有的几率分裂成个相同的生物(),求天后个这样的生物死亡的几率(不到天死亡也可以)。

设一个生物前天死亡的几率为,那么由乘法原理可得答案为

然后考虑如何求,考虑一开始一个生物,然后分裂出了个生物,那么由加法原理得

直接递推即可。

题意:给定一张无向连通图,求起点到终点长度为的最短路。

矩阵快速幂,有亿点细节。

矩阵快速幂解决图上问题是老生常谈了(不会可以点进去看一看),这道题,我们重定义矩阵乘法:若,则有

很容易证明这个矩阵乘法的结合律,即

然后可以直接套上矩阵快速幂的板子。

注意:输入边时顺序是权值,起始点,终止点;在离散化点的标号后记得离散化起点和终点;邻接矩阵不能有,而是

题意:求区间内数字和能被整除且数字可被整除的数数量。

简单数位dp,手打一遍过。

记忆化搜索的时候记几个参数,当前处理的数位长度,当前数位和模,当前数字模,是否卡位(即前面的数位是否与上限的数位完全契合)。

然后就很容易搜索了,注意最大值不会超过倍数位,因此数组可以开小一点,避免MLE

  1. int dfs(int len,int cnt,int sum,int flg,int k){
  2. if(len==0)
  3. return (cnt==0&&sum==0);
  4. if(vis[len][cnt][sum][flg]==stp)//时间戳标记
  5. return rec[len][cnt][sum][flg];
  6. int res=0;
  7. for(int i=0;i<=9;i++)
  8. if(flg==0||i<=num[len])
  9. res+=dfs(len-1,(cnt+i)%k,(sum*10+i)%k,i==num[len]&&flg,k);
  10. vis[len][cnt][sum][flg]=stp;
  11. return rec[len][cnt][sum][flg]=res;
  12. }

题意:给定一个有向图,求经过,允许最多反向走一次边的最长回路。(路径的长度为路径上点的个数)

毒瘤细节题。

我们首先做一遍,然后缩个点,建一个新图。

对于每个点用拓扑排序求出它到所在的连通块和所在的连通块到它的最长路径,然后枚举每一条路径来计算这一条路径走反向的答案。

复杂度:

  1. ans=size[bel[1]];
  2. for(int i=1;i<=m;i++){
  3. int x=bel[xx[i]],y=bel[yy[i]];
  4. if(x==y)
  5. continue;
  6. if(ok[x][0]&&ok[y][1])
  7. ans=max(ans,dis[x][0]+dis[y][1]-size[bel[1]]);
  8. if(ok[y][0]&&ok[x][1])
  9. ans=max(ans,dis[y][0]+dis[x][1]-size[bel[1]]);
  10. }

2020.10.18

第五天。

题意:

考虑定义一条有向边的意义为把公主让给了,那么每个点一定入度为所有的边会形成一个外向基环树森林

贪心地把公主按照嫁妆从大到小排序,每个公主看成一条无向边连接两个王子,那么可行的方案一定会形成一个基环树森林

用并查集维护所有王子组成的基环树,用标记来记录一个并查集代表的集合为基环树还是树,然后考虑选择一条边的方法:

  1. for(int i=1;i<=m;i++){
  2. int x=find(g[i].x),y=find(g[i].y);
  3. if(x==y){
  4. if(flg[x]==0)
  5. flg[x]=1,ans+=g[i].z;
  6. continue;
  7. }
  8. if(flg[x]+flg[y]<=1){
  9. f[y]=x,flg[x]=flg[x]+flg[y];
  10. ans+=g[i].z;
  11. }
  12. }

题意:

考虑将每个社团抽象为一个点,每个能力值抽象为一个点,社团与所有它成员的能力值连边,那么我们可以用匈牙利算法来解决这个:每一次多取一个能力值,如果可以那么加一,否则当前就是最终答案。

可以不断删边(标记每一条边删掉就好了),然后用匈牙利做到,但是仍然无法通过。

考虑将删边转化为加边(时间倒流),然后不难发现是递增的

因为是递增的,所以每一次匈牙利都可以从上一次结束的位置开始,这样我们的复杂度就优化成了

  1. for(int i=d;i>=1;i--){
  2. for(int j=ans;;j++){
  3. stp++;
  4. if(dfs(j)==0){
  5. ans=j;
  6. break;
  7. }
  8. }
  9. res[i]=ans;
  10. add(p[k[i]],c[k[i]]);
  11. }

题意:给定,求个数的排列中前个数恰好有不是错排的方案数。

题意:给定棵树,高度为,权值为,当一颗树的时就说它被砍倒了。有一个电锯,每一次砍伐树会让一棵树的高度,砍伐的费用为当前砍倒的编号的最大的树的,第一次砍伐不需要费用。保证,求砍倒所有树的最小费用。

很显然,我们第一次砍的一定是第一棵树,且砍倒最后一棵树以后就不需要任何费用了,这样,题意就转化为如何花费最小的费用砍倒第棵树。

有一个很显然的贪心思想,我们砍树一定从前往后砍(中间可以跳过一些树),因为我们砍这棵树一定无法更新砍树的费用。

为砍倒第棵树的费用,那么可以列出转移方程

这个一定无法通过题目,因此需要优化。

考虑斜率优化:设两个的决策点满足,且更优,那么有:

变一下形式:

化成斜率式之后,由于是递增的,因此用单调队列维护一个上凸壳就好了。

时间复杂度:

题意:一棵个节点带点权和边权的树,定义树上距离的边权和,定义控制当且仅当子树中且,求每个点能控制多少个点。

很经典的一道题了。

首先把每个点控制的点数转化为每个点能对多少个点产生贡献,然后因为边权非负,我们可以预处理一下边权的前缀和,然后倍增跳祖先到最远可以控制的点就可以了。

用树上差分统计答案就可以了。

  1. void dfs(int x){
  2. for(int i=1;i<=20;i++)
  3. fore[x][i]=fore[fore[x][i-1]][i-1];
  4. for(int i=start[x];i;i=then[i]){
  5. int y=to[i];
  6. dis[y]=dis[x]+worth[i],fore[y][0]=x;
  7. dfs(y);
  8. }
  9. int y=x;
  10. ans[fore[x][0]]++;
  11. for(int i=20;i>=0;i--)
  12. if(dis[x]-dis[fore[y][i]]<=a[x])
  13. y=fore[y][i];
  14. ans[fore[y][0]]--;
  15. }

题意:给定一棵个点的基环树,每个点有点权。两个相邻的点不能同时选中,选择若干点求最大点权和乘实数的值。

基环树基础练习题。

用一个很显然的基环树套路,切断环上的一条边,对于这一条边的左右端点各做一个P1352 没有上司的舞会,然后求最大值就可以了。

题意:给定长度为的数列,选择个元素,使连续的个元素至少有一个被选中,使权值和最大。

为前个数选择了个数,且满足条件的最大权值和。

很显然有一个转移:,用单调队列可以加速到

  1. f[0][0]=0;
  2. for(int i=1;i<=m;i++){
  3. l=1,r=0,q[++r]=0;
  4. for(int j=1;j<=n;j++){
  5. while(l<=r&&j-q[l]>k)
  6. l++;
  7. f[i][j]=f[i-1][q[l]]+a[j];
  8. while(l<=r&&f[i-1][j]>=f[i-1][q[r]])
  9. r--;
  10. q[++r]=j;
  11. }
  12. }
  13. for(int i=n-k+1;i<=n;i++)
  14. ans=max(ans,f[m][i]);

2020.10.19

第六天。

题意:给定一个由个节点,个管道组成的无向图,每条管道有两个权值代表最大流量,代表使用该管道需要的费用,每一条路径上的流量为这条路径上的最小,求如何使用管道使流量与费用之比最小。

数据范围:

可以枚举最大流量,然后用把所有流量限制小于的边去掉,把花费变成边权作一遍,得到的花费就是最小的了,最后在所有答案中取一个就好了。

题意:给定一个个元素组成的数组,求所有子数组最大值减去最小值之和。

单调栈板子题,用单调栈维护每个点要成为最大值/最小值,最多可以向左/向右扩展多远,然后算一下贡献就可以了。

代码以求最大值为例:

  1. top=0;
  2. for(int i=1;i<=n;i++){
  3. while(top>0&&a[i]>=a[stk[top]])
  4. maxqr[stk[top]]=i-1,top--;
  5. maxql[i]=stk[top]+1,stk[++top]=i;
  6. }
  7. while(top>0)
  8. maxqr[stk[top]]=n,top--;

题意:个区间,每个区间为,里面至少要取个数,求满足条件的情况下最少要取多少个数。

差分约束裸题,可以用实现。

  1. for(int i=1;i<=m;i++){
  2. int x,y,z;
  3. scanf("%d%d%d",&x,&y,&z);
  4. add(y,x-1,-z);
  5. }
  6. for(int i=1;i<=n;i++)
  7. add(i-1,i,1),add(i,i-1,0);
  8. if(Bellman_Ford()==0)
  9. printf("%d\n",dis[n]-dis[0]);

2020.10.20

第七天。

题意:给定一个字符串和一个字符串,每次可以删掉的第一个字符,然后放到一个初始为空的字符串 的首部或尾部,求有多少种不同的方法使得最后的前缀。

区间dp,设为区间有多少种方法用拼出,边界为(可以放空串的前面或空串的后面)。

转移很显然,对于长度为的区间

记得对所有超越边界的转移进行特判。

  1. for(int i=1;i<=n;i++)
  2. if(a[1]==b[i]||i>m)
  3. f[i][i]=2;
  4. for(int len=2;len<=n;len++)
  5. for(int i=1;i+len-1<=n;i++){
  6. int j=i+len-1;
  7. if(a[len]==b[i]||i>m)
  8. f[i][j]=(f[i][j]+f[i+1][j])%mod;
  9. if(a[len]==b[j]||j>m)
  10. f[i][j]=(f[i][j]+f[i][j-1])%mod;
  11. }

题意:给定一个长度为的密码,且其由前个小写字母组成。在输入密码的过程中,你的手指需要在密码相邻的两个字母在键盘上的位置移动,缓慢度为这两个位置的距离。对于一个键盘,输入密码的缓慢度为输入密码的缓慢度之和,请设计一个键盘,让缓慢度最小。

数据范围:

状压dp。

看到这么小的,我们就可以大概猜出要压这个,于是套路的设计状态:为设计的键盘(不需要设计完)用到的所有字母压缩起来是的最小缓慢度。

但是有一个问题,对于这个状态,我们不能知道它字母的顺序,所以并不能统计答案。

我们要设计一种无后效性的dp:每一次装一个字母就直接统计出来它的所有贡献(费用提前思想)。

考虑装一个字母的贡献:会让所有相邻且一个装了一个没装的字母对距离加一,这样我们就可以快速地统计出来答案了。

相邻的状态数,很容易统计出来,然后就是套路的状压dp了。

  1. for(int i=1;i<n;i++)
  2. cnt[s[i]-96][s[i+1]-96]++,cnt[s[i+1]-96][s[i]-96]++;
  3. st=(1<<m)-1,f[0]=0;
  4. for(int i=1;i<=st;i++){
  5. int sum=0;
  6. f[i]=inf;
  7. for(int j=1;j<=m;j++)
  8. for(int k=j+1;k<=m;k++)
  9. if(((i>>(j-1))&1)^((i>>(k-1))&1))
  10. sum+=cnt[j][k];
  11. for(int j=1;j<=m;j++)
  12. if((i>>(j-1)&1))
  13. f[i]=min(f[i],f[i^(1<<(j-1))]+sum);
  14. }

题意:有个珠子,第个珠子颜色是,每次操作把相邻的两个珠子交换。现在要把相同颜色的珠子排列在相连的一段,问至少要多少次操作。

数据范围:

考虑冒泡排序的过程,我们很显然知道操作数量为逆序对数量。但是这里是要让序列连续,所以需要给每个颜色标一个号。

不能单纯的进行标号,也不能对标号进行状压,这是的。

如果我们每一次标号都标一个更大的数的话,那么我们就可以设出一个没有后效性的状态表示状态为的集合逆序对数量的最小值,这里集合中二进制为的位置标示已经进行标号的颜色,的位置则相反。

转移很套路,枚举每一种颜色进行标号就好了。但是暴力计算的的复杂度并不行,需要优化。

为所有数对满足的数量,可以进行的统计。

复杂度为

  1. for(int i=1;i<=n;i++){
  2. for(int j=1;j<=m;j++)
  3. cnt[j][a[i]]+=now[j];
  4. now[a[i]]++;
  5. }
  6. t=(1<<m)-1,f[0]=0;
  7. for(int i=1;i<=t;i++){
  8. f[i]=inf;
  9. for(int j=1;j<=m;j++){
  10. if((i>>(j-1))&1){
  11. int sum=0;
  12. for(int k=1;k<=m;k++)
  13. if((j!=k&&(i>>(k-1))&1))
  14. sum+=cnt[k][j];
  15. f[i]=min(f[i],f[i^(1<<(j-1))]+sum);
  16. }
  17. }
  18. }

题意:给定一棵节点数为的树,删一条边然后加上一条边,使得该树的重心唯一。(删掉的边和加上的边可以是同一条)

这道题以前做的时候还是黄题,突然就蓝了。

如果给定的树只有一个重心,那么随便删了重加就好了。

如果有两个重心,由常识可得树的两个重心相邻,且两颗重心的子树大小相同。那么可以随便删某一个重心的一个叶子结点,接在另一个重心的子树上就好了。

  1. void find(int x,int last){
  2. for(int i=start[x];i;i=then[i]){
  3. int y=to[i];
  4. if(y==last)
  5. continue;
  6. find(y,x);
  7. if(flg)
  8. return ;
  9. }
  10. flg=1;
  11. printf("%d %d\n",x,last);
  12. if(p2==0)
  13. printf("%d %d\n",x,last);
  14. else printf("%d %d\n",x,p2);
  15. }
  16. find(p1,p2);//p1,p2为重心

题意:求最小路径点覆盖。

一个定理:最小路径点覆盖等于点数减去最大匹配(记得拆点,成为二分图形式)。

然后匈牙利就好了。

简单证一下吧:将一个点拆成入点和出点,求最大匹配。那么每个入点最大度数为,出点最大度数为,若将每个点的入点和出点连一条边,入点出点形成的一定是若干条路径。而如果这些路径不是最小路径点覆盖的话,那么两个可以合并的路径之间的边一定会对二分图的最大匹配进行一次贡献,因此这些路径一定是最小路径点覆盖。

加强版:P2764 最小路径覆盖问题

输出路径可以直接用跳。

  1. for(int i=1;i<=m;i++){
  2. int x,y;
  3. scanf("%d%d",&x,&y);
  4. add(x,y);
  5. }
  6. for(int i=1;i<=n;i++){
  7. stp++;
  8. ans+=dfs(i);
  9. }
  10. for(int i=1;i<=n;i++)
  11. lst[i]=match[i],nxt[match[i]]=i;
  12. for(int i=1;i<=n;i++)
  13. if(lst[i]==0){
  14. for(int j=i;j!=0;j=nxt[j])
  15. printf("%d ",j);
  16. puts("");
  17. }
  18. printf("%d\n",n-ans);

题意:给定一个长度为的序列,然后再给一个数字,再给出组询问,每组询问给出一个区间,求这个区间里面有多少个子区间的异或值为

数据范围:

莫队水题。

根据莫队的性质很显然可以转化题意为给定区间,求区间内多少个数对异或起来为(其中数对为异或前缀和)。然后做一次莫队,莫队的加入和删除都可以直接暴力做,因为值域非常小。

  1. //b为异或前缀和
  2. void add(int x){
  3. now+=cnt[k^b[x]];
  4. cnt[b[x]]++;
  5. }
  6. void del(int x){
  7. cnt[b[x]]--;
  8. now-=cnt[k^b[x]];
  9. }

题意:给定长度为的序列,一开始全为颜色,一共有种颜色。需要支持次操作,一共两种:区间覆盖颜色,区间查询颜色数量。

数据范围:

线段树简单题。

首先上一个线段树,然后这个操作有几个思路:

这里采取第三种写法,仅给出部分:

  1. bitset<maxk>val[maxn<<2];
  2. inline void pushup(int now){
  3. val[now]=val[lc[now]]|val[rc[now]];
  4. }
  5. inline void getlazy(int now,int v){
  6. val[now].reset();
  7. val[now][v]=1;
  8. lazy[now]=v;
  9. }
  10. inline void pushdown(int now){
  11. if(lazy[now]==0)
  12. return ;
  13. getlazy(lc[now],lazy[now]),getlazy(rc[now],lazy[now]);
  14. lazy[now]=0;
  15. }

2020.10.21

第八天。

题意:给定一个字符串,一共种字符,求它长度为的字符个数。

将每个串看成一个进制的数,然后暴力查答案就好了。

  1. mod=1;
  2. for(int i=1;i<=m;i++)
  3. mod*=cnt;
  4. now=1;
  5. for(int i=1;i<m;i++)
  6. now=now*cnt+a[i];
  7. for(int i=m;i<=n;i++){
  8. now=(now*cnt+a[i])%mod;
  9. if(hash[now]==0)
  10. hash[now]=1,ans++;
  11. }

题意:农夫约翰有个农场,每个农场有块地,其间有条双向路(需要耗费时间),条时光隧道(会时光倒流时间)。问是否可能回到过去?

数据范围:

直接按照题意建边,然后时光隧道赋负权值,道路赋正权值,跑一个判负环。

题意:求直径的长度与直径的数量。

我们枚举每个点,如果它作为中间点,那么直径应该是经过它的最长链和次长链拼起来,然后树形dp就好了。

记得有多组数据。

  1. void dfs(int x,int last){
  2. flen[x]=0,flens[x]=1;
  3. for(int i=start[x];i;i=then[i]){
  4. int y=to[i];
  5. if(y==last)
  6. continue;
  7. dfs(y,x);
  8. int val=flen[y]+worth[i];
  9. if(flen[x]+val==ans)
  10. anss+=1ll*flens[x]*flens[y];
  11. if(flen[x]+val>ans)
  12. ans=flen[x]+val,anss=1ll*flens[x]*flens[y];
  13. if(val==flen[x])
  14. flens[x]+=flens[y];
  15. if(val>flen[x])
  16. flen[x]=val,flens[x]=flens[y];
  17. }
  18. }

题意:列,有一个木块在上面移动,有些格子不能放,有些格子不能竖着放,求到终点的最小步数。



毒瘤搜索,写了很久,但是不难调。

对于种状态(立着,横着躺,竖着躺)每种做一个不一样的转移,然后直接就好了。

这个转移有点难算,建议手动模拟一下就知道怎么转移了。

  1. int dx[13]={0,1,0,0,-2,-1,1,0,0,-1,2,0,0};
  2. int dy[13]={0,0,1,-2,0,0,0,-1,2,0,0,-1,1};
  3. int ds[13]={0,2,1,1,2,1,1,0,0,0,0,2,2};
  4. struct status{
  5. int x,y,s,d;
  6. }tmp;
  7. queue<status>q;
  8. //s=0立着,s=1横着躺,s=2竖着躺(注意,横着躺默认位置为左边,竖着躺默认位置为上面)
  9. int bfs(){
  10. while(!q.empty())
  11. q.pop();
  12. tmp.x=sx,tmp.y=sy,tmp.s=ss,tmp.d=0,vis[sx][sy][ss]=0;
  13. q.push(tmp);
  14. while(!q.empty()){
  15. status now=q.front();
  16. int x=now.x,y=now.y,s=now.s,d=now.d,is=s==0? 1:(s==1? 5:9),ie=s==0? 4:(s==1? 8:12);
  17. q.pop();
  18. for(int i=is;i<=ie;i++){
  19. int ux=x+dx[i],uy=y+dy[i],us=ds[i];
  20. if(vis[ux][uy][us]<=d+1)
  21. continue;
  22. if(ux<1||ux>n||uy<1||uy>m||(us==1&&uy+1>m)||(us==2&&ux+1>n))
  23. continue;
  24. if(a[ux][uy]==-1||(us==0&&a[ux][uy]==0)||(us==1&&a[ux][uy+1]==-1)||(us==2&&a[ux+1][uy]==-1))
  25. continue;
  26. if(us==0&&a[ux][uy]==2)
  27. return d+1;
  28. if(vis[ux][uy][us]<=d+1)
  29. continue;
  30. vis[ux][uy][us]=d+1;
  31. tmp.x=ux,tmp.y=uy,tmp.s=us,tmp.d=d+1;
  32. q.push(tmp);
  33. }
  34. }
  35. return -1;
  36. }

题意:给出个平行于轴的栅栏,求从第个栅栏的某个位置出发,绕过所有栅栏到达另一侧位置(即轴上)的最短水平距离。

数据范围:

很显然有一个dp方式:

为到达栅栏的左边/右边的最小距离。对于一个端点,考虑转移过来的一定是它向上走碰到的第一个栅栏,这样就可以有的转移。

考虑线段树优化一下,我们完成每一个栅栏后就将这个栅栏对应的区间在线段树上覆盖一遍,覆盖的值为这个栅栏的编号,查询的时候直接查询这一个位置是什么编号,然后直接转移。

注意有负下标,需要给每一个位置加上一个大数变成正数。

  1. memset(f,0x3f,sizeof(f));
  2. build(1,maxx+base,1);
  3. for(i=1;i<=n;i++){
  4. int flg0=query(1,maxx+base,1,l[i]+base),flg1=query(1,maxx+base,1,r[i]+base);
  5. if(flg0==0)
  6. f[i][0]=abs(l[i]);
  7. else f[i][0]=min(f[i][0],min(f[flg0][0]+abs(l[i]-l[flg0]),f[flg0][1]+abs(l[i]-r[flg0])));
  8. if(flg1==0)
  9. f[i][1]=abs(r[i]);
  10. else f[i][1]=min(f[i][1],min(f[flg1][0]+abs(r[i]-l[flg1]),f[flg1][1]+abs(r[i]-r[flg1])));
  11. update(1,maxx+base,1,l[i]+base,r[i]+base,i);
  12. }
  13. printf("%lld\n",min(f[n][0]+abs(l[n]-s),f[n][1]+abs(r[n]-s)));

2020.10.22

第九天。

题意:个数字,需要进行若干次区间剪切和粘贴操作来将它们进行排序,求最小操作数。

直接写个IDA*,这里的估价函数为设的是有多少个数字的后继是正确的,然后不难发现一次操作最多让估价函数改变,就可以进行一次比较强的剪枝。

  1. void work(int x,int y,int z){
  2. int b[maxn],cnt=0;
  3. for(int i=y+1;i<=z;i++)
  4. b[++cnt]=a[i];
  5. for(int i=x;i<=y;i++)
  6. b[++cnt]=a[i];
  7. for(int i=x;i<=z;i++)
  8. a[i]=b[i-x+1];
  9. }
  10. void revwork(int x,int y,int z){
  11. int b[maxn],cnt=0;
  12. for(int i=x+(z-y);i<=z;i++)
  13. b[++cnt]=a[i];
  14. for(int i=x;i<=x+(z-y)-1;i++)
  15. b[++cnt]=a[i];
  16. for(int i=x;i<=z;i++)
  17. a[i]=b[i-x+1];
  18. }
  19. int ida_star(int now,int goal){
  20. int val=check();
  21. if(val==0)
  22. return 1;
  23. if(now==goal)
  24. return 0;
  25. if((goal-now+1)*3<val)
  26. return 0;
  27. for(int i=1;i<=n;i++)
  28. for(int j=i;j<=n;j++)
  29. for(int k=j+1;k<=n;k++){
  30. work(i,j,k);
  31. if(ida_star(now+1,goal))
  32. return 1;
  33. revwork(i,j,k);
  34. }
  35. return 0;
  36. }

题意:一个的矩阵,每一行和每一列必须选定一个方向,只能沿着这个方向走。给定对起点与终点,询问是否有一种方向选定方案使得每一对起点走到终点最多只需要拐一次弯。

数据范围:

神仙题。

对于每一行,套路地拆点:表示向左,表示向右;同样的对于每一列表示向上,表示向下。

然后,对于每一对起点终点,首先讨论不需要拐弯的特殊情况,即起点,终点处于同一行或同一列,这样我们直接强制确定某一行/列的方向就好了。

关于如何强制确定一个命题:将这个命题的反命题向这个命题连边,那么就一定无法选定这个命题的反命题了。

关于要拐弯的情况,以起点终点为例,我们需要保证以下命题成立:

意思就是第四行向左且第二列向上,或第四列向上且第一行向左。

这样我们就要想办法建出来图来满足这种形式的命题

有一个逻辑恒等式可以套上去:

(读者自证不难)

这样,我们就可以按照上述式子建图了。

注意要对于的大小关系分类讨论。

时间复杂度:

具体见:题解 UVA10319 【Manhattan】

题意:给定一个的矩阵,用中的数填充。要使每行每列最小值都为,问有多少种写法(对取模)。

数据范围:

容斥又不会做,就是反演这种东西,才能维持的了生活。

首先,因为要使每行每列最小值为,所以每一行都要有

正难则反,我们不能很快地计算列每一行列都有,可以尝试设表示矩阵中恰好列全部没有表示矩阵中有列全部没有

很容易计算,设,则有

根据的定义,很显然有:

对于上面的式子,我们可以使用二维二项式反演来得到另一个式子:

最后,我们的答案就是了。

具体地,我们可以首先预处理逆元,然后数组,然后通过带入上式得到,直接计算就好了。

复杂度:

具体看题解 CF1228E 【Another Filling the Grid】


2020.10.23

第十天。

题意:个大臣排成一列,国王站在最前面,每个人手上有两个数,每个大臣获得的金币为他前面所有人左手上的数的乘积除以他右手上的数,需要让金币最多的大臣金币最少。

贪心毒瘤题。

有一个贪心策略:按照左手上的数乘右手上的数进行从小到大排序,证明就不写了。(提示:可以交换两个相邻的人,观察答案的变化)

记得写高精

题意:对于一个长度为的序列进行 次操作,每次操作都是交换序列中的某两个数。对于每一个操作,回答当前序列中有多少个逆序对。

分块毒瘤题。

对于每一个块用维护它排序后的序列,对于每一次操作,暴力查询这两个数对答案的所有贡献并删去,然后在原序列修改并暴力重构数字所在块,并最后加上这两个数对答案新的贡献。

记得将这两个数之间的贡献也算进去。

  1. int BF(int x,int y,int v,int t){
  2. int res=0;
  3. for(int i=x;i<=y;i++)
  4. if((t==0&&a[i]<v)||(t==1&&a[i]>v))
  5. res++;
  6. return res;
  7. }
  8. int query(int x,int y,int v,int t){
  9. if(x>y)
  10. return 0;
  11. int p=pos[x],q=pos[y];
  12. if(p==q)
  13. return BF(x,y,v,t);
  14. int res=BF(x,r[p],v,t)+BF(l[q],y,v,t);
  15. for(int i=p+1;i<=q-1;i++){
  16. if(t==0)
  17. res+=(lower_bound(b[i].begin(),b[i].end(),v)-b[i].begin());
  18. else res+=(b[i].size()-(upper_bound(b[i].begin(),b[i].end(),v)-b[i].begin()));
  19. }
  20. return res;
  21. }
  22. ans-=(long long)(query(x+1,n,a[x],0)+query(1,x-1,a[x],1));
  23. ans-=(long long)(query(y+1,n,a[y],0)+query(1,y-1,a[y],1));
  24. if(a[x]>a[y])
  25. ans++;
  26. update(x,y);
  27. ans+=(long long)(query(x+1,n,a[x],0)+query(1,x-1,a[x],1));
  28. ans+=(long long)(query(y+1,n,a[y],0)+query(1,y-1,a[y],1));
  29. if(a[x]>a[y])
  30. ans--;

实际上修改操作可以直接用的自带操作做到

  1. void update(int x,int y){
  2. int p=pos[x],q=pos[y];
  3. b[p].erase(lower_bound(b[p].begin(),b[p].end(),a[x]));
  4. b[q].erase(lower_bound(b[q].begin(),b[q].end(),a[y]));
  5. b[p].insert(lower_bound(b[p].begin(),b[p].end(),a[y]),a[y]);
  6. b[q].insert(lower_bound(b[q].begin(),b[q].end(),a[x]),a[x]);
  7. swap(a[x],a[y]);
  8. }

时间复杂度:


最终成果:十天刷完CSP45题。


添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注