题解小合集——第一弹

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;

以上是关于题解小合集——第一弹的主要内容,如果未能解决你的问题,请参考以下文章

C语言小游戏第一弹~关机搞怪小程序

北漂生活第十八弹-日子还照旧

北漂生活第十八弹-日子还照旧

Maya学习笔记第一弹 —— 基础操作

算法萌新如何学好动态规划(第一弹)

万物皆可快速上手之Electron(第一弹)