赞
踩
2023.03第29次CCF-CSP计算机认证考试
CCF计算机软件能力认证考试系统
大二菜鸟第一次参加CSP考试,发挥得很烂(其实是实力太菜了 ),考前也没看往年题目套路,有很多不甘吧,不过拟打算六月再参加一次。如果早知道题目难度是依次递增的,就不写完两题就去啃最后一题了,最后写第三题的时候都在赶,最后一题还没啃下多少分…
话不多说,接下来分享我的思路。
在坐标轴上给定你一个矩形,再给若干个矩形(这些矩形之间无重合部分),算出这些矩形与你的矩形的重合部分面积总和为多少。
枚举出矩形在不同情况下的不同计算方法,如在边上、被包含的情况等等,时间复杂度为O(n)。这种方法的好处在于几乎不用思考用什么算法,只需要注意边界条件。
不得不感谢出题人手下留情,给出了不会出现矩形多次覆盖同一区域的情况的条件,否则情况将会复杂一点。
思路图解:
补充: 求重叠线段长度从而算出重叠面积的方法更为简便。
#include<bits/stdc++.h> using namespace std; int n, a, b; long long ans; int main() { int x1, x2, y1, y2; scanf("%d%d%d", &n, &a, &b); for(int i=0;i<n;i++) { scanf("%d%d%d%d",&x1,&y1,&x2,&y2); if(x1>=0&&y1>=0&&x2<=a&&y2<=b) ans+=(x2-x1)*(y2-y1); else if(x1<0&&x2>0&&y1<b&&y2>b) ans+=(b-y1)*x2; else if(x1<a&&a<x2&&y1<b&&b<y2) ans+=(b-y1)*(a-x1); else if(x1<0&&0<x2&&y1<0&&0<y2) ans+=x2*y2; else if(x1<a&&a<x2&&y1<0&&0<y2) ans+=(a-x1)*y2; else if(x1>=0&&x2<=a&&y1<b&&y2>b) ans+=(x2-x1)*(b-y1); else if(x1<a&&x2>a&&y1>=0&&y2<=b) ans+=(y2-y1)*(a-x1); else if(x1>=0&&x2<=a&&y1<0&&y2>0) ans+=(x2-x1)*y2; else if(y1>=0&&y2<=b&&x1<0&&x2>0) ans+=(y2-y1)*x2; } printf("%lld",ans); return 0; }
评测结果
有几块田,耕完需要的时间都不同,花的资源也不同,但是所有田可以同时耕。用资源能缩短耕田的天数,但是最少不能小于k天。需要思考如何分配资源使总天数最少。
听一些大佬说是二分题。但是我没有这样做,提供一下我当时的思路。
这题让我想起木桶盛水的故事,“由长短不一的木板造出的木桶能盛多少水取决于最短的那块木板”。不过这题是反过来的,花费的总时间取决于最多天数的田,最多天数的田不变的情况下,就算分配所有资源给后面的田,需要的总时间也不变。于是我们考虑贪心法:每次分配资源给最大天数的田。
接下来,还需要考虑一个问题。如果存在若干个天数相同的田,只减少其中若干片田的时间,总时间仍不变。就是说,要使这种情况下的天数有效减少,必须同时削减所有相同天数的田,而且需要减少相同时间,使它们步伐保持一致,所以我们可以考虑合并方法,把所有具有相同天数的田所需的资源合并在一起,视为一块田。同时,在计算中削减某块田的天数后也要进行合并。
基于此,我想到了使用STL库的map。将键值对设置为<天数,所需资源>,方便将相同的天数合并它们的资源。另外,map具有自动对key进行排序的特点,正好满足我们对天数进行贪心的需求。需要注意map默认对key降序排列,所以要加greater。
map<ll,ll,greater<ll>>land;//按天数降序
在map的begin位置的是天数最大的田,要做的是将它与天数第二大的田合并资源,合并后,删除该键值对。循环执行,直到资源不够了或者所有天数都为k时结束。
m-=cost;
it1->second+=it->second;
land.erase(it->first);
还有一种情况,资源不够合并了。这时算算当前资源还能减多少天,算出来直接输出,程序结束。
ans=it->first;
while(m>=it->second)
{
m-=it->second;
ans--;
}
printf("%lld",ans);
break;
需要注意,无论是否存在天数为k的田,都需要加入key为k的键值对,以保证每次都能取出两个键值对进行合并。
land[k]+=0;//最少天数,保证每次能有两个合并
时间复杂度分析:需要分两种情况:
1.所有都能减少到k天,外层循环为O(n),即总共扫过一遍表;map.erase操作复杂度为O(logn),总共为O(nlogn)。
2.资源不够,设当前位置田在原来排第m位,则已进行mlogm次运算,内层循环不超过1e5-k次,复杂度为O(mlogm+1e5-k)。
由于m<=n,所以两种情况复杂度相差不大。
可以优化: 在每次合并操作之后让迭代器往后移一位(注意跳出循环条件和迭代器赋值需要些许改动),省去了erase,复杂度为O(n)。不过这题数据量不大,O(nlogn)和O(n)都行(map重度依赖 )。
补充:又回来补了个二分法,就当练手
这题的二分主要坑点在于多数情况下是不能直接搜到答案的(资源不能恰好分完),而是直接循环到low>high,最后停止处落在答案左侧或右侧,但一定经过答案。另外,从最小天数k到田的最大天数都是我们的搜索范围,low应该初始化为k。
对于如何找到答案,我们每次得到预测天数mid时,计算大于mid天的田全部减到mid天需要多少资源,此时有三种情况,我们用不同的返回值:
0——刚好用完,直接得出答案;
-1——资源不够;
1——资源盈余。
用这样的函数实现:
int dcs(int day)
{
int sum=0;
for(int i=n-1;land[i].first>day&&i>=0;i--)
sum+=land[i].second*(land[i].first-day);
if(m==sum) return 0;//资源刚好
else if(m<sum) return -1;//不够资源
else return 1;//资源盈余
}
在二分查找中调用这个函数,同时对返回值进行处理。最终答案如果不能恰好分配,那答案一定是在资源有盈余的情况下得出来的,所以我们用最小天数mind维护返回值为1的情况,查找结束后mind即为答案。
while(low<=high)
{
mid=(low+high)>>1;
if(dcs(mid)==0) {mind=mid;break;}
else if(dcs(mid)==1) {mind=min(mind,mid);high=mid-1;}
else low=mid+1;
}
return mind;
贪心:
#include<bits/stdc++.h> using namespace std; typedef long long ll; ll n,m,k; ll ans=-1; map<ll,ll,greater<ll>>land;//按天数降序 int main() { scanf("%lld%lld%lld",&n,&m,&k); for(int i=0;i<n;i++) { ll t,c; scanf("%lld%lld",&t,&c); land[t]+=c; } land[k]+=0;//最少天数,保证每次能有两个合并 map<ll,ll,greater<ll>>::iterator it=land.begin(); while(m>=it->second&&it->first>k)//资源还够减少天数,且不是全部都为最小天数 { map<ll,ll,greater<ll>>::iterator it1=it; it1++; ll cost=((it->first-it1->first)*it->second); if(m-cost<0)//不够减 { ans=it->first; while(m>=it->second) { m-=it->second; ans--; } printf("%lld",ans); break; } else//足够合并 { m-=cost; it1->second+=it->second; land.erase(it->first); } it=land.begin(); } if(ans==-1)//其实这已经说明能全部减到最小天数了 printf("%lld",it->first); return 0; }
评测结果(贪心)
二分:
#include<bits/stdc++.h> using namespace std; const int MAX_N=1e5+10; typedef pair<int,int>pit; int n,m,k; pit land[MAX_N]; bool cmp(pit p1,pit p2){return p1.first<p2.first;} int dcs(int day) { int sum=0; for(int i=n-1;land[i].first>day&&i>=0;i--) sum+=land[i].second*(land[i].first-day); if(m==sum) return 0;//资源刚好 else if(m<sum) return -1;//不够资源 else return 1;//资源盈余 } int binsrch() { int mind=1e6; int low=k,high=land[n-1].first,mid; while(low<=high) { mid=(low+high)>>1; if(dcs(mid)==0) {mind=mid;break;} else if(dcs(mid)==1) {mind=min(mind,mid);high=mid-1;} else low=mid+1; } return mind; } int main() { scanf("%d%d%d",&n,&m,&k); for(int i=0;i<n;i++) { int t,c; scanf("%d%d",&land[i].first,&land[i].second); } sort(land,land+n,cmp); printf("%d",binsrch()); return 0; }
评测结果(二分)
给定用户的编号和他们各自的属性名称和属性值,再给出满足题目规定的表达式的用户:
a:b——有属性a且值为b的用户集合
a~b——有属性a且值不为b的用户集合
&(a :/~ b)(c :/~ d)——先分别求括号内的用户集合,再求集合交
|(a :/~ b)(c :/~ d)——先分别求括号内的用户集合,再求集合并
这是一道模拟。数据有原子表达式、简单表达式和嵌套表达式三种。只要求拿70分的话很简单,表达式第一个字符为数字的是原子表达式,第三个字符为数字的是简单表达式。要拿到满分我们需要面对类似“&(&(3:1)(|(1:2)(2:3)))(|(&(2~5)(2~7))(2:5))”这样的字符串。
首先要做的就是解析字符串。因为怕递归可能会超时所以我这里提供一种迭代的方法(菜! 后面知道递归方法也可以解决)。逆向思维从右往左,维护一个集合栈,遇到数字则说明是原子式,立即计算,算完入栈。同一括号内,遇到“&”“|”符号就取栈顶的两个集合进行交并,运算结果再入栈,可以保证我们的运算顺序是对的(因为这两个符号和它的两个表达式相邻)。此时,表达式中的括号成为了无效字符,可以跳过。
处理字符串,我们从len-1往前搜索,每次开始前先跳过括号,如果遇到了数字,则说明接下来要处理一个原子式了,设立一个左标记把它提取出来送到base_expr函数处理,然后更新游标。如果是操作符,则需要进行集合的交并运算。
for(int r=len-1;r>=0;r--) { while(str[r]==')'||str[r]=='(')r--; if(isdigit(str[r])) { int l=r-1; while(l>=0&&(isdigit(str[l])||str[l]==':'||str[l]=='~'))l--; base_expr(&str[l+1],&str[r+1]); r=l-1; } //集合的交和并 if(r>=0&&str[r]=='&') its(); else if(r>=0&&str[r]=='|') uni(); }
因为设计到集合交并,所以使用求集合交并方便且快的bitset。每次从栈中取两个,算完再入栈。
集合的交和并:
inline void its()//交运算 { bs b1=ss.top(); ss.pop(); b1=b1&ss.top(); ss.pop(); ss.push(b1); } inline void uni()//并运算 { bs b1=ss.top(); ss.pop(); b1=b1|ss.top(); ss.pop(); ss.push(b1); }
bitset按位表示。第m位为1,则说明集合中有编号m的用户。要使用bitset直接表示max=1e9的DN显然不合理,但是用户数量最多为2500,所以可以使用哈希用数组dnlist下标来映射DN(对应的其他DN编号也换成下标)。
用一个类型为pair二维数组lib表示用户的属性,下标和dnlist一一对应。当求断言时,在lib中找有该属性且为指定值的用户,修改bitset。但是仅仅这样我们无法求反断言式,所以要unordered_map一个属性和用户集合,表示有该属性的用户有哪些,然后我们可以在这些用户里面查找指定pair,断言时查找成功,反断言时查找失败(有该属性但不是指定值),说明满足条件,标记。
void asrt(int aname,int aval,char oper)//断言与反断言 { bs b; pit p(aname,aval); if(oper==':')//断言 { for(si it=ulib[aname].begin();it!=ulib[aname].end();it++)//获取有该属性的用户 { if(binary_search(lib[*it].begin(),lib[*it].end(),p)) b.set(*it); } } else//反断言 { for(si it=ulib[aname].begin();it!=ulib[aname].end();it++)//获取有该属性的用户 { if(!binary_search(lib[*it].begin(),lib[*it].end(),p)) b.set(*it); } } ss.push(b); }
最后就是提取bitset,按位映射回dnlist,内容存到ans数组,升序排序再输出。
#include<bits/stdc++.h> using namespace std; const int MAX_N=2510; typedef pair<int,int> pit; typedef set<int>::iterator si; typedef set<int> sti; typedef bitset<MAX_N> bs; int n,m; int dnlist[MAX_N]; int ans[MAX_N]; char str[2010]; vector<vector<pit>>lib; unordered_map<int,sti>ulib;//键值对:<attname,set<DN>> stack<bs>ss;//存每次运算结果 int mystoi(char *s,char *end) { int base=1,sum=0; for(int i=end-s-1;i>=0;i--) { sum+=base*(s[i]-'0'); base*=10; } return sum; } inline void its()//交运算 { bs b1=ss.top(); ss.pop(); b1=b1&ss.top(); ss.pop(); ss.push(b1); } inline void uni()//并运算 { bs b1=ss.top(); ss.pop(); b1=b1|ss.top(); ss.pop(); ss.push(b1); } void asrt(int aname,int aval,char oper)//断言与反断言 { bs b; pit p(aname,aval); if(oper==':')//断言 { for(si it=ulib[aname].begin();it!=ulib[aname].end();it++)//获取有该属性的用户 { if(binary_search(lib[*it].begin(),lib[*it].end(),p)) b.set(*it); } } else//反断言 { for(si it=ulib[aname].begin();it!=ulib[aname].end();it++)//获取有该属性的用户 { if(!binary_search(lib[*it].begin(),lib[*it].end(),p)) b.set(*it); } } ss.push(b); } void base_expr(char *expr,char *end)//解析原子表达式 { int op=0; while(expr[op]!=':'&&expr[op]!='~')op++; int num1,num2; num1=mystoi(expr,&expr[op]); num2=mystoi(&expr[op+1],end); asrt(num1,num2,expr[op]); } int main() { scanf("%d",&n); lib.resize(n+1); for(int i=1;i<=n;i++)//建表 { int DN,attnum,attname,attv; scanf("%d%d",&DN,&attnum); dnlist[i]=DN; lib[i].resize(attnum); for(int j=0;j<attnum;j++) { scanf("%d%d",&attname,&attv); lib[i][j]=pit(attname,attv); ulib[attname].insert(i); } sort(lib[i].begin(),lib[i].end()); } scanf("%d",&m); for(int i=0;i<m;i++) { scanf("%s",str); int len=strlen(str); for(int r=len-1;r>=0;r--) { while(str[r]==')'||str[r]=='(')r--; if(isdigit(str[r])) { int l=r-1; while(l>=0&&(isdigit(str[l])||str[l]==':'||str[l]=='~'))l--; base_expr(&str[l+1],&str[r+1]); r=l-1; } //集合的交和并 if(r>=0&&str[r]=='&') its(); else if(r>=0&&str[r]=='|') uni(); } if(!ss.empty()) { int index=0; bs ansbs=ss.top(); for(int i=1;i<=n;i++) if(ansbs[i]==1) ans[index++]=dnlist[i]; sort(ans,ans+index); for(int i=0;i<index;i++) printf("%d ",ans[i]); } printf("\n"); while(!ss.empty())ss.pop(); } return 0; }
评测结果(好险啊 )
后面可能还会分析历年的csp题目。实力实在有限,但在分析的过程中也学到了很多。
另外就是,都看到这里了,能不能给个小小的赞鼓励一下——
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。