试题内容请前往CCF官网查看:
CCF-CSP计算机软件能力认证考试
http://118.190.20.162/home.page
阅读本题解前,您应当了解下列知识:
- 线段树 教程
- C++ 标准库(含STL) 教程
这是一份以C++代码编写的CSP 专业组 202206题解。
请注意这不是CSP-S/J的中学生竞赛的题解。
由于作者并非计算机专业科班出身,水平有限,并非每一题都能完整的解答,有完整解答者也不一定是最优解,现将模拟测试系统中的得分列举如下:
题目 | 得分 | 时间 | 内存 |
---|---|---|---|
归一化处理 | 100 | 15ms | 3.222MB |
寻宝!大冒险! | 100 | 15ms | 3.171MB |
角色授权 | 100 | 4.515s | 70.87MB |
光线追踪 | 100 | 1.156s | 26.80MB |
PS无限版 | 30 | TLE | 28.10MB |
基础题。
遵循题面上给出的提示即可。
时间复杂度 O ( n ) O(n) O(n)。
#include
using namespace std;
int a[10010];
int main() {
int n,s=0;
double avg,D=0.0,d;
ios::sync_with_stdio(false);
cin>>n;
for(int i=0;i<n;i++){
cin>>a[i];
s+=a[i];
}
avg=(double)s/(double)n;
for(int i=0;i<n;i++){
D+=(a[i]-avg)*(a[i]-avg);
}
D=D/(double)n;
d=sqrt(D);
for(int i=0;i<n;i++){
cout<<(a[i]-avg)/d<<endl;
}
return 0;
}
基础题。
由于藏宝图左下角必定是一颗树,因此将地图上每一颗树作为藏宝图左下角匹配。匹配时使用二重循环,循环体使用set
的find
方法判定该位置是否有树。由于set
基于红黑树实现,find
方法的时间复杂度为 O ( l o g n ) O(log n) O(logn)。考虑到使用set
存储树的位置,运算符重载是必要的。
时间复杂度 O ( n S 2 l o g n ) O(nS^2logn) O(nS2logn)。
#include
using namespace std;
struct pos{
int r,c;
pos():r(0),c(0){}
pos(int _r,int _c):r(_r),c(_c){}
void read(){cin>>r>>c;}
bool operator<(const pos &p)const {
if(r!=p.r){
return r<p.r;
}else{
return c<p.c;
}
}
pos operator+(pos p){
return pos(r+p.r,c+p.c);
}
};
class full_mat{
public:
int n;
vector<vector<bool> > dat;
void read(int m){
n=m;
dat.resize(m+1);
for(int i=m;i>=0;i--){
dat[i].resize(m+1);
for(int j=0;j<=m;j++){
int t;
cin>>t;
dat[i][j]=(t==1);
}
}
}
};
class sparse_mat{
public:
int n,L;
set<pos> trees;
void read(int _n,int _L){
n=_n;L=_L;
trees.clear();
for(int i=0;i<n;i++){
pos t;t.read();
trees.insert(t);
}
}
bool in_mat(pos p){
return (p.r>=0 && p.c>=0 && p.r<=L && p.c<=L);
}
bool match(full_mat &sub_mat,pos origin){
if(!in_mat(origin+pos(sub_mat.n,sub_mat.n))){
return false;
}
for(int i=0;i<=sub_mat.n;i++){
for(int j=0;j<=sub_mat.n;j++){
auto it=trees.find(origin+pos(i,j));
if(it==trees.end() && sub_mat.dat[i][j]){
return false;
}
if(it!=trees.end() && !sub_mat.dat[i][j]){
return false;
}
}
}
return true;
}
int count_match(full_mat &sub_mat){
int ans=0;
for(auto p:trees){
if(match(sub_mat,p)) {
ans++;
}
}
return ans;
}
};
int main() {
int n,L,s;
sparse_mat pri;
full_mat sub;
ios::sync_with_stdio(false);
cin>>n>>L>>s;
pri.read(n,L);
sub.read(s);
cout<<pri.count_match(sub)<<endl;
return 0;
}
基础题。
CSP专业组的T3通常都是这种“阅读理解”题,难度不算大,一定要耐心审题。本题适合练习面向对象的程序设计思想。有几个要点要注意:
不要暴力枚举,很容易就超时了;
角色的存储建议使用map
,这样查找角色的时间复杂度减至log
级别;
角色关联的存储则也可以使用map
,但是不要用角色作为关键字,因为我们查询时使用用户名或用户组,这样查询的复杂度也是log
级别;
仿照CSS,我们可以将用户名和用户组统一起来,在用户名前加#
,在用户组前加.
,譬如.usergroup
或#user
,这样可以将用户名和用户组统一用一个字符串表示;
#include
using namespace std;
struct oper{
string op_name,res_type,res_name;
void read(){
cin>>op_name>>res_type>>res_name;
}
};
class __role{
public:
set<string> ops;bool any_op;
set<string> res_types;bool any_res_type;
set<string> res_names;bool any_res_name;
void __read(){
int nv,no,nn;
ops.clear();cin>>nv;any_op=false;
for(int i=0;i<nv;i++){
string s;cin>>s;
ops.insert(s);
if(s=="*") any_op=true;
}
res_types.clear();cin>>no;any_res_type=false;
for(int i=0;i<no;i++){
string s;cin>>s;
res_types.insert(s);
if(s=="*") any_res_type=true;
}
res_names.clear();cin>>nn;any_res_name=false;
for(int i=0;i<nn;i++){
string s;cin>>s;
res_names.insert(s);
}
any_res_type=(res_names.size()==0);
}
bool can_perform_op(oper op){
if(!any_op && ops.find(op.op_name)==ops.end()) return false;
if(!any_res_type && res_types.find(op.res_type)==res_types.end()) return false;
if(!any_res_name && res_names.find(op.res_name)==res_names.end()) return false;
return true;
}
};
typedef pair<string,__role> role;
role create_role(){
role ret;
cin>>ret.first;
ret.second.__read();
return ret;
}
typedef pair<string,set<string> > auth_item; //{role_name: ex-usernames}
auth_item create_auth_item(){
auth_item ret;
int ns; ret.second.clear();
cin>>ret.first>>ns;
for(int i=0;i<ns;i++){
string type,name;
cin>>type>>name;
if(type=="u") {
ret.second.insert("#"+name);
}else{
ret.second.insert("."+name);
}
}
return ret;
}
class user{
public:
string name;
set<string> groups;
void read(){
int ng;
cin>>name>>ng;
groups.clear();
for(int i=0;i<ng;i++){
string s;cin>>s;
groups.insert(s);
}
}
};
//角色关联反向存储表
class auth_rtable{
private:
void set_merge(set<string> &dst,set<string> &src){
for(auto e:src){
dst.insert(e);
}
}
public:
map<string,set<string> > rmp; //{ex-username:roles}
void add_auth_item(auth_item &itm){
for(auto uname:itm.second){
auto it=rmp.find(uname);
if(it==rmp.end()){
set<string> ns;
ns.insert(itm.first);
rmp.insert(make_pair(uname,ns));
}else{
it->second.insert(itm.first);
}
}
}
set<string> get_user_all_role_name(user &usr){
set<string> ret;
auto it=rmp.find("#"+usr.name);
if(it!=rmp.end()) {
set_merge(ret,it->second);
}
for(auto grp:usr.groups){
auto it=rmp.find("."+usr.name);
if(it!=rmp.end()) {
set_merge(ret,it->second);
}
}
}
};
class database{
public:
map<string,__role> roles;
auth_rtable auth;
void read(int n,int m){
roles.clear();
for(int i=0;i<n;i++){
roles.insert(create_role());
}
for(int i=0;i<m;i++){
auth_item ai=create_auth_item();
auth.add_auth_item(ai);
}
}
bool authenticate(user &usr,oper &op){
auto all_role_name=auth.get_user_all_role_name(usr);
for(auto r:all_role_name){
if(roles[r].can_perform_op(op)) return true;
}
return false;
}
};
int main(){
ios::sync_with_stdio(false);
database db;
int n,m,q;
cin>>n>>m>>q;
db.read(n,m);
while(q--){
user usr;oper op;
usr.read();op.read();
cout<<(db.authenticate(usr,op)?1:0)<<endl;
}
return 0;
}
注:该程序在查询时(db.authenticate
)将用户对应的所有角色都列出来了,这是没有必要的,可以再优化。
中等题。
容易看出:
1.光线总是在平行于坐标轴的方向上传播;
2.反射点的坐标总是整数;
3.反射次数至多为 l o g 0.8 1 0 − 9 ≈ 93 log_{0.8}10^{-9}\approx 93 log0.810−9≈93次,记反射次数为 h h h;
本题最精巧之处即是“所有反射面的 ∣ x 1 − x 2 ∣ |x_1-x_2| ∣x1−x2∣之和不超过 3 × 1 0 5 3\times 10^5 3×105”这个条件,这意味着,反射点的数量不会超过个 3 × 1 0 5 3\times 10^5 3×105, 此即解题关键。
假设我们现有点 P ( x 0 , y 0 ) P(x_0,y_0) P(x0,y0),和方向 d = 0 d=0 d=0( x x x增加方向),那么我们需要找出直线 y = y 0 y=y_0 y=y0上横坐标比 x 0 x_0 x0大的第一个点。
为了解决上述问题,我们可以设计一个字典:其键为点的纵坐标 y 0 y_0 y0,键值为一个排好序的数组,存储 y = y 0 y=y_0 y=y0上所有反射点的横坐标,例如:
Key | Value | 实际的点 |
---|---|---|
2 | -1,3 | (-1,2)(3,2) |
3 | 1,2,4 | (1,3)(2,3)(4,3) |
当方向 d d d是 y y y轴的方向时,我们还需要一个上述的字典:其键为点的横坐标 x 0 x_0 x0,键值为一个排好序的数组,存储 x = x 0 x=x_0 x=x0上所有反射点的纵坐标。
在C++中使用map
可以方便查找 y = y 0 y=y_0 y=y0键的键值,并将键值中的横坐标都排好序,从而实现上述功能。map
和set
动态修改(insert
和erase
)和查询(lower_bound
和upper_bound
,即二分查找)的时间复杂度均是log级别的,速度较快。这意味着,每次进行3操作的时间复杂度不高于 O ( h log 2 m ) O(h\log^2m) O(hlog2m);1操作则相当于将镜面上的整点加入字典,其时间复杂度不高于 O ( ∣ x 2 − x 1 ∣ log 2 m ) O(|x_2-x_1|\log^2m) O(∣x2−x1∣log2m)。
再考虑2操作。相当于把对应点从字典中删除,即1的逆操作。其时间复杂度也不高于 O ( ∣ x 2 − x 1 ∣ log 2 m ) O(|x_2-x_1|\log^2m) O(∣x2−x1∣log2m)。
总的时间复杂度不会超过 O ( ( h + Σ x ) log 2 m ) O((h+\Sigma x) \log^2m) O((h+Σx)log2m)。
事实上,下面的代码使用map
存储所有的反射点。内层的map
#include
#include
#include
using namespace std;
enum class LightDir {
X_POS = 0,
Y_POS = 1,
X_NEG = 2,
Y_NEG = 3
};
int dx[] = { 1,0,-1,0 }, dy[] = {0,1,0,-1};
int sgn(int x) {
if (x > 0) return 1;
if (x < 0) return -1;
return 0;
}
struct point {
int x, y;
bool operator==(const point& p) const {
return x == p.x && y == p.y;
}
};
typedef pair<point, int> point_with_int;
const point NULL_POINT = { 2e9, 2e9 };
const point_with_int NULL_PD = { NULL_POINT, -1 };
struct mirror {
int x1, y1, x2, y2, k, id;
double a;
void read(int id) {
cin >> x1 >> y1 >> x2 >> y2 >> a;
if (x1 > x2) {
swap(x1, x2); swap(y1, y2);
}
k = (y2 - y1) / (x2 - x1);
this->id = id;
}
}mirrors[200001];
class space {
/*
* psx = 所有镜面整点按x方向排序;
* key存放所有点的y值,value递增存储对应x坐标和倾斜方向
* psy = 所有镜面整点按y方向排序;
* key存放所有点的x值,value递增存储对应y坐标和倾斜方向
*/
map<int, map<int,int> > psx, psy;
void add_point(int x, int y, int id) {
auto it = psx.find(y);
if (it == psx.end()) {
map<int, int> add;
add.insert({ x,id }); psx.insert({ y, add });
}
else {
it->second.insert({ x,id });
}
it = psy.find(x);
if (it == psy.end()) {
map<int, int> add;
add.insert({ y,id }); psy.insert({ x, add });
}
else {
it->second.insert({ y,id });
}
}
void del_point(int x, int y) {
psx[y].erase(x);
psy[x].erase(y);
}
public:
void add_mirror(mirror m) {
for (int x = m.x1 + 1, y = m.y1 + m.k; x < m.x2; x++, y += m.k) {
add_point(x, y, m.id);
}
}
void del_mirror(mirror m) {
for (int x = m.x1 + 1, y = m.y1 + m.k; x < m.x2; x++, y += m.k) {
del_point(x, y);
}
}
//返回反射点和反射面编号
point_with_int find_nearst_reflect_point(point p, LightDir d) {
if (d == LightDir::X_POS || d == LightDir::X_NEG) {
auto it = psx.find(p.y);
if (it == psx.end()) return NULL_PD;
map<int, int>::iterator it2;
if (d == LightDir::X_POS) {
it2 = it->second.upper_bound(p.x);
if (it2 == it->second.end()) return NULL_PD;
}
else {
it2 = it->second.lower_bound(p.x);
if (it2 == it->second.begin()) return NULL_PD;
--it2;//技巧:lower_bound的前一个就是第一个比p.x小的数
}
return { {it2->first,p.y}, it2->second };
}
else {
auto it = psy.find(p.x);
if (it == psy.end()) return NULL_PD;
map<int, int>::iterator it2;
if (d == LightDir::Y_POS) {
it2 = it->second.upper_bound(p.y);
if (it2 == it->second.end()) return NULL_PD;
}
else {
it2 = it->second.lower_bound(p.y);
if (it2 == it->second.begin()) return NULL_PD;
--it2;
}
return { {p.x,it2->first}, it2->second };
}
}
}instance;
LightDir next_dir(LightDir dir, int mirror_k) {
if (dir == LightDir::X_POS) {
return mirror_k == 1 ? LightDir::Y_POS : LightDir::Y_NEG;
}
else if (dir == LightDir::X_NEG) {
return mirror_k == -1 ? LightDir::Y_POS : LightDir::Y_NEG;
}
else if (dir == LightDir::Y_POS) {
return mirror_k == 1 ? LightDir::X_POS : LightDir::X_NEG;
}
else {//(dir == LightDir::Y_NEG)
return mirror_k == -1 ? LightDir::X_POS : LightDir::X_NEG;
}
}
pair<point, int> test_source(point p, LightDir d, double I, int T) {
do {
point_with_int ret = instance.find_nearst_reflect_point(p, d);
point p2 = ret.first; int id = ret.second;
int dist = abs(p.x - p2.x) + abs(p.y - p2.y);
if (p2 == NULL_POINT || dist > T) {
p.x += dx[(int)d] * T;
p.y += dy[(int)d] * T;
break;
}
p = p2;
d = next_dir(d, mirrors[id].k);
I = I * mirrors[id].a;
if (I < 1.0) {
return { {0,0},0 };
}
T -= dist;
} while (T > 0);
return { p,(int)I };
}
int main() {
ios::sync_with_stdio(false);
int m;
cin >> m;
for (int i = 1; i <= m; i++) {
int op; cin >> op;
if (op == 1) {
mirrors[i].read(i);
instance.add_mirror(mirrors[i]);
}
else if (op == 2) {
int k; cin >> k;
instance.del_mirror(mirrors[k]);
}
else {
int x, y, d, t;
double I;
cin >> x >> y >> d >> I >> t;
auto ans = test_source({ x,y }, (LightDir)d, I, t);
cout << ans.first.x << ' ' << ans.first.y << ' ' << ans.second << endl;
}
}
return 0;
}
中等题。
对于前30%的分数,我们每次操作直接遍历 [ l , r ] [l,r] [l,r]即可。
对于后70%的分数,我们可以构建一个线段树,维护每一段的和与平方之和。维护和是容易的,而维护平方之和的方法我还没有想到,望有大佬多多指教。
记投影直线为 y = k x + b y=kx+b y=kx+b,待投影点为 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),则投影点 ( x 1 , y 1 ) (x_1,y_1) (x1,y1)满足 { y = k x + b k ( y − y 0 ) = − ( x − x 0 ) \left\{\begin{array}{c} y=kx+b\\ k(y-y_0)=-(x-x_0) \end{array}\right. {y=kx+bk(y−y0)=−(x−x0)
解得
{ x 1 = x 0 + k ( y 0 − b ) k 2 + 1 y 1 = k x 1 + b \left\{\begin{array}{c} x_1=\frac{x_0+k(y_0-b)}{k^2+1}\\ y_1=kx_1+b \end{array}\right. {x1=k2+1x0+k(y0−b)y1=kx1+b
有直线为 l : y = k x + b l:y=kx+b l:y=kx+b,线外一点 P ( x 0 , y 0 ) P(x_0,y_0) P(x0,y0), P P P在 l l l上的投影点为 M ( x 1 , y 1 ) M(x_1,y_1) M(x1,y1),则 P P P关于 l l l的对称点为 Q ( 2 x 1 − x 0 , 2 y 1 − y 0 ) Q(2x_1-x_0,2y_1-y_0) Q(2x1−x0,2y1−y0)。