本周分为模拟题和作业题两部分,作业题针对课上的讲的贪心和二分进行了巩固和训练。
模拟
A题
题目描述
一个圆环上写有26个字母,还上一个指针最初指向a,每次只能顺时针或逆时针旋转一格,a顺时针移动到z,逆时针移动到b。现给定一个字符串,最少需要转几次才能得到
思路
对每一个字符求到当前指针所指字母的最小距离,要么顺时针要么逆时针,所以可以利用ASCII码来判断,逆时针的距离是ASCII码差值的绝对值,顺时针的距离则是26减去逆时针的距离,选择较小的一个即可。
#include<iostream>
#include<string.h>
#include<algorithm>
using namespace std;
char c[10005];
int main ()
{
cin>>c;
int n=strlen(c);
int ans=0,tmp=0;
for(int i=0;i<n;i++)
{
int t=c[i]-'a';
ans+=min(abs(t-tmp),26-abs(t-tmp));
tmp=t;
}
cout<<ans;
return 0;
}
/*zeus*/
/*18*/
B题
题目描述
考试周一共n天,每天需要ai个生煎,只有两种购买方式:①在某一天一次性买两个生煎。②今天买一个生煎,同时为明天买一个生煎,店家会给一个券,第二天用券来拿。每种购买方式每天可以使用无数次。考试周结束时手里不能剩有券。请问能否每天恰好买ai个生煎。
思路
虽然每天两种购买方式都可以使用无数次,但是可以按照需要购买数的奇偶来分。如果为偶数则全买方式1,如果为奇数则买一次方式2。考试的时候是另开了一个数组来存储券数,后来想想其实没必要,只要将第二天的ai数目减一即可,若第n+1天为负则有券剩余,则不能满足要求。
#include<stdio.h>
using namespace std;
int a[100005];
int b[100005];
int main()
{
int n;
scanf("%d",&n);
for(int i=0;i<n;i++)
{
scanf("%d",&a[i]);
}
if(a[0]%2==1) b[1]=1;
else if(a[0]%2==0) b[1]=0;
for(int i=1;i<n;i++)
{
if((a[i]-b[i])%2==1) b[i+1]=1;
else if((a[i]-b[i])%2==0) b[i+1]=0;
else
{
printf("NO");
return 0;
}
}
if(b[n]>0) printf("NO");
else printf("YES");
return 0;
}
/*4
1 2 1 2*/
//YES
C题
题目描述
宇宙射线默认向上,会在发射出一段距离后向该方向的左右45°分裂成两条。共分裂n(n<=30)次,每次在分裂方向前进ai(<=5)个单位长度。计算共有多少个位置到达。
思路
考试的时候只是简单的进行了规律的寻找,一直在寻找数和数的规律,所以完全没有过orz。
课下补题的时候发现这就是一个搜索题,利用bfs或dfs即可完成。下面使用了bfs,每次将分裂后的起点加入队列中。然后每次从队列中取出一个点先进行前进,然后再分裂。但是!如果只是简单这样做会MLE,因为队列会一直加入2^30 次方的数据,量太大了。再结合题目数据进行分析,向上最多30* 5=150,所以数组数目可以开[300][300],点的个数共有90000个,但是如果将每个分裂的点都考虑进来的话,共有2^30*5个点,很明显有很多个点发生了重复,所以要进行剪枝。将同一层(保证步长相同)的点到达的分裂情况相同(指方向和位置)的点剪去,即不加入队列。
#include <stdio.h>
#include<iostream>
#include <math.h>
#include <queue>
#include<string.h>
using namespace std;
int n,ans;
int a[35];
int visit[300][300];
int reduce[300][300][10][10];//剪枝数组
struct point
{
int x,y;
int angle;
};
void straight(point &s, int length)
{
int i=length;
switch (s.angle)
{
case 0:
while(i)
{
s.y++;
visit[s.x][s.y]=1;
i--;
}
break;
case -1:
while(i)
{
s.x--;
s.y++;
visit[s.x][s.y]=1;
i--;
}
break;
case 1:
while(i)
{
s.x++;
s.y++;
visit[s.x][s.y]=1;
i--;
}
break;
case -2:
while(i)
{
s.x--;
visit[s.x][s.y]=1;
i--;
}
break;
case 2:
while(i)
{
s.x++;
visit[s.x][s.y]=1;
i--;
}
break;
case -3:
while(i)
{
s.x--;
s.y--;
visit[s.x][s.y]=1;
i--;
}
break;
case 3:
while(i)
{
s.x++;
s.y--;
visit[s.x][s.y]=1;
i--;
}
break;
case 4:
while(i)
{
s.y--;
visit[s.x][s.y]=1;
i--;
}
break;
case -4:
while(i)
{
s.y--;
visit[s.x][s.y]=1;
i--;
}
break;
default:break;
}
}
void bfs(int sx,int sy)
{
int un=0;//未到达过
int get=0;
int time=1;//新加入队列的个数
int yk=1;//层数
queue<point> q;
point c;
c.x=sx;
c.y=sy;
c.angle=0;
q.push(c);
while(yk<=n)
{
point s=q.front();
q.pop();
//一个按照角度前进的函数
straight(s,a[yk-1]);
//分叉
point tmp;
tmp.x=s.x;
tmp.y=s.y;
tmp.angle=s.angle-1;
if(tmp.angle==-5)
{
tmp.angle=3;
}
if(!reduce[tmp.x][tmp.y][tmp.angle+3][tmp.angle+5])
{
reduce[tmp.x][tmp.y][tmp.angle+3][tmp.angle+5]=1;
q.push(tmp);
}
else un++;
tmp.angle=s.angle+1;
if(tmp.angle==5)
{
tmp.angle=-3;
}
if(!reduce[tmp.x][tmp.y][tmp.angle+3][tmp.angle+5])
{
reduce[tmp.x][tmp.y][tmp.angle+3][tmp.angle+5]=1;
q.push(tmp);
}
else un++;
time--;
if(time==0) //判断是否为同一层
{
time=pow(2,yk)-un-get*2;
get=un+get*2;
memset(reduce,0,sizeof(reduce));
un=0;
yk++;
}
}
}
int main()
{
ans=0;
scanf("%d",&n);
for(int i=0;i<n;i++)
scanf("%d",&a[i]);
visit[150][150]=1;
memset(visit, 0, sizeof(visit));
memset(reduce,0,sizeof(reduce));
bfs(150,150);
for(int i=0;i<300;i++)
for(int j=0;j<300;j++)
if(visit[i][j]==1) ans++;
printf("%d",ans);
return 0;
}
/*4
4 2 2 3*/
//39
ps:放一下某大佬的dfs代码orz,我还有待加强,尤其是在思路技巧和数据处理方面。
int dx[8]={0,1,1,1,0,-1,-1,-1};
int dy[8]={1,1,0,-1,-1,-1,0,1};
void dfs(int t,int x,int y,int d)
{
if(flag[t][x][y][d]||t>sum) return;
flag[t][x][y][d]=1;//标记到达
if(!vis[x][y])
{
vis[x][y]=1;
}
if(!tim[t])
{
dfs(t+1,x+dx[d],y+dy[d],d);
}
else
{
dfs(t+1,x+dx[(d+1)%8],y+dy[(d+1)%8],(d+1)%8);
dfs(t+1,x+dx[(d+7)%8],y+dy[(d+7)%8],(d+7)%8);
}
}
作业题
A题
题目描述
有n个作业,每个作业只花费1天,但是有DDL,如果未在DDL之前完成则扣除改作业相应的分数。如何安排有最小的总扣除分数?
easy版思路
将n个DDL根据分数从大到小排序,依次遍历,对于第i个DDL,根据时间ti从后向前遍历,一旦有空闲则将之安排进去。由于两套遍历所以复杂度为O(n^2);
hard版思路
从后往前枚举每一天,给每一天安排任务,枚举到第x天时,将所有ti=x的 deadline 加入最大堆中,再从最大堆中选取一个价值最大的 deadline 安排在第x天。时间复杂度为O(nlogn),实现的时候利用STL优先级队列表示堆,按分数进行排序。遍历任务如果ddl为当天则push,同时选出该天对应分数最高者pop。
#include <stdio.h>
#include <string.h>//memset在这里!
#include <queue>//优先级队列
using namespace std;
struct ddl
{
int time,score,finish,number;
bool operator<(const ddl &d) const
{
//if(time!=d.time) return time<d.time;
if(score!=d.score) return score<d.score;
else return false;
}
}job[1005];
int n;
int main()
{
scanf("%d",&n);
for(int p=0;p<n;p++)
{
int ans=0,num=0,last=0;
memset(job,0,sizeof(job));
scanf("%d",&num);
for(int i=0;i<num;i++)
{
scanf("%d",&job[i].time);
job[i].number=i;
if(job[i].time>last) last=job[i].time;
}
for(int i=0;i<num;i++)
{
scanf("%d",&job[i].score);
}
//从最后一天开始遍历
priority_queue<ddl> q;
for(int i=last;i>=0;i--)
{
for(int j=0;j<num;j++)
{
if(job[j].time==i)
q.push(job[j]);//ddl为第i天的话push
}
if(!q.empty())
{
ddl tmp=q.top();
job[tmp.number].finish=i;//完成时间为第i天
q.pop();
}
}
for(int j=0;j<num;j++)
{
if(job[j].finish==0)
ans+=job[j].score;
}
printf("%d\n",ans);
}
return 0;
}
B题
题目描述
给出四个数列,每个数列n个数字,从每个数列中各取一个数,有多少种方案使得4个数和为0?
思路
采用二分的思想,而不是所有的进行枚举。分别计算AB数组的和并进行排序,然后枚举CD的和并计算它的相反数在AB和中出现的次数,即一个数在有序数列中第一次和最后一次出现的位置。查找相反数的时候采用二分查找。
#include <stdio.h>
#include <algorithm>
using namespace std;
int num,ans=0;
int a[4][4005];
int sum1[16000005];
int sum2[16000005];
int main()
{
scanf("%d",&num);
for(int i=0;i<num;i++)
for(int j=0;j<4;j++)
scanf("%d",&a[j][i]);
//两两分组,分别计算数字之和并存储。
int tmp=0;
for(int i=0;i<num;i++)
{
for(int j=0;j<num;j++)
{
sum1[tmp]=a[0][i]+a[1][j];
sum2[tmp]=a[2][i]+a[3][j];
tmp++;
}
}
sort(sum1,sum1+tmp);//排序,便于sum2查找
for(int i=0;i<tmp;i++)
{//对每一个sum2都在sum1中找一遍相反数的个数 ,查找时二分查找
int l=0,r=tmp;
while(l<r)
{
int mid=(l+r)>>1;
if(sum1[mid]+sum2[i]>=0)
{
r=mid;
}
else l=mid+1;
//有可能一段数都符合
while(sum1[l]+sum2[i]==0&&l<tmp)
{
ans++;
l++;
}
}
}
printf("%d",ans);
return 0;
}
/*6
-45 22 42 -16
-41 -27 56 30
-36 53 -37 77
-36 30 -75 -46
26 -38 -10 62
-32 -54 -6 45*/
//5
C题
题目描述
给定数组X,构造数组B={|Xi-Xj|,1<=i<j<=n},求解B的中位数。
思路
如果暴力枚举,将数列B计算出来然后取中位数,复杂度为O(n^2),会超时。
可以先计算出B数组的数字个数,然后中位数的名次很容易得到。再计算B数组中的最小数和最大数,寻找中间的数作为假定的中位数P,然后对其正确性进行验证。计算Xj-Xi<=P的个数,若个数少于中位数名次-1,则在0-P中二分,反之则在P+1~MAX中二分。总的来说一共进行了两次二分。一次找名次,一次找中位数。
#include <stdio.h>
#include <algorithm>
#include <string.h>
using namespace std;
int N;
int cat[100005];
int find(int x,int i)
{
//这里rr应该是N-1,因为这里是计算找到满足这个条件的can[j]出现的最大位置,是数组的下标
int ll=i+1,rr=N-1,ans=-1;
while(ll<=rr)
{
int mmid=(ll+rr)/2;
if(cat[mmid]<=x)
{
ans=mmid;
ll=mmid+1;
}
else rr=mmid-1;
}
return ans;
}
int main()
{
while(scanf("%d",&N)!=EOF)
{
memset(cat,0,sizeof(cat));
for(int i=0;i<N;i++)
scanf("%d",&cat[i]);
sort(cat,cat+N);
int len=N*(N-1)/2;
int re=(len+1)/2;//中位数应在位次
int l=0,r=cat[N-1]-cat[0];
int mid=0;
while(l<=r)
{
int tmp=0;
mid=(l+r)/2;
for(int j=0;j<N;j++)
{
/*for(int i=0;i<j&&i<N;i++)
{
if(cat[j]<=cat[i]+mid)
tmp++;
}*///超时了
int a=find(cat[j]+mid,j);
//这里应该是a!=-1,应该如果find未找到的话,返回的ans,原值就是-1
if(a!=-1) tmp+=a-j;
}
if(tmp>=re) r=mid-1;
else l=mid+1;
}
printf("%d\n",l);
}
return 0;
}