第三周
A题
输入一串数字,给定和sum和加数个数n,要求给出n个数字和为sum的方案个数。
要得到一串数字中任意n个数字的和,我们采用深度优先搜索:对于任意位置的数字都允许其作为加数和不作为加数的进行下一步搜索,我们就可以完成对所有情况的遍历。当遇到符合条件的情况时,搜索停止且搜索量加一,遇到不符合条件的情况(例如加数个数已超过条件限制)也要及时终止搜索,以防运行时间过长。
本题作为针对课堂例题的练习,难度不大。重点在于对深度优先搜索“剪枝”的把握。无论广度优先搜索还是深度优先搜索,在使用时都应该有意识的对情况进行判断选择,对于不符合条件的情况应使搜索及时终止,以防程序运行时间过长。
#include<iostream>
using namespace std;
void dfs(int i,int sum,int t,int n,int K,int &num,int *a){
if(t==K&&sum==0){
num++;
return;
}
if(i>=n) return;
if(t>K||sum<0) return;
dfs(i+1,sum,t,n,K,num,a);
dfs(i+1,sum-a[i],t+1,n,K,num,a);
}
int main(){
int T;
cin>>T;
for(int i=0;i<T;i++){
int n,K,S;
cin>>n>>K>>S;
int sum=S;
int a[n];
for(int j=0;j<n;j++){
cin>>a[j];
}
int num=0;
dfs(0,sum,0,n,K,num,a);
cout<<num<<endl;
}
}
B题
给定多个闭区间,选择尽可能少的点使每个区间至少包含一个点。
要尽可能的少找点说明一个点要尽可能满足多个区间,所以对于任意不包含点的区间,我们在区间内新增点的同时,也要尽可能保证这个点有落在其他区间的最大可能性。如何保证?我们将所有区间按照右端点由小到大顺序排列。然后依次进行判断:如果该区间内没有点(区间左端点值比最后一个点的值要大)我们就将区间右端点作为新增点。也就是说,如果区间内有点,我们对该区间不做处理,没有点,就选择区间最大点作为新增点,之所以选择区间最大点,是因为如果两个区间有交集,那么必包含前一区间的最大值。选择最大点就使所选点落在其他区间的可能性最大。当所有区间判断完毕,我们也就得到了所需点的数量。
本题在实现的时候一直认为除了右端点,区间左端点的排列方法也很重要。但在刚刚实验总结的过程中,突然发现左端点的排列方式是无关紧要的。尽管不是一个很重要的发现,但是在总结与思考的过程中获得了新的收获,也算完成了总结思考的初衷。
#include<iostream>
#include<algorithm>
using namespace std;
struct D{
int a;
int b;
bool operator<(const D &p) const{
if(b!=p.b) return b<p.b;
else return a>p.a;}
};
int main(){
int N;
cin>>N;
D d[N];
for(int i=0;i<N;i++){
cin>>d[i].a>>d[i].b;
}
sort(d,d+N);
int point=d[0].b;
int t=1;
for(int i=1;i<N;i++){
if(d[i].a>point){
point=d[i].b;
t++;
}
}
cout<<t<<endl;
}
C题
给出多个区间,要求从中选择尽可能少的区间以覆盖一个目标区间,输出最少区间数。
要用尽可能少的区间覆盖一个目标区间。说明我们在每一次选择的过程中要选择一个覆盖当前未被覆盖区域最多的区间。具体做法是在每一次覆盖之前将每个区间不属于未被覆盖部分的部分去掉,然后按照区间左端点由小到大,右端点由小到大的方式排列。在区间左端点最小的那部分区间中选择右端点值最大的区间。如果该区间右端点大于未被覆盖区间的右端点,证明未被覆盖区间被完全覆盖,我们将区间数增一后输出,反之证明仍未被完全覆盖,我们在区间数量增一后要继续选择。注意此时未被覆盖区间的左端点变为已选择区间的右端点。
这道题出现的问题在于算法设计过程的一些纰漏,例如未实现每次选择前的切除(去掉每个区间中不属于未被覆盖的部分)和排序,未对可能出现的部分情况进行判断(例如如果所有区间左端点都大于未覆盖区间左端点,则不可能成功覆盖等),这就要求在思想正确的同时,也要从头至尾思考算法执行过程,确保过程没有疏漏,不出纰漏的算法才是正确有价值的算法。
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
struct D{
int a;
int b;
bool operator<(const D &p) const{
if(a!=p.a) return a<p.a;
else return b<p.b;}
};
int main(){
int N,T;
cin>>N>>T;
int begin=0;
D d[N];
for(int i=0;i<N;i++){
scanf("%d%d",&d[i].a,&d[i].b);
}
sort(d,d+N);
int t=0;
int i=0;
int times=0;
bool flag=false;
while(!flag&×!=1&&i<N){
times=1;
if(d[i].a>begin+1){
times=1;
}
else{
int j=i;
while(d[j].a<=begin+1&&j<N){
d[j].a=begin+1;
times=0;
j++;}
sort(d+i,d+j);
i=j-1;
if(d[i].b>=T){
t++;
flag=true;
}
else if(d[i].b<T&&d[i].b>d[i].a){
t++;
begin=d[i].b;
}
else if(d[i].b<=d[i].a){
times=1;
}
i++;
}}
if(flag) cout<<t<<endl;
else cout<<-1<<endl;
}
CSP
A题
一个可随意转动但一次只能转动一位的轮盘刻有a~z26个字母,起始点为a。给定一串字符串,给出轮盘依次选择每个字符的最小转动次数。
由于轮盘可随意转动,所以无论是顺时针一直转还是逆时针一直转都会转到想要的位置。区别在于次数的不同,如果按照一个方向需要i次,那么另一个方向便需要26-i次。比较这两个数的大小,我们就可以得到选择每个字符的最小转动次数,依次相加我们就得到了最终答案。
这道题思想较为简单,按位处理即可,唯一需要注意的一点是在计算转动次数的时候仅仅用后一项减前一项是会出现负数的,需要先把负数转为正数再进行i和26-i的大小比较。
#include<iostream>
#include<string>
using namespace std;
int main(){
string s;
cin>>s;
long long int step=0;
char begin='a';
for(int i=0;i<s.length();i++){
int a=s[i]-begin;
if(a<0) a=-a;
if(a>13){
step=step+26-a;
}
else step=step+a;
begin=s[i];
}
cout<<step<<endl;
}
B题
生煎有两种购买方式:当天一次性买两个,或者当天一个,第二天凭劵一个。给定天数和每天要购买的生煎个数。判断在两种购买方式的限制下,购买的生煎能否恰好吃完。
由于如果劵的数量多于第二天要购买生煎的数量,那么一定吃不完,所以最有可能吃完的情况是每一天劵的数量都最少的情况。如果在凭劵领取的生煎之外,还需要偶数个生煎,那么直接全部采取方案一,反之采取一次方案二剩下全部为方案一。这样如果在劵数最少的情况下劵数仍超过第二天的生煎购买数量,证明无论如何也不会恰好吃完。基于这个思想,我们依次判断,如果到了最后一天的第二天(购买数必为0)前一天的劵数都不超过购买数,证明生煎恰好能吃完,反之则不能。
本题一开始被两种购买方案迷惑,一直在思考每天两种方案的组合,导致在进行深度优先搜索时情况过多,所需时间空间超过题目限制。在发觉自己算法思想有问题时仔细思考,才得到了对后来最有可能情况的处理。本实验使我明白遇到问题不能上来就莽,要多想想解决问题的思路,争取以最简单的思想与算法解决问题,在成功解决问题的前提下,少做少错。
#include<iostream>
using namespace std;
int a[100001];
int m;
void dfs(int s,int n){
if(s<m){
if(a[s]<n){cout<<"NO"<<endl;}
else {
n=(a[s]-n)%2;
dfs(s+1,n);
}
}
else if(s==m){
if((a[s]-n)%2==0){
cout<<"YES"<<endl;
}
else {
cout<<"NO"<<endl;
}}}
int main(){
cin>>m;
for(int i=1;i<=m;i++){
cin>>a[i];
}
dfs(1,0);
}
C题
宇宙射线会在发射出一段距离后向发射方向的左右45度方向分裂且继续发射。给定射线分裂次数和每一次分裂的长度,给出被射线涉及到的位置数。
这道题最暴力的做法是发射后向左右两个方向分别进行深度优先搜索,但是这样操作无疑会超出时间限制。我们不妨换种思路:由于每一次都会向左右45度分裂,所以对于每一次被涉及到的位置,都会有与其关于上一次发射方向对称的位置。如果利用对称的思想,我们就可以将每个过程中的两次深度优先遍历化简为一次深度优先遍历。由深度优先搜索的最末一个分支开始,找到其关于上次发射方向相对称的点,再以这些点为整体,继续寻找相对称的点,由此我们就可以找到对称的所有点,以每个过程一次深度优先搜索实现两次深度优先搜索的功能。最后得到被涉及到的位置数。
由于对剪枝处理的不熟练,导致在一开始尽管想到了两次深度优先搜索然后剪枝的处理方法但还是未能实现。紧接着就想通过对称解决问题。起初是想在深度优先搜索的同时进行对称查找,但这种操作并不能实现对所有分支的处理。但在后来在与同学请教时,发现了对称处理的办法:先深度搜索到最末分支,然后递归对称,由最末分支开始找到所有对称位置。同时也在后续学长的讲解中,了解了剪枝的处理办法。递归对称方法诚然很巧妙,但是不是很容易思考。我会尽可能加深对剪枝的理解,以便于在之后处理解决类似的问题。
#include<iostream>
#include<stdio.h>
#include<set>
using namespace std;
int b[31];
int m;
int ds[8][2]={{0,1},{1,1},{1,0},{1,-1},{0,-1},{-1,-1},{-1,0},{-1,1}};
struct point{
int x;
int y;
point(){
x=0, y=0;
}
point(int a, int b){
x = a;
y = b;
}
};
bool operator <(const point &m,const point &n){
if(m.x!=n.x)
return m.x<n.x;
else
return m.y<n.y;
}
set<point> a;
void dfs(const point& p,int i,int s){
if(s<=m){
int m=p.x,n=p.y;
m=m+ds[(i+1)%8][0]*b[s];
n=n+ds[(i+1)%8][1]*b[s];
point p1(m,n);
dfs(p1,(i+1)%8,s+1);
m=p.x;n=p.y;
for(int j=0;j<b[s];j++){
m=m+ds[(i+1)%8][0];
n=n+ds[(i+1)%8][1];
point p2(m,n);
a.insert(p2);
}
if(s>1){for(set<point>::iterator it=a.begin();it!=a.end();it++)
{
point p3;
if(i%4==0)
{
p3.x=2*p.x-it->x;
p3.y=it->y;
}
else if(i%4==1)
{
p3.x=it->y-p.y+p.x;
p3.y=it->x+p.y-p.x;
}
else if(i%4==2)
{
p3.x=it->x;
p3.y=2*p.y-it->y;
}
else if(i%4==3)
{
p3.x=p.x+p.y-it->y;
p3.y=p.x+p.y-it->x;
}
a.insert(p3);
}
}}
else return;}
int main(){
cin>>m;
for(int i=1;i<=m;i++){
cin>>b[i];
}
point p(0,0);
dfs(p,7,1);
cout<<a.size()<<endl;
}
第四周
A题
给定多个作业的分数和DDL,给出多种做作业顺序情况下最小扣分值。
要求扣分最少,那么分数多的作业就要尽可能多的完成。我们就可以按照分数优先对作业进行选择。首先将所有作业按照分数由大到小的顺序排列。从分数最高的作业开始,以作业的DDL为起始点向前搜索,如果该作业DDL前有时间完成该作业,我们就将该作业的完成时间设为与作业DDL最接近的时间,反之该作业就被我们舍弃,该作业分数为应扣分数,对每个作业都进行处理,我们就得到了最小扣分值。
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
struct D{
int DDL;
int score;
bool operator<(const D &p) const{
if(score!=p.score) return score<p.score;
}
};
int main(){
int N,T;
cin>>T;
int i=0;
while(i<T){
cin>>N;
i++;
D d[N];
int a[1500]={0};
int sum=0,sum1=0;
for(int i=0;i<N;i++){
scanf("%d",&d[i].DDL);
}
for(int i=0;i<N;i++){
scanf("%d",&d[i].score);
sum=sum+d[i].score;
}
sort(d,d+N);
for(int i=N-1;i>=0;i--){
for(int j=d[i].DDL;j>0;j--){
if(a[j]==0){
a[j]=1;
sum1=sum1+d[i].score;
break;
}
}}
cout<<sum-sum1<<endl;
}}
B题
给定四个数列,每个数列都有n个数字。从每个数列中取一个数作为一种组合,给出和为0的组合数。
该题最暴力的做法是枚举每一种组合,但在n较大的情况下必会超出程序运行时间。我们采取二分法对程序进行优化:将四个数列两两分组,计算出每一组中任意两个数的和,再将这两组和放在一起比对,如果互为相反数,则证明相加和为0,为符合条件的情况,由此得到和为0的组合数。
分组求和的设计很好实现,问题在于求和后的比对工作。在尝试了一系列比对方法(比如分别对两组和进行排序,其中一组由前到后遍历,另一组由后到前遍历,在此思想下的多次尝试)后决定依次算出第二组和每个元素的相反数在第一组和中的个数再相加。但在此思想下也WA多次,但在与AC同学的交流中发现多组数据答案是一致的,我决定把它归结为玄学问题(……),在参考了AC同学的意见之后,艰难的完成了实验。思想是真的掌握了,肝也是真的肝不动了。
#include<iostream>
#include<stdio.h>
#include<algorithm>
#include<vector>
using namespace std;
int A[4001],B[4001],C[4001],D[4001];
vector<int>sum1,sum2;
int m=0;
int head(int s)
{
int left=0,mid=0,right=m,result=-1;
while (left<=right)
{
mid=(left+right)/2;
if (sum1[mid]==s)
{
result=mid;
right=mid-1;
}
else if (sum1[mid]>s)
right=mid-1;
else
left=mid+1;
}
return result;
}
int tail(int s)
{
int left=0,mid=0,right=m,result=-1;
while (left<=right)
{
mid=(left+right)/2;
if (sum1[mid]==s)
{
result=mid;
left=mid+1;
}
else if (sum1[mid]>s)
right=mid-1;
else
left=mid+1;
}
return result;
}
int main(){
int n;
cin>>n;
for(int i=0;i<n;i++){
cin>>A[i]>>B[i]>>C[i]>>D[i];
}
int sum=0;
for(int i=0;i<n;i++){
for(int j=0;j<n;j++){
sum=A[i]+B[j];
sum1.push_back(sum);
sum=C[i]+D[j];
sum2.push_back(sum);
m++;
}
}
sort(sum1.begin(),sum1.end());
sum=0;
for (int i=0;i<m;i++)
{
int s=0-sum2[i];
int h=head(s);
int t=tail(s);
if (h!=-1&&t!=-1)
sum=sum+t-h+1;
}
cout<<sum<<endl;
return 0; }
C题
给定一个数组,定义新数组元素为给定数组中任意两项差的绝对值,求新数组的中位数。
由题意知新数组所有元素都是正数。那么我们可以将数组元素按序排列。(结果不变的同时,按序排列更方便操作)如果中位数是P,那么说明符合条件的被减数一定是所有被减数数量的一半。由于P一定介于0到差值最大值(排序后第一位和最后一位差)之间,所以我们可以采取二分法进行查找。对于每一个可能的P,计算出此时符合条件的被减数数量,判断是否符合要求,直至找到最后真正的中位数P。
这道题在学长提前的讲解下难度不是很大,唯一问题在于P的二分。最初的算法设计是找到符合条件的P(在该P下被减数数量满足要求)即输出。但后来发现符合条件的P并不一定属于新数组。在符合条件后仍要进行二分查找,最后范围内仅有的那一个数才是真正的中位数。
#include<iostream>
#include<stdio.h>
#include<algorithm>
using namespace std;
int tail(int s,int n,int *a)
{
int left=0,mid=0,right=n-1;
while (left<=right)
{
mid=(left+right)/2;
if (a[mid]>s)
{
right=mid-1;
}
else{
left=mid+1;
}}
return left-1;
}
int main(){
int N;
while(scanf("%d",&N)!=EOF){
int cat[N];
for(int i=0;i<N;i++){
scanf("%d",&cat[i]);
}
sort(cat,cat+N);
int num=0;
int m=cat[N-1]-cat[0],n=0;
int mid=(N*(N-1)/2+1)/2;
while(n<=m){
num=0;
int p=(m+n)/2;
for (int i=0;i<N;i++)
{
int s=p+cat[i];
int t=tail(s,N,cat);
if (t-i>0)
num=num+t-i;
}
if(num<mid){
n=p+1;
}
else{
m=p-1;
}
}
cout<<m+1<<endl;}
}