HNUCM2020年湖南省大学生计算机程序设计竞赛第1场选拔赛题解

问题 A: 小h的消息加密

题目描述

小h最近看了谍战电影,对里面消息的加密方式很感兴趣,他决定和朋友试一试,小h给朋友两个序列,两个序列的最长公共单调递增子序列就是要传递的消息,有时候序列太长了,小h的朋友找不出来,所以他找到了你

输入

第一个数字n表示序列长度
后面两行每行n个数字表示小h给出的两个序列
(n<=1000)

输出

输出需要传递的原序列的长度

样例输入

3
1 2 3
3 1 2

样例输出

2

提示

需要传递的是1 2这个子序列

思路

这是公共递增子序列问题,最朴素的算法是O(n^3) dp[i][j]表示以b[j]结尾的公共子序列

如果a[i]==b[j]那答案就是a[i]和b[j]匹配接在dp[i-1][k]后(k小于j且b[k]<b[j])

​ 至于为什么是dp[i-1]呢,因为dp[i-1][k]必定比dp[i-x][k](x>1)更优

如果a[i]!=b[j]那答案就是dp[i-1][j],不要a[i]嘛

PS:当然n的范围是1000的话n3就是109肯定会超时的

O(n3)代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
int a[1005],b[1005],dp[1005][1005];
int main()
{
    
    
    int n,m;
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<=n;++i)scanf("%d",&b[i]);
    for(int i=1;i<=n;++i){
    
    
        int num=0;
        for(int j=1;j<=n;++j){
    
    
            if(a[i]==b[j]){
    
    
                dp[i][j]=1;
                for(int k=1;k<j;++k){
    
    
                    if(b[k]<b[j]&&dp[i-1][k]+1>dp[i][j]){
    
    
                        dp[i][j]=dp[i-1][k]+1;
                    }
                }
            }else{
    
    
                dp[i][j]=dp[i-1][j];
            }
        }
    }
    int ans=0;
    for(int i=1;i<=n;++i)ans=max(ans,dp[n][i]);
    printf("%d\n",ans);
    return 0;
}

n^3会超时那考虑优化一维前两维很明显是不能优化的,必须枚举所有情况,那只能从第三维入手

枚举第二维的时候维护最长的能接在b[j]前的不就可以了

用一个临时数组num存比在b[j]前比b[j]小的最长公共递增子序列长度

那第三维就可以省略了。

当然因为dp的时候状态至于前一列状态有关,甚至可以将dp数组转成一维,不过意义不大,在这就不写了

O(n2)代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
int a[1005],b[1005],dp[1005][1005];
int main()
{
    
    
    int n,m;
    scanf("%d",&n);
    for(int i=1;i<=n;++i)scanf("%d",&a[i]);
    for(int i=1;i<=n;++i)scanf("%d",&b[i]);
    for(int i=1;i<=n;++i){
    
    
        int num=0;
        for(int j=1;j<=n;++j){
    
    
            dp[i][j]=dp[i-1][j];
            if(a[i]>b[j]&&num<dp[i-1][j])num=dp[i-1][j];//b[k]比a[i]小就取dp[i-1][k]的最大值
            if(a[i]==b[j])dp[i][j]=num+1;//当a[i]==b[j],num取的是比a[i]小的b[k]那自然b[j]>b[k]
        }
    }
    int ans=0;
    for(int i=1;i<=n;++i)ans=max(ans,dp[n][i]);
    printf("%d\n",ans);
    return 0;
}

问题 B: 小h的数列Ⅱ

题目描述

有一个初始长度为0的数列,三种操作
1.将某一个元素插入
2.将某一个元素删除
3.查询当前状态

输入

第一个数字m表示有m个操作
后面m行表示m个操作
每行输入一个数字op
如果op=1表示第一个操作,后面接着两个数字a,b表示在第a个位置插入b(a以及a后的数字后移)
如果op=2表示第二个操作,后面接着一个数字a表示删除第a数字
如果op=3表示第三个操作,查询当前数列的状态
(m<=1000,操作保证合法)

输出

对于每一个op=3输出当前数列的状态

样例输入

3
1 1 3
1 2 4
3

样例输出

3 4

思路

这题本来是作为链表题来出的,后面改了数据范围,那用数组暴力模拟也可以过

数组代码

