第三周作业:
第一题:选数问题
1.题意简介:从键盘输入n个整数,要求选择其中的k个数,使得这k个数的和为S,输出可选择的方案的个数。
2.做法思路:根据题意,选择正数的过程很适合用DFS来求解。在代码中,我定义了一个dfs_num函数,利用DFS的思想,依次选取键盘输入的正数,只有当正数和为S且选择的正数个数为k时,方案总数total才+1。
另外考虑到数据的数量不固定,为了简化复杂度,我在dfs_num中额外增加了剪枝操作的判断语句,如果选择的数超过规定的个数,或者所有的数都判断完了,或者选择的数之和已经超过S,就不需要继续DFS遍历下去,直接return。
3.代码:
#include<iostream> #include<math.h> #include<stdio.h> using namespace std; int total=0;//记录一组测试用例的答案 double a[100]={0};//存数据的数组 void dfs_num(int i,int j,int K,double sum,int num) {//递归,i为选择的数的个数,j为选择的数的序号,K为需要选择的数的个数,sum为当前选择的数的总和与要求总和的差 if(i==K&&sum==0) {//已经选择了足够的数,并且总和为S total++; return; } else if(i>K||j==num||sum<=0) {//如果选择的数超过规定的个数,或者所有的数都判断完了,或者选择的数之和已经超过S //剪枝操作 return; } dfs_num(i,j+1,K,sum,num); dfs_num(i+1,j+1,K,sum-a[j],num); } int main() { int n; cin>>n;//测试用例的数量 for(int i=0;i<n;i++) { int num,K,S;//用例中的数据个数,需要选择的数据个数,要求的和 cin>>num>>K>>S; //获取数据 for(int i=0;i<num;i++) { cin>>a[i]; } //通过类似dfs的算法来求解 dfs_num(0,0,K,S,num); cout<<total<<endl; total=0; } }
第二题:选点问题
1.题意简介:数轴上有 n 个闭区间 [a_i, b_i]。取尽量少的点,使得每个区间内都至少有一个点(不同区间内含的点可以是同一个)。
2.做法思路:要寻找最少的点,换句话说就是要判断所有无交集区间的个数。本题中我定义了一个区间类struct qu,类内包含一个区间的左边界a和右边界b,开始时申请足够的qu类的数组q,从键盘依次读入区间的左右边界,存放在q[i]中。读取完毕后进行一个从大到小的排序,便于下一步判断。
为了判断所有区间是否有交集,我首先令x=q[0].b,然后与q[1].a进行比较,若q[1].a小于x,则表示区间q[0]和q[1]有交集,则此时不需要增加点的个数,并且之后令x=q[1].b,以此类推,进行循环,每一次循环判断两个相邻区间是否有交集,判断完毕后输出无交集的区间个数num即可。
3.代码:
#include<iostream> #include<algorithm> using namespace std; struct qu {//区间类 int a;//区间左边界 int b;//区间右边界 qu(){};//构造函数 bool operator <(qu &q) {//重载操作符 if(a==q.a) return b<q.b; else return a<q.a; } }; int main() { qu q[100]; int n; cin>>n;//测试数据的个数 for(int i=0;i<n;i++) { cin>>q[i].a>>q[i].b;//输入给定的区间 } sort(q,q+n);//为区间排序,方便比较 int num=1;//点的数目,开始时只要区间不为0,就会有一个点 int x=q[0].b;//初始定义x为第一个区间的右边界 for(int i=0;i<n;i++) { if(x<q[i].a) {//x和q[i]区间没有交集,需要增加一个点 num++; x=q[i].b; } else//x和q[i]有交集,不需要增加点 { x=min(q[i].b,x); } } cout<<num<<endl; }
第三题:区间覆盖问题
1.题意简介:数轴上有 n (1<=n<=25000)个闭区间 [ai, bi],选择尽量少的区间覆盖一条指定线段 [1, t]( 1<=t<=1,000,000)。
覆盖整点,即(1,2)+(3,4)可以覆盖(1,4)。
不可能办到输出-1
2.做法思路:该题目涉及到最少问题,所以可以采用贪心算法解决,即每一次选择区间都选择尽量大的区间。
筛选:考虑到区间个数最多为25000个,线段长度最长为1000000,如果不进行一些筛选就直接进行选择的话时间复杂度会很高,所以我采用一维数组qu[1000001]={0}来存区间,数组序号表示区间左端点,数组内容表示区间右端点,在读取区间i的时候,若qu[i]不为0,表示这两个区间具有相同的左端点,此时根据贪心算法的规则,就比较两个区间的右端点,在qu[i]中存入较大的右端点,而较小的区间则舍弃,这样就能在进行选择前先进行一次筛选,减少选择算法的时间。
选择:利用贪婪算法,在循环体中,第一步找出所有左端点在qu[i]+1内的区间的右端点最大值,+1是为了保证找到的右端点最大值大于qu[i],因为根据题意,如果qu[i]+1范围内的所有区间长度都小于qu[i],则不可能用这些区间覆盖目标线段。在找到这个最长区间q[m]后,更新找到的区间的总长度,选择的区间个数+1,然后继续循环,直到总长度大于或等于线段长度t为止。
3.代码:
#include<iostream> #include<stdio.h> #include<cstdlib> #include<algorithm> //区间覆盖 using namespace std; int qu[1000001]={0};//区间数组,序号为左顶点,数组内数据为右顶点 int main() { int n,t; cin>>n>>t;//读取区间个数和线段长度 //读取区间,左端点相同时保留较大的区间 for (int i=0;i<n;i++) { int x,y; cin>>x>>y; if (qu[x]<y) qu[x]=y; } //cout<<a[1]<<endl; int m=qu[1]; int l=1; int sum=1;//记录当前已经选择的区间数量 int max=0;//记录已经选择的区间的右边界最大值 bool flag=true; while (m<t&&flag) { for (int i=l+1;i<=m+1;i++) if (qu[i]>max) {//在m+1的右边界范围内寻找所有区间中右顶点的最大值 max=qu[i]; } if (max<=m) { m=t; flag=false; } else { sum++; l=m+1; m=max; max=0; } } if(!flag) sum=-1; cout<<sum<<endl; return 0; }
第四周作业:
第一题:DDL的恐惧
1.题意简介:已知有n个作业,每个作业都有自己的ddl,如果没有在ddl前做完这个作业,那么老师会扣掉这个作业的全部平时分。已知每天只能做一个作业,要求合理安排作业,能被尽可能少扣一点分。
输入格式:输入的第一行是单个整数T,为测试用例的数量。每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。然后两行,第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
要求输出:输出最小的总降低分数。
2.做法思路:根据题目可以知道,我们要想得到最小的降低分数,就是需要在最长的ddl之前尽可能多的完成作业,由于每天只能完成一个作业,所以最佳的完成规划为每样作业都在自己的ddl那一天完成,这样就能把其他时间用来完成其他作业。
在上述思路的基础上,我们还需要考虑没有完成的作业的扣分情况,我们需要优先考虑那些分多的作业,所以在程序中,我们用ddl类的数组d[1500]读取完ddl和相应分数之后进行一个sort()的从大到小的排序,把分值高的优先排到前面,即优先完成前面的作业。
针对上述的思路,我们可以用一个一维数组c[1000]来记录每一天的时间占用情况,从之前排好序的数组d中从0开始依次往后取,取出的数据的ddl对应第c[ddl]天,表示这一天用来做这一样作业;如果c[ddl]已经被占用,则继续向前遍历,直到遇到没有被占用的时间为止。
3.代码:
#include<iostream> #include<stdio.h> #include<algorithm> using namespace std; struct ddl {//ddl类 int time;//截止时间 int score;//扣分 ddl(){ time=0; score=0; } }; bool compare(ddl a,ddl b) { return a.score>b.score; } int main() { int t; cin>>t;//读取测试样例个数 for(int i=0;i<t;i++) { ddl d[1500]; int n; cin>>n;//读取作业数量 for(int i=0;i<n;i++) { cin>>d[i].time; } for(int i=0;i<n;i++) { cin>>d[i].score; } sort(d,d+n,compare);//对ddl按扣分降序排序 int c[1000]={0};//空余时间 int uns=0;//总扣分 bool cando; for(int i=0;i<n;i++) { //cout<<"uns="<<uns<<endl; cando=false; //cout<<c[i]<<endl; for(int j=d[i].time;j>0;j--) { if(c[j]==0) { c[j]=d[i].score; cando=true; break; } } if(!cando) { uns=uns+d[i].score; } } cout<<uns<<endl; } }
第二题:四个数列
1.题意简介:已知有四个数列,每个数列有n个数字,现要从每个数列中取出一个数,问有多少种方案使得四个数和为0。
输入格式:第一行一个数字n,表示每个数列有n个数字(1<=n<=4000),接下来的 n 行中,第 i 行有四个数字,分别表示数列 A,B,C,D 中的第 i 个数字(数字不超过 2 的 28 次方)。
要求输出:输出满足条件的不同组合的个数。
2做法思路:本题思路较简单,是一个很典型的枚举操作类型,但是如果只是进行4次枚举,算法的复杂度就为O(n^4),按最多的数据量来算会超时,所以需要优化算法。本题的算法优化就在于枚举上,采用二分的思想,首先我们可以枚举前两个数列的数,然后再枚举后两个数列,这样算法复杂度就能降低到O(n^2),另外,在枚举后两个数列的时候,可以同时计算枚举结果的相反数(因为四个数和要为0)在之前两个数列的枚举中出现的次数,最后输出总次数即可。
针对上述的计算相反数的算法中也可以采用二分的思想,但这里需要运用两次二分查找,一次寻找相反数第一次出现的位置,一次寻找相反数第二次出现的位置,最后用第二次的位置减去第一次的位置+1就是该相反数的总出现次数(注意在寻找之前,需要对前两个数列的枚举结果进行排序)。
3.代码:
#include<iostream> #include<stdio.h> #include<algorithm> using namespace std; int sum1[16000000]; int findmin(int sum1[],int sum,int n) //二分查找最小的 { int l=0,r=n*n-1,ans=-1; while(l<=r) { int mid=(l+r)>>1; if(sum1[mid]>=-sum) { ans=mid; r=mid-1; } else l=mid+1; } if(sum1[ans]==-sum) return ans; else return -1; } int findmax(int sum1[],int sum,int n) //二分查找最大的 { int l=0,r=n*n-1,ans=-1; while(l<=r) { int mid=(l+r)>>1; // cout<<"&&"<<a[mid]<<" "<<x<<endl; if(sum1[mid]<=-sum) { ans=mid; l=mid+1; } else r=mid-1; } if(sum1[ans]==-sum) return ans; else return -1; } int main() { int n; cin>>n;//读取数列中数字的个数 int A[4001]; int B[4001]; int C[4001]; int D[4001]; for(int i=0;i<n;i++) {//读取所有数字 cin>>A[i]>>B[i]>>C[i]>>D[i]; } int m=0; //枚举A和B for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { int sum=A[i]+B[j]; sum1[m]=sum; //cout<<sum1[m]<<" "; m++; } } //cout<<endl; sort(sum1,sum1+m); /*for(int i=0;i<m;i++) cout<<sum1[i]<<" "; cout<<endl;*/ //枚举C和D,同时计算A和B的枚举中出现的相反数的次数 int num=0; for(int i=0;i<n;i++) { for(int j=0;j<n;j++) { int sum=C[i]+D[j]; //cout<<sum<<" "; int min=findmin(sum1,sum,n); int max=findmax(sum1,sum,n); if(min!=-1&&max!=-1) num=num+(max-min+1); /*for(int k=0;k<m;k++) { if(sum1[k]==-sum) { num++; if(sum1[k+1]!=-sum) break; } }*/ } } //cout<<endl; cout<<num<<endl; }
第三题:神秘礼物(这个题目槽点满满(。))
1.题意简介:已知一个数组cat[n],要求用这个数组生成一个新的数组,新数组定义为对于任意的 i, j 且 i != j,均有 ans[] = abs(cat[i] - cat[j]),1 <= i < j <= N。试求出这个新数组的中位数,中位数即为排序之后 (len+1)/2 位置对应的数字,'/' 为下取整。
输入格式:组输入,每次输入一个 N,表示有 N 个数,之后输入一个长度为 N 的序列 cat, cat[i] <= 1e9 , 3 <= n <= 1e5。
要求输出:输出新数组的中位数。
2.做法思路:这道题目看起来就是求中位数的一个题目,但是我们可以看到,输入的cat数组的长度最长为1e5,如果我们采用先按照题目给出的算法求出ans数组,然后再去找数组中位数的方法去做的话,时间复杂度极高,肯定会超时,所以这里我采用的方法为:
判断一个数P是不是中位数,可以转化为判断P在数组中从小到大排的名次的问题,如果P的名次等于中位数,那么P就是中位数,而这种方法的实现过程可以使用二分的思想来减少时间复杂度。
另外是计算P的名次的问题,这里我们也不能直接从数组开头依次访问来计算,时间复杂度依旧会很高,所以我们可以采用另外一种计算方式:计算出cat[i]-cat[j]<=P的数组对数也可以达到计算P的名次的目的,且算法的复杂度也比直接遍历要小,再将上式变形可以得到cat[j]<=cat[i]+P,依据变形后的算式,我们只需要枚举i ,然后计算出满足条件的cat[j]的个数即可。在这个过程中,也可以采用二分的方法进行。
3.代码:
#include <iostream> #include <algorithm> #include <stdio.h> using namespace std; int search(int target, int n,int cat[]) {//二分法查找 int start = 0, end = n - 1; while (start <= end) { int mid = (start + end) / 2; if (cat[mid] <= target) start = mid + 1; else if (cat[mid] > target) end = mid - 1; } if (start == 0) return -1; else return start - 1; } int find(int n,int cat[]) {//查找中位数 int max = cat[n-1]-cat[0]; int rank = (n*n-n+2)/4; int a=0, b=0, c=0; int start=0, end=max, mid=0; while(start <= end) { int mid = (start+end)/2; b=0; for(int j=0;j<n;j++) { a=cat[j] + mid; c=search(a,n,cat) - j ; if(c>0) b=b+c; } if(b>=rank) end=mid-1; else if(b<rank) start=mid+1; } return end+1; } int read() { int a=0,b=1; char c=' '; while(c<'0' || c>'9') { c=getchar(); if(c=='-') b=-1; } while(c>='0' && c<='9') { a=a*10+c-'0'; c=getchar(); } return a*b; } int main() { int n,sum; while(scanf("%d",&n)!=EOF) { //读取cat数组的数据 int cat[n]; for(int i=0;i<n;i++) { //cin>>cat[i]; cat[i]=read(); } sort(cat,cat+n);//进行排序,便于寻找中位数 sum = find(n,cat); cout<<sum<<endl; } return 0; }
CSP模拟题:
第一题:咕咕咚的奇遇
1.题意简介:已知26个英文字母按顺序排列,且首尾相连(a的前一个是z,z的后一个是a),开始时给定一个指向a的指针,每一次指针可以移动一个字母,现给定一个英文字符串,要求输出按顺序读完这个字符串指针需要移动的最小次数。
2.做法思路:分析题目我们可以知道,指针每一次的移动不受左右限制,但每次只能移动一个字母,想要得到最少的总移动次数,就需要考虑每一次指针从当前字母移动到下一个目标字母所需要的移动次数最小的问题,这样的话我们就可以采用递归的思想来解决这个问题。由于每次指针只会向左移动或者向右移动,所以我们在单次算法中可以设计两个移动方向r和l,分别代表向右/左移动到目标字母所需要的移动次数,分别计算出r和l,再取它们中的较小值,就得到了单次移动的最小次数,接下来只需要进行递归操作即可得出答案。
3.代码:
#include<iostream> #include<cstring> #include<stdio.h> using namespace std; int sum=0; char a[26]={'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'}; void find(int i,int j,int n,char a[24],char b[10002]) { if(j==n) return; int r=0; int l=0; for(int x=i;x<=26;x++) { if(x==26) x=0; if(a[x]==b[j]) break; else r++; } for(int x=i;x<=26;x--) { if(x==-1) x=25; if(a[x]==b[j]) { i=x; break; } else l++; } sum=sum+min(r,l); j++; find(i,j,n,a,b); } int main() { char b[10002]={0}; cin>>b; int i=0; int j=0; int n=0; while(b[n]!=0) n++; find(i,j,n,a,b); cout<<sum<<endl; return 0; }
第二题:咕咕咚想吃饭
1.题意简介:咕咕咚这个孩子很奇怪,每天都要吃煎饼果子(好像是?),而且每天的摄入量都不一样,而且是个土豪。现在煎饼果子老板推出了两种购买方案:方案一:一次性买两个煎饼果子;方案二:立刻买一个煎饼果子,并且额外购买一张兑换券,兑换券可以到第二天直接换一个煎饼果子。现给出咕咕咚想吃煎饼果子的天数n和每天的煎饼果子数量a[i],要求计算能否在n天结束时,每天都吃到a[i]个煎饼果子(即没有剩余),且恰好用完兑换券。能则输出“YES”,不能则输出“NO”。
2.做法思路:本题需要分两种情况讨论,分析两种方案可以知道,当天需要的煎饼果子数量可以通过规划选择两种方案的次数来解决,当然此时我们还需要记录获得的兑换券的张数,这些兑换券在第二天可以换一部分的煎饼果子,剩下的煎饼果子则继续考虑选择两种方案的次数。仔细观察可以发现,我们最后要判断的就是手里有的兑换券的数量num是否为0 ,所以在设计算法时,我们可以围绕num来进行计算,即根据选择的方案算出每天结束时剩余的兑换券个数,用算式num=(a[i]-num)%2,即可以绕过选择方案的复杂搭配,也可以直接得到兑换券剩余的数量,循环n天后,判断num是否为0即可。
3.代码:
#include <iostream> using namespace std; int main() { int n; cin>>n; int num = 0;//剩余的兑换券张数 int a[n];//每天要买的个数 for(int i=0;i<n;i++) cin>>a[i]; for(int i=0;i<n;i++) { if(num>a[i]) { cout<<"NO"<<endl; return 0; } else num = (a[i]-num)%2;//今天买完后剩下的兑换券 } if(num!=0) cout<<"NO"<<endl; else cout<<"YES"<<endl; return 0; }
第三题:可怕的宇宙射线(物理意义上和题目难度上都很可怕(。))
1.题意简介:宇宙射线在一个二维平面上进行传播,初始方向默认为向上,每次经过一段距离就会进行分裂,向该方向的左右45°方向分裂两条射线。宇宙射线共分裂n次,每次分裂后会向该方向前进a[i]个单位的长度。
输入格式:第一行一个正整数n,表示宇宙射线会分裂n次,第二行n个正整数a1,a2,a3,…ai,第i个数ai(ai<=5)表示第i次分裂的宇宙射线会朝原方向上走ai个单位长度。
要求输出:输出分裂结束后宇宙射线一共经过了多少个位置。
2.做法思路:初步审题,我选择了直接递归求解的方法,但是该题中宇宙射线的分裂是指数型的,分裂的线段长度最大可达2^30,直接递归毫无疑问直接超时,因此需要我继续改进算法。在进一步分析题目,可以发现每次宇宙射线分裂后移动的长度都不超过五个单位,且分裂次数最多为30次,根据这个信息可以确定宇宙射线的最大传播范围为300*300的一个方形区域。另外注意到,宇宙射线每次分裂都是朝固定的左右45°方向进行分裂的,也就是说以一开始的初始方向为中心轴,两边的分裂轨迹是对称的,借助这一个特点,在搜索路径时就可以只搜索一半的路径,另一半在递归模拟分裂时做对称标记即可。使用这种方法的算法复杂度相较于一开始的指数式递归就能节省大量的时间,也就可以解决超时的问题了。搜索完成后,所有经过的位置都由bool map[i][j]记录,只需要统计map中为true的位置的个数就可以得出答案。
3.代码:
#include<iostream> using namespace std; int d[8][2]={{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1}}; //从北开始的顺时针 struct point {//点类 int x,y;//坐标 point() { x=0; y=0; } }; void half(point p,int f,point &p2,int x,int y) {//对称路线,记录一半,另一半对称标记 f=f%4; //f有四种情况 switch(f) { case 0: { p2.x=p.x+p.y-y; p2.y=p.x+p.y-x; break; } case 1: { p2.x=2*p.x-x; p2.y=y; break; } case 2: { p2.x=y-p.y+p.x; p2.y=x+p.y-p.x; break; } case 3: { p2.x=x; p2.y=2*p.y-y; break; } default:return; } } void find(int i,int n,int j,int a[],bool map[400][400],point p) {//递归分裂过程 if(i>=n) return; //大于n,返回 int x=p.x; int y=p.y; x=x+d[j][0]*a[i]; y=y+d[j][1]*a[i]; //终点位置 point p2; p2.x=x; p2.y=y; int rf=(j+1)%8; find(i+1,n,rf,a,map,p2); //递归 x=p.x,y=p.y; for(int n=0;n<a[i];n++) { x=x+d[j][0]; y=y+d[j][1]; map[x][y]=true; } if(i!=0) { for(int n=0;n<400;n++) { for(int m=0;m<400;m++) { if(map[n][m]==true) { point p3; half(p,j,p3,n,m);//对称点进行标记 map[p3.x][p3.y]=true; } } } } } int main() { int n; cin>>n;//读取分裂次数 bool map[400][400]={false}; int m[400][400]={-1}; int a[n]; for(int i=0;i<n;i++) {//读取第i次分裂走的长度 cin>>a[i]; } //开始分裂 point p1; p1.x=200; p1.y=200; //进行分裂,用map数组标记分裂过程中经过的点 find(0,n,0,a,map,p1); //根据map的标记计算经过的点的数量 int sum=0; for(int i=0;i<400;i++) for(int j=0;j<400;j++) { if(map[i][j]) sum++; } cout<<sum<<endl; return 0; }