问题 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;
}