#include<bits/stdc++.h>
#include<string.h>
#include<math.h>
#include<stdio.h>
#include<algorithm>
#define ll long long
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
const int mod=1e9+7;
int ans[maxn];
int main()
{
    
    
    int n,op,now=0,a,b;
    scanf("%d",&n);
    while(n--){
    
    
        scanf("%d",&op);
        if(op==1){
    
    
            scanf("%d %d",&a,&b);
            ++now;
            for(int i=now;i>a;--i){
    
    //插入位置到最后一个元素右移
                ans[i]=ans[i-1];
            }
            ans[a]=b;
        }else if(op==2){
    
    
            scanf("%d",&a);
            for(int i=a;i<now;++i){
    
    //删除位置后一个元素到最后一个元素左移
                ans[i]=ans[i+1];
            }
            --now;
        }else{
    
    
            for(int i=1;i<=now;++i){
    
    
                printf("%d%c",ans[i],i==now?'\n':' ');
            }
        }
    }
    return 0;
}

链表代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
const int inf=0x3f3f3f3f;
struct node{
    
    //链表节点结构体
    int v;
    node *nex;
};
void cout_(node *head){
    
    //循环输出链表的值
    node *now=head->nex;//第一个元素是head的nex(head是不存东西的)
    while(now!=NULL){
    
    //循环输出,直到链表末尾
        printf("%d",now->v);
        now=now->nex;
        if(now!=NULL){
    
    
            printf(" ");
        }
    }
    printf("\n");
}
int main()
{
    
    
    int n,m,op,a,b;
    node *head=(node*)malloc(sizeof(node));//给头结点指针分配内存
    head->nex=NULL;
    scanf("%d",&m);
    for(int i=1;i<=m;++i){
    
    
        scanf("%d",&op);
        if(op==1){
    
    
            scanf("%d %d",&a,&b);
            a--;
            node *now=head;
            while(a--){
    
    //找到插入位置的前一个元素
                now=now->nex;
            }
            node *ne=(node*)malloc(sizeof(node));
            //链表插入
            ne->v=b;
            ne->nex=now->nex;
            now->nex=ne;
        }else if(op==2){
    
    
            scanf("%d",&a);
            a--;
            node *now=head;
            while(a--){
    
    //找到删除位置的前一个元素
                now=now->nex;
            }
            //链表删除
            node *p=now->nex;
            now->nex=p->nex;
            free(p);
        }else{
    
    
            //输出链表数据
            cout_(head);
        }
    }
    return 0;
}

问题 C: 回文素数

题目描述

现有一个正整数,希望去掉这个数中某一个数字之后可以得到一个回文素数。
回文素数是指一个素数同时还是一个回文数(回文数即从左到右和从右到左均一样的数,例如12321)。【注意:一位数也被认为是回文数】
输入两个正整数N和M,满足N<M,请编写一个程序统计N和M之间存在多少个满足上述要求的数?

输入

单组输入。输入一行,包含两个正整数N和M,1<=N<M<=1000000,两个数字之间用空格隔开。

输出

输出在N和M之间(包含N和M)满足要求的数的个数。

样例输入

110 120

样例输出

10

提示

在110和120之间存在10个满足要求的数,分别是110、111、112、113、114、115、116、117、118和119,它们去掉最后一位数都可以得到一个回文素数11。

思路

暴力枚举区间[n,m],枚举去掉每一位的数字,判断回文和素数
下面的代码用了6素数法(当然普通的也可以)
就是素数都在6的左右两边,所以只需要判断能否被6左右两边的数字整除就可以了
证明:
所有的数字可以写成

6*k+0,6*k+1,6*k+2,6*k+3,6*k+4,6*k+5

这6种形式,+0的可以被6整除,+2,+4的可以被2整除,+3的可以被3整除
那就只剩下+1和+5的
+1的在6的倍数的右边,+5的也就是-1,在6的倍数的左边

