CSP 202206 题解:归一化处理,寻宝大冒险,角色授权,光线追踪,PS无限版

试题内容请前往CCF官网查看:

CCF-CSP计算机软件能力认证考试
http://118.190.20.162/home.page

阅读本题解前,您应当了解下列知识:

  1. 线段树 教程
  2. 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

1 归一化处理

基础题。

遵循题面上给出的提示即可。

时间复杂度 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;
}

2 寻宝!大冒险!

基础题。

由于藏宝图左下角必定是一颗树,因此将地图上每一颗树作为藏宝图左下角匹配。匹配时使用二重循环,循环体使用setfind方法判定该位置是否有树。由于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;
}

3 角色授权

基础题。

CSP专业组的T3通常都是这种“阅读理解”题,难度不算大,一定要耐心审题。本题适合练习面向对象的程序设计思想。有几个要点要注意:

  1. 不要暴力枚举,很容易就超时了;

  2. 角色的存储建议使用map,这样查找角色的时间复杂度减至log级别;

  3. 角色关联的存储则也可以使用map,但是不要用角色作为关键字,因为我们查询时使用用户名或用户组,这样查询的复杂度也是log级别;

  4. 仿照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)将用户对应的所有角色都列出来了,这是没有必要的,可以再优化。

4 光线追踪

中等题。

容易看出:

1.光线总是在平行于坐标轴的方向上传播;

2.反射点的坐标总是整数;

3.反射次数至多为 l o g 0.8 1 0 − 9 ≈ 93 log_{0.8}10^{-9}\approx 93 log0.810993次,记反射次数为 h h h

本题最精巧之处即是“所有反射面的 ∣ x 1 − x 2 ∣ |x_1-x_2| x1x2之和不超过 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键的键值,并将键值中的横坐标都排好序,从而实现上述功能。mapset动态修改(inserterase)和查询(lower_boundupper_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(x2x1log2m)

再考虑2操作。相当于把对应点从字典中删除,即1的逆操作。其时间复杂度也不高于 O ( ∣ x 2 − x 1 ∣ log ⁡ 2 m ) O(|x_2-x_1|\log^2m) O(x2x1log2m)

总的时间复杂度不会超过 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;
}

5 PS无限版(仅部分思路)

中等题。

对于前30%的分数,我们每次操作直接遍历 [ l , r ] [l,r] [l,r]即可。

对于后70%的分数,我们可以构建一个线段树,维护每一段的平方之和。维护是容易的,而维护平方之和的方法我还没有想到,望有大佬多多指教。

附录:几种变换的推导

1. 投影

记投影直线为 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(yy0)=(xx0)
解得
{ 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(y0b)y1=kx1+b

2. 对称

有直线为 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(2x1x0,2y1y0)

你可能感兴趣的:(CSP专业组题解,c++,算法,数据结构)