题解小合集——第一弹
Posted xsx-blog
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了题解小合集——第一弹相关的知识,希望对你有一定的参考价值。
(转载于我的洛谷博客)
索引:
第一题:P2552 团体操队形
第二题:P3146 248
第三题:P3147 262144
第四题:P1972 花花的项链
第五题:P1484 种树
第六题:P5132 Cozy Glow之拯救小马国
第七题:P1198 最大数
第八题:P2023 维护序列
第九题:P1967 货车运输
第十题:P1313 计算系数
第一题:P2552 团体操队形
题解思路:大模拟,仔细点就好
这道题以一个点需要注意——就是梅花桩队形的奇数排列和偶数排列的规律是略有不同的
奇数排列时,相互交叉排列的两行如果展开的话就是一个 相邻两个数之间有一个空格 的数列;但是偶数排列时则不然,它的展开在行(或列)的交际处会空两格格子,所以这里需要注意一下(这就是我评测记录里那一大堆WA的原因了)
以下是源码:
#include<cstdio>
#include<iostream>
using namespace std;
int t,n,x,y,m,r;
void count1(int &hang,int &lie)//处理横竖全满队形
hang=1;
hang+=m/r;
lie=m%r;
if(lie==0)lie=r,hang--;
// printf("%d %d\n",hang,lie);
void count2(int &hang,int &lie)//处理r为奇数的梅花桩队形
hang=1;
hang+=(m/r)*2;
if(m%r!=0)
if((m%r)>((r+1)/2))hang++;
lie=((m%r)*2-1)%r;
else
hang--;
lie=(r*2-1)%r;
if(lie==0)lie=r;
// printf("%d %d\n",hang,lie);
void count3(int &hang,int &lie)//处理r为偶数的梅花桩队形
hang=1;
hang+=(m/r)*2;
if(m%r!=0)
if((m%r)>(r/2))
hang++;
lie=((m%r)*2)%r;
else lie=((m%r)*2-1)%r;
else
hang--;
lie=(r*2)%r;
if(lie==0)lie=r;
int main()
scanf("%d",&t);
for(int i=1;i<=t;i++)
int hang=0,lie=0;
scanf("%d%d%d%d%d",&n,&x,&y,&r,&m);
if(x==1)
if(y==1)count1(hang,lie);
else count1(lie,hang);
else
if(r&1)
if(y==1)count2(hang,lie);
else count2(lie,hang);
else
if(y==1)count3(hang,lie);
else count3(lie,hang);
printf("%d %d ",hang,lie);
return 0;
第二题:P3146 248
题解思路:区间DP
感觉这道题是被恶意评到绿题的
这是一个炒鸡水十分经典的区间DP,我不会告诉你我只用了五分钟就把它A了,我也不赘述思路了,直接贴代码:
另外,补充说明一下,此题题干不全,应该加一句话:“如果相邻两个数相同,则可以合并为一个数,新数等于原数加一
#include<cstdio>
#include<string>
using namespace std;
int a[250],n,f[250][250],ans;
int main()
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
f[i][i]=a[i];
for(int i=2;i<=n;i++)//枚举区间长度
for(int j=1;j<=n-i+1;j++)//枚举起点
int e=j+i-1;
for(int k=j;k<=e;k++)//枚举断点
if(f[j][k-1]==f[k][e])//如果两个点相等
f[j][e]=max(f[j][e],f[j][k-1]+1),ans=max(ans,f[j][e]);
printf("%d",ans);
return 0;
如果有兴趣的同学可以去做P3147增强难度版
第三题:P3147 262144
题解思路:DP(好吧我也不知道这个叫什么DP)
显然,这题和第二题P3146题面完全一样,但是却并不是P3146的双倍经验题,因为它的数据范围改到了很大,因此,我们不能再使用区间DP的思路了,我们必须另辟蹊径
我们发现,这个题目每次合并的都是相邻且相等的区间,而且原序列的数字大小都不大于40,这说明最大合并出的数字是有上限的,最大是:
1,1,2,3,4,5,6,7,8,9,……,39,40
这样的序列合并出来的最大值是58(别问我为什么,自己去算)
因此,我们可以根据这个特点去构造思路:使用f[i][j]表示能合成出i的 以j为起点的区间 的终点的下标(应该没有辣么绕口)
故,状态转移方程为f[i][j]=f[i-1][f[i-1][j]]
以下是标程:
#include<cstdio>
#include<iostream>
using namespace std;
int f[60][270000],n,a,ans;
int main()
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a);
f[a][i]=i+1;
for(int i=2;i<=58;i++)
for(int j=1;j<=n;j++)
if(!f[i][j])f[i][j]=f[i-1][f[i-1][j]];
if(f[i][j])ans=i;
printf("%d",ans);
return 0;
第四题:P1972 花花的项链
(我是绝对不会告诉你们这个题有双倍经验题SP3267的)
这个题乍一看还以为是前缀和解决,结果
仔细想了想,才发现如果使用前缀和,是无法判断区间里的数字在该区间里是不是只出现了一次,这样就会出现错误
比于这组数据
1,2,1,3
在询问【1,4】区间时,答案是3;
但是在询问【2,4】区间时,由于在此区间里1只出现了一次,所以答案还是3,但前缀和是3-1=2;
于是只能不情愿的使用数据结构了
题解思路:树状数组
由于我懒线段树比较浪费空间,我们用树状数组解决这个题
具体处理是,我们使用树状数组来记录该区间内不同数字的个数,其每个节点表示该位置是否有一个不同的数字
还是用上面那组数据,我们发现,在一个询问区间中,出现过一次的数再出现一次时就相当于没有出现。所以我们在第一次碰到一个数字a[j]时,把他加入到树状数组,然后用v[a[j]]记录下它出现的位置j;如果在该询问区间里有相同数字,就把第二个数加入树状数组,把第一个数在树状数组内的值设为0
但是你可能会问,对于上面那组数据,如果询问[1,4],那么树状数组内的值应为0,1,1,1,那当询问[1,2]时,他不就错了吗?
因此我们就要用一种赖皮的巧妙的方法来避免这种情况。由于这个题是离线的,所以我们对询问区间以右端点为顺序从小到大排序,然后逐次处理。在处理小区间时(比如[1,2]),树状数组并不会把第一次出现的数字设为0,所以我们得到正确答案并记下来;然后在处理较大区间时(比如[1,4]),我们为了节约复杂度,就不让它从头循环了,而是从上一个小区间的右端点处开始循环,这样在循环是确实修改了树状数组内[1,2]的值,但是却不会影响正确答案
好像有点冗长
以下是标程:
#include<cstdio>
#include<algorithm>
using namespace std;
int a[1000010],n,l,r,t,tree[1000010];
int v[1000010],ans[1000010];
struct Query
int l,r,pos;
ask[1000010];
bool cmp(Query x,Query y)
return x.r<y.r;
int lowbit(int x)
return x&(-x);
void add(int x,int y)
for(x;x<=n;x+=lowbit(x))tree[x]+=y;
int query(int x)
int ans=0;
for(x;x;x-=lowbit(x))ans+=tree[x];
return ans;
int main()
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
scanf("%d",&t);
for(int i=1;i<=t;i++)
scanf("%d%d",&ask[i].l,&ask[i].r);
ask[i].pos=i;
sort(ask+1,ask+1+t,cmp);
int next=1;
for(int i=1;i<=t;i++)
for(int j=next;j<=ask[i].r;j++)
if(v[a[j]])add(v[a[j]],-1);
add(j,1);
v[a[j]]=j;
next=ask[i].r+1;
ans[ask[i].pos]=query(ask[i].r)-query(ask[i].l-1);
for(int i=1;i<=t;i++)
printf("%d\n",ans[i]);
return 0;
第五题:P1484 种树
题解思路:堆
又是一道关于堆的灵活运用的题……
题目中的主要思想是如何反悔。我们可以看到,当我们选择a[i]时,如果要反悔,就可以把a[i]的值重新设置为a[i-1]+a[i+1]-a[i],就相当于记录了这样选可以多获得的利润。我们把这些利润和堆中剩下的数比一比,如果利润更大,就选择利润并加到答案里,这样就相当于反悔并重新选择了a[i-1]和a[i+1]
注意,这个堆并不是用来记录答案的!
以下是标程:
#include<queue>
#include<cstdio>
#include<iostream>
using namespace std;
int n,k,lc[500010],rc[500010];//lc记录左坑的序号,rc记录右坑的序号
bool vis[500010];//记录此坑还能不能用
long long a[500010],ans;//记录权值
struct node//坑
int id;//坑的序号
long long w;//坑的权值
bool operator< (node b) const
return w<b.w;
t;//中间变量
priority_queue <node> q;//经典的大根堆
int main()
scanf("%d%d",&n,&k);
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
t.id=i;
t.w=a[i];
lc[i]=i-1;
rc[i]=i+1;
q.push(t);//踢到大根堆里
lc[n+1]=n;//完善左右坑的信息
rc[0]=1;
for(int i=1;i<=k;i++)//开始反悔
while(vis[q.top().id])q.pop();//在大根堆里清理掉不能用的坑
t=q.top();//取出堆顶元素
q.pop();//踢掉
if(t.w<0)break;//如果最大值都小于零了,那还不如不种
ans+=t.w;//加到答案里
int c=t.id;
a[c]=a[lc[c]]+a[rc[c]]-a[c];//反悔并计算这样做的利润
t.w=a[c];
vis[lc[c]]=vis[rc[c]]=1;//更新该节点的左右坑状态,以遍在下一次循环中在堆中踢掉左右坑
lc[c]=lc[lc[c]];//更新左右坑的位置
rc[c]=rc[rc[c]];
lc[rc[c]]=c;//更新更新后的左右坑的左右坑的位置
rc[lc[c]]=c;
q.push(t);//将反悔利润踢进堆
printf("%lld",ans);//输出答案
return 0;
第六题:P5132 Cozy Glow之拯救小马国
题解思路:还能怎么做?暴力!
这个题就是暴力美学的完美体现。看起来这个题的复杂度高得飞起,但是实际上,如果我们把样例的计算式列出来,就会发现根本没有那么麻烦,可以合并的项目太多了,因此这个题,暴力计算就能过。
以下是标程:
#include<cstdio>
#include<iostream>
using namespace std;
int n,a[200010],num;
long long ans;
int main()
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
scanf("%d",&num);
if(i>j)ans+=(long long)num*min(a[i],a[j]);
printf("%lld",ans);
return 0;
第七题:P1198 最大数
题解思路:线段树
这是一道考察线段树的应用能力题
由于它连序列都没给,因此我们也不用建树了,只写查询和修改函数就行
另外,由于题目中说有负数,所以我们在查询时将ans1,ans2这两个记录答案的变量设的很小以应对负数的情况
一下就是高清无码的标程:
#include<cstdio>
#include<iostream>
using namespace std;
const long long inf=-2147483647;
int n,m;
long long cur,mod,num,t[200001*4];
void modify(int u,int s,int k,int l,int r)//u是已经加入的节点数(就是A操作的次数)
//s是加入的数字,k、l、r不解释
if(l==r)t[k]=s;return;
int mid=l+r>>1;
if(mid>=u)modify(u,s,k<<1,l,mid);
if(mid<u)modify(u,s,k<<1|1,mid+1,r);
t[k]=max(t[k<<1],t[k<<1|1])%mod;
long long query(int k,int l,int r,int x,int y)
if(x<=l&&y>=r)return t[k];
int mid=l+r>>1;
long long ans1=inf,ans2=inf;
if(x<=mid)ans1=query(k<<1,l,mid,x,y);
if(y>mid)ans2=query(k<<1|1,mid+1,r,x,y);
return max(ans1,ans2);
int main()
scanf("%d%lld",&m,&mod);
char o[2];
for(int i=1;i<=m;i++)
scanf("%s %lld",o,&num);
// cout<<o<<"ohhh";
if(o[0]=='A')
n++;
// scanf("%lld",&num);
num=(num+cur)%mod;
// printf("test:%lld\n",num);
modify(n,num,1,1,m);//由于一共有m次操作,所以线段树中最多有m个数,因此在调用时让r=m即可
else
// scanf("%lld",&num);
if(num==0)cur=0;//小小的特判,大大的作用
else cur=query(1,1,m,n-num+1,n);
printf("%lld\n",cur);
return 0;
第八题:P2023 维护序列
题解思路:线段树
这显然是一道线段树题目,但是只拿模板改一改是不够的,乘法的加入使得问题变复杂了,必须要仔细的思考
我们可以用两个懒标记,一个记录加上的数,一个记录乘上的数
我曾经尝试着把乘法的懒标记和加法的懒标记分开处理,乘就只乘,加就只加,但是这样不对,因为加法的懒标记在乘法处理后会变大,所以不能简单的分开处理。
具体思路见代码:
最后赘述一句,数据范围错了!只开100000会WA,你要开200000!
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
long long t[800010],a[200010],add1[800010],add2[800010],p;
int n,m;
void build(int k,int l,int r)
if(l==r)t[k]=a[l];return;
int mid=l+r>>1;
build(k<<1,l,mid);
build(k<<1|1,mid+1,r);
t[k]=t[k<<1]+t[k<<1|1];
void pushdown(int k,int l,int r)
if(add1[k]==0&&add2[k]==1)return;
int lc=k<<1,rc=k<<1|1;
if(l!=r)
add2[lc]=add2[lc]*add2[k]%p;
add2[rc]=add2[rc]*add2[k]%p;
add1[lc]=(add1[lc]*add2[k]%p+add1[k])%p;
add1[rc]=(add1[rc]*add2[k]%p+add1[k])%p;
t[k]=(t[k]*add2[k]%p+add1[k]*(r-l+1)%p)%p;
add1[k]=0;add2[k]=1;
long long query(int k,int l,int r,int x,int y)
pushdown(k,l,r);
if(l>=x&&y>=r)return t[k];
int mid=l+r>>1;
long long ans=0;
if(x<=mid)ans+=query(k<<1,l,mid,x,y);
ans=ans%p;
if(y>mid)ans+=query(k<<1|1,mid+1,r,x,y);
return ans%p;
void modify1(int k,int l,int r,int x,int y,int v)
pushdown(k,l,r);
if(l>=x&&y>=r)add1[k]=(add1[k]+v)%p;return;
int mid=l+r>>1;
if(x<=mid)modify1(k<<1,l,mid,x,y,v);
if(y>mid)modify1(k<<1|1,mid+1,r,x,y,v);
pushdown(k<<1,l,mid);
pushdown(k<<1|1,mid+1,r);
t[k]=(t[k<<1]+t[k<<1|1])%p;
void modify2(int k,int l,int r,int x,int y,int v)
pushdown(k,l,r);
if(l>=x&&y>=r)add2[k]=add2[k]*v%p;add1[k]=add1[k]*v%p;return;
int mid=l+r>>1;
if(x<=mid)modify2(k<<1,l,mid,x,y,v);
if(y>mid)modify2(k<<1|1,mid+1,r,x,y,v);
pushdown(k<<1,l,mid);
pushdown(k<<1|1,mid+1,r);
t[k]=(t[k<<1]+t[k<<1|1])%p;
int main()
scanf("%d%lld",&n,&p);
// p=2147483647;
for(int i=1;i<=n;i++)
scanf("%lld",&a[i]);
build(1,1,n);
scanf("%d",&m);
int b,c,d;
long long e;
for(int i=1;i<=5*n;i++)
add2[i]=1;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&b,&c,&d);
if(b==2)
scanf("%lld",&e);
modify1(1,1,n,c,d,e);
else if(b==1)
scanf("%lld",&e);
modify2(1,1,n,c,d,e);
else
printf("%lld\n",query(1,1,n,c,d));
return 0;
第九题:P1967 货车运输
题解思路:最大生成树+LCA
这个题,我一开始看到的时候就想到了floyd,但是一看这坑爹的数据范围,floyd就被pa掉了
但是这么大的数据范围要怎么处理呢?暴力建图肯定不行。那要怎么办呢(为之奈何?)。我们发现,有一些载重很小的路是不会通过的,因此我们就想到了最大生成树。在建完最大生成树后,我们需要找出树上两个点的距离,这时就会用到LCA,因此这个题的思路就是最大生成树+LCA啦!
(LCA详解请看本博客的第十四题)
代码如下:
#include<cstdio>
#include<iostream>
#include<algorithm>
using namespace std;
int fa[10010],head[10010],cnt,n,m,t,num;
int f[10010][21],dep[10010],w[10010][21];
bool vis[10010];
struct Edge
int dis,to,from;
edge[50010];
struct Tree
int nst,dis,to;
tree[100010];
bool cmp(Edge a ,Edge b)
return a.dis>b.dis;
int find(int x)
if(fa[x]==x)return x;
return fa[x]=find(fa[x]);
void build(int a,int b,int c)
edge[++num].from=a;
edge[num].to=b;
edge[num].dis=c;
void add(int a,int b,int c)
tree[++cnt].nst=head[a];
tree[cnt].to=b;
tree[cnt].dis=c;
head[a]=cnt;
void kruskal()
sort(edge+1,edge+m+1,cmp);
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
int u=edge[i].from,v=edge[i].to;
if(find(u)!=find(v))
fa[find(u)]=find(v);
add(u,v,edge[i].dis);
add(v,u,edge[i].dis);
void dfs(int u)
vis[u]=true;
for(int i=head[u];i;i=tree[i].nst)
int v=tree[i].to;
if(vis[v])continue;
dep[v]=dep[u]+1;
f[v][0]=u;
w[v][0]=tree[i].dis;
dfs(v);
return;
int lca(int x,int y)
if(find(x)!=find(y))return -1;
int ans=2147483647;
if(dep[x]>dep[y])swap(x,y);
for(int i=20;i>=0;i--)
if(dep[f[y][i]]>=dep[x])
ans=min(ans,w[y][i]);
y=f[y][i];
if(x==y)return ans;
for(int i=20;i>=0;i--)
if(f[x][i]!=f[y][i])
ans=min(ans,min(w[x][i],w[y][i]));
x=f[x][i];
y=f[y][i];
ans=min(ans,min(w[x][0],w[y][0]));
return ans;
int main()
scanf("%d%d",&n,&m);
int a,b,c;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&a,&b,&c);
build(a,b,c);
kruskal();
// printf("^-^");
for(int i=1;i<=n;i++)
if(!vis[i])
dep[i]=1;
dfs(i);
f[i][0]=i;
w[i][0]=2147483647;
for(int i=1;i<=20;i++)
for(int j=1;j<=n;j++)
f[j][i]=f[f[j][i-1]][i-1];
w[j][i]=min(w[j][i-1],w[f[j][i-1]][i-1]);
scanf("%d",&t);
for(int i=1;i<=t;i++)
scanf("%d%d",&a,&b);
printf("%d\n",lca(a,b));
return 0;
第十题:P1313 计算系数
题解思路:组合数+快速幂
身为一个合格的蒟蒻,我是不会二项式定理的推广式的(就只能手推了)
一看到这个题,我的头就是一大,于是我拿出了草稿纸一算——好吧并不难(建议大家自己算一算,并没有你们想象的那么麻烦)
我们首先发现,在每个式子的展开式里,a的指数总等于x的指数,b的指数总等于y的指数,而且,x^n*y^m的常数系数就是一个指定的组合数c[k][n](详情参见二项式定理)
那么,我们就可以使用杨辉三角预处理出组合数,再用快速幂计算a和b的乘方,最后把他们乘起来就好了
注意,原题中a和b的范围大于10007,所以计算前要将a和b膜一下!!
以下是标程:
#include<cstdio>
#include<iostream>
using namespace std;
int a,b,k,n,m,c[1010][1010],p=10007;
void couple()//计算杨辉三角
c[0][0]=1;
c[1][0]=c[1][1]=1;
for(int i=2;i<=k;i++)
for(int j=0;j<=i;j++)
if(j==0||j==i)c[i][j]=1;continue;
c[i][j]=(c[i-1][j]+c[i-1][j-1])%p;//看到很多人总喜欢每一项都%,其实是不用的,只要把已经%过的数的和或积再%一下就好了,多%就是浪费时间复杂度
long long qpow(int a,int b)//快速幂
long long ret=1%p;
while(b)
if(b&1)ret=ret*a%p;
b>>=1;
a=a*a%p;
return ret;
int main()
scanf("%d%d%d%d%d",&a,&b,&k,&n,&m);
a=a%p;b=b%p;
couple();
long long ans=(c[k][n]*(qpow(a,n)*qpow(b,m))%p)%p;//简单粗暴的输出
printf("%lld",ans);
return 0;
以上是关于题解小合集——第一弹的主要内容,如果未能解决你的问题,请参考以下文章