代码

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e6+5;
const int inf=0x3f3f3f3f;
int get(int x){
    
    //获取数字x的位数
  int ans=0;
  while(x){
    
    
    ++ans;
    x/=10;
  }
  return ans;
}
int a[105];
bool ju1(int x){
    
    //判断素数
  if(x<=3)return x>1;
  if(x%2==0||x%3==0)return 0;
  int k=sqrt(x)+1;
  for(int i=5;i<k;i+=6){
    
    
    if(x%i==0||x%(i+2)==0)return 0;
  }
  return 1;
}
bool ju2(int x){
    
    //判断回文
  int a=x,b=0;
  while(x){
    
    
    b=b*10+x%10;
    x/=10;
  }
  return (a==b);
}
int ju(int x){
    
    //判断回文素数
    int lx=get(x);
    for(int i=lx;i>=1;--i){
    
    //将x拆分成一位位的存放到数组a中
      a[i]=x%10;
      x/=10;
    }
    for(int i=1;i<=lx;++i){
    
    //枚举去掉的位数
      int y=0;
      for(int j=1;j<=lx;++j){
    
    //获取去掉第i位后的数字
        if(j==i)continue;
        y=y*10+a[j];
      }
      if(ju1(y)&&ju2(y)){
    
    //判断素数和回文
        return 1;
      }
    }
    return 0;
}
int vis[maxn];
int main()
{
    
    
    int n,m,ans=0;
    scanf("%d %d",&n,&m);
    for(int i=n;i<=m;++i){
    
    
      if(ju(i)){
    
    
        ++ans;
      }
    }
    printf("%d\n",ans);
    return 0;
}

问题 D: 数列求和

题目描述

求以下数列的和:

f(n)=1/5-1/10+1/15-1/20+1/25-......+1/(5*(2*n-1))-1/(5*2*n)。

输入

每组数据一个输入,每个输入一行,输入n。(n<=100)

输出

输出数列前n项的和,结果四舍五入保留四位小数。

样例输入

1

样例输出

0.1000

思路

由最后两项可以知道,数列是

(1/5-1/10)+(1/15-1/20)+...+(1/(5*(2*n-1))-1/(5*2*n))

所以直接计算f(n)将中间答案累加就可以了

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
int main()
{
    
    
    int n;
    double ans=0,res=0;
    scanf("%d",&n);
    for(int i=1;i<=n;++i){
    
    
        ans+=1.0/(5*(2*i-1))-1.0/(5*2*i);
        res+=ans;
    }
    printf("%.4lf\n",res);
    return 0;
}

问题 E: 牛牛的闹钟

题目描述

牛牛总是睡过头,所以他定了很多闹钟,只有在闹钟响的时候他才会醒过来并且决定起不起床。从他起床算起他需要X分钟到达教室,上课时间为当天的A时B分,请问他最晚可以什么时间起床?

输入

每个输入包含一个测试用例。
每个测试用例的第一行包含一个正整数,表示闹钟的数量N(N<=100)。
接下来的N行每行包含两个整数,表示这个闹钟响起的时间为Hi(0<=A<24)时Mi(0<=B<60)分。
接下来的一行包含一个整数,表示从起床算起他需要X(0<=X<=100)分钟到达教室。
接下来的一行包含两个整数,表示上课时间为A(0<=A<24)时B(0<=B<60)分。
数据保证至少有一个闹钟可以让牛牛及时到达教室。

输出

输出两个整数表示牛牛最晚起床时间。

样例输入

3 
5 0 
6 0 
7 0 
59 
6 59

样例输出

6 0

思路

循环判断最大的小于最迟起床时间的时间

#include<bits/stdc++.h>
#define ll long long
using namespace std; 
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
struct node{
    
    
    int a,b;
}w[105];
int main()
{
    
    
    int n,x,a,b;
    while(~scanf("%d",&n)){
    
    
        for(int i=1;i<=n;++i){
    
    
            scanf("%d %d",&w[i].a,&w[i].b);
        }
        scanf("%d %d %d",&x,&a,&b);
        int en=a*60+b;//上课时间
        int ans=0;
        for(int i=1;i<=n;++i){
    
    
            int st=w[i].a*60+w[i].b+x;//第i个点起到学校的时间
            if(st<=en&&st>w[ans].a*60+w[ans].b+x){
    
    //可以到学校并且比记录的答案更优就更新
                ans=i;
            }
        }
        printf("%d %d\n",w[ans].a,w[ans].b);
    }
    return 0;
}

问题 F: 繁殖

题目描述

在R星球有一类昆虫它是可以无性繁殖的,身为昆虫调查员的你接到了命令前往R星球去调查昆虫的繁殖情况
经过一系列的调查,你发现该种昆虫一生最多可以繁殖出两个子代。现在你发现了这种昆虫身上可能会带有一种病毒,上级已经给你发来通知,告诉你了两只带有该病毒昆虫的编号。
为了快速找出这种病毒你必须找到这两只昆虫的最近公共父辈(不会存在两只昆虫出现相同编号)。

输入

第一行 一段序列,代表昆虫的序号。每个昆虫带有两个子代,若没有子代的用-1代替。(如序列:1 2 -1 -1 3 -1 -1,表示1号有2,3两个子代而2,3没有子代,类似于二叉树的先序遍历)
第二行 两个已知带有病毒的昆虫编号

输出

输出一行,代表病毒昆虫的公共父辈编号

样例输入

3 5 6 -1 -1 2 7 -1 -1 4 -1 -1 1 9 -1 -1 8 -1 -1
5 1

样例输出

3

思路

最近公共祖先问题(LCA)
最朴素的解法是求出每个点的深度,对于要判断的两个点,先一次次让它走向它的父节点,知道两个点深度一样,再同时让它俩走向所在点的父节点,直到走到同一个节点,这个点就是所求两个点的最近公共祖先

朴素LCA代码

#include<bits/stdc++.h>
#include<string>
#define ll long long
using namespace std;
const int maxn=2e5+5;
const int inf=0x3f3f3f3f;
struct node{
    
    
    int l,r,v;
};
node edge[maxn];
int cnt=0,pre[maxn],dep[maxn];
int fa[maxn];
int build(int v,int f){
    
    
    int x,now=++cnt;
    scanf("%d",&x);
    edge[now].v=x;//第now个节点的昆虫编号
    dep[now]=v;//第now个节点的深度
    fa[now]=f;//第now个节点的父节点编号
    if(x!=-1){
    
    
        pre[x]=now;//编号为i的昆虫对应的节点编号
        edge[now].l=build(v+1,now);
        edge[now].r=build(v+1,now);
    }
}
int lca(int a,int b){
    
    
    if(dep[a]<dep[b])swap(a,b);//将a换成深的节点
    while(dep[a]!=dep[b]){
    
    //将a一步步上移
        a=fa[a];
    }
    while(a!=b){
    
    //a,b同时上移
        a=fa[a];
        b=fa[b];
    }
    return edge[a].v;//返回a节点的昆虫编号
}
int main()
{
    
    
    build(1,0);
    int a,b;
    scanf("%d %d",&a,&b);
    printf("%d\n",lca(pre[a],pre[b]));
    return 0;
}

容易发现上移过程是一步步的,很憨,那可以考虑多跨几步嘛
然后就有了LCA的倍增优化,倍增优化是利用二进制的性质
dp[i][j]表示第i个节点上移j次所在的节点
具体实现看代码吧

倍增优化LCA代码

#include<bits/stdc++.h>
#include<string>
#define ll long long
using namespace std;
const int maxn=2e5+5;
const int inf=0x3f3f3f3f;
struct node{
    
    
    int l,r,v;
};
node edge[maxn];
int cnt=0,pre[maxn],dep[maxn];
int dp[maxn][32];
int build(int v,int fa){
    
    
    int x,now=++cnt;
    scanf("%d",&x);
    edge[now].v=x;
    dep[now]=v;
    dp[now][0]=fa;
    if(x!=-1){
    
    
        pre[x]=now;//编号为i的昆虫对应的节点数
        edge[now].l=build(v+1,now);
        edge[now].r=build(v+1,now);
    }
}
int lca(int a,int b){
    
    
    if(dep[a]<dep[b])swap(a,b);
    int x=dep[a]-dep[b];//两个点的深度距离
    for(int i=0;i<=28;++i){
    
    //将a和b的深度距离二进制化地对a进行上移
        if(x&(1<<i)){
    
    
            a=dp[a][i];
        }
    }
    if(a==b)return edge[a].v;
    for(int i=28;i>=0;--i){
    
    
        if(dp[a][i]!=dp[b][i]){
    
    
            a=dp[a][i];
            b=dp[b][i];
        }
    }
    return edge[dp[a][0]].v;
}
int main()
{
    
    
    build(1,0);
    for(int i=1;i<=28;++i){
    
    
        for(int j=1;j<=cnt;++j){
    
    
            dp[j][i]=dp[dp[j][i-1]][i-1];//处理出所有的dp数组
        }
    }
    int a,b;
    scanf("%d %d",&a,&b);
    printf("%d\n",lca(pre[a],pre[b]));
    return 0;
}

问题 G: 1234

题目描述

小明的弟弟刚刚学会了写英语的一(one)、二(two)、三(three)和四(four)。

他在纸上写了一个单词,这个单词是one、two、three和four中的某一个。已知单词最多只有一个字母写错,但是单词的长度肯定不会错。

你能认出他写的是哪个单词吗?

输入

单组输入。

输入占1行,对应一个单词。单词长度正确,且最多只有一个字母写错。所有字母都是小写字母,且输入保证只有一种理解方式。

输出

输出一行,即该单词对应的阿拉伯数字。

样例输入

thref

样例输出

3

思路

先判断长度,如果是长度是5那就是3如果长度是4那就是4
如果长度是3就判断一下和"one"对应有几个位上的字符相等,有大于等于2个数字相等就是1,否则是2

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=2e5+5;
string s;
int main()
{
    
    
    cin>>s;
    if(s.size()==5){
    
    
        cout<<"3"<<endl;
    }else if(s.size()==4){
    
    
        cout<<"4"<<endl;
    }else{
    
    
        if(((s[0]=='o')+(s[1]=='n')+(s[2]=='e'))>=2){
    
    
            cout<<"1"<<endl;
        }else{
    
    
            cout<<"2"<<endl;
        }
    }
    return 0;
}

问题 H: 寻找虫虫

题目描述

小虫虫由于出来找食物飞走了,小红红回到学校之后发现虫虫不见了,好在他在虫虫的身上装了定位。小红红跟着定位来到了一个迷宫,这个迷宫很复杂,几乎每条道路都是交错的,为了能快的找到虫虫,你能帮助小红红判断该路径是否存在回路嘛?
Tips:
在离散数学的定义下:在某个集合上的偏序得出全序的过程称为拓扑排序。
偏序:若集合S上的关系R为自反,反对称和传递的,则称R是集合S上的偏序关系
设R是集合S上的偏序,若对于每个x,y∈xRy或yRx,则称R为集合S的全序。
拓扑排序的流程如下:
\1. 在有向图中选取一个没有前驱节点的点输出
\2. 从图中删除该顶点和所有以该点为尾的弧
一直重复以上步骤,直至顶点全部输出,或图中存在环为止。

输入

输入的第一行包含一个整数n,表示有n个节点。n不超过50
之后的n行中有整数0和1,对于第i行j列的值,若为1则表示i与j两点有边连接且又向的i点指向j点,若为0则没有边。保证不存在点的自回路(即i == j 时的值为0)

输出

若读入的有向图含有回路,输入"can not find the bug"
若没有回路,则按照题目的描述依次输出图的拓扑排序后的有序数列,每个整数后输出一个空格。
注意尾行的换行。

样例输入

4
0 1 0 0
0 0 1 0
0 0 0 0
0 0 1 0

样例输出

3 0 1 2 

思路

按题目要求进行拓扑排序…

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn=1e5+5;
const int inf=0x3f3f3f3f;
int w[105][105],in[105],ans[105],cnt;
int main() {
    
    
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;++i){
    
    
        for(int j=1;j<=n;++j){
    
    
            scanf("%d",&w[i][j]);
            if(w[i][j]){
    
    //计算入度
                ++in[j];
            }
        }
    }
    queue<int> pq;
    for(int i=n;i>=1;--i){
    
    //由样例可知是从大到小遍历
        if(in[i]==0){
    
    //将入度为0的入队
            pq.push(i);
        }
    }
    while(!pq.empty()){
    
    //将队中的出队
        int now=pq.front();
        pq.pop();
        ans[++cnt]=now-1;
        for(int i=n;i>=1;--i){
    
    //由样例可知是从大到小遍历
            if(w[now][i]){
    
    //要出队的点有边的入度减一
                --in[i];
                w[now][i]=0;
                if(in[i]==0){
    
    //入度减为0就入队
                    pq.push(i);
                }
            }
        }
    }
    if(cnt==n){
    
    //如果所有都被遍历完了说明没有回路
        for(int i=1;i<=cnt;++i){
    
    
            printf("%d%c",ans[i],i==cnt?'\n':' ');
        }
    }else{
    
    
        printf("can not find the bug\n");
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_43984169/article/details/108549284