第40次CSP认证前四题
总览
本次CSP认证的考试难度总体中等,主要是题目的信息量和代码量很大,对代码基本功有一定要求,一些经典算法部分考察不多,但是还是没有跳脱出经典算法的思想和方法论,并且很多地方都是我们的培训(包括之前的培训)讲过的,建议大家如果以后还要参加培训,一定要仔细学习我们准备的题目的算法并且思考背后逻辑和本质,平时就写一些代码和做一些简单的算法题保持”码力“,加强数学能力!
第一题
题目解读:
1.判断的标准:小C和实际的判断是否相同,所以大致思路就是:先处理实际集合是否相同,再处理小C公式得到的结果,对比两者,相同输出correct,反之输出wrong;
2.输入:首先注意,是先输出所有的集合S,再输入所有的集合T,也就是并不是S1,T1,S2,T2,是S1,S2,T1,T2,所以不能按照多测来做,应该全部读入之后储存起来再处理,其次看到集合,有同学就想用set,这里没必要,因为输入的集合保证有序且不重,使用set只会带来O(logn)的额外时间复杂度,使用二维vector即可;
代码
如上文叙述,使用两个二维动态数组分别储存S和T:
const int N = 1e4+10, MAX = 1e9, INF = -1e9 ,M = 1e3 + 10;
int n, m;
int a[N];
vector<vector<int>> s(M);
vector<vector<int>> t(M);
读入的时候使用push_back来读入,s[i].size()就是第i个S集合的大小:
for(int i = 1; i <= 2 * m ; i ++ ){
int num;
cin >> num;
while(num --){
int e;
cin >> e;
if(i <= m)s[i].pb(e);
else t[i - m].pb(e);
}
}
接着我们来写实际集合的判断,逻辑为:
如果大小不同,则不同
进一步,如果任意一对元素不同,则不同
bool f1(int i){
if(s[i].size() != t[i].size()){
return false;
}
for(int j = 0; j < s[i].size(); j ++ ){
if(s[i][j] != t[i][j]){
return false;
}
}
//如果代码进行到这里,表明相同
return true;
}
接着我们来写小C的判断,按照公式即可:
bool f2(int i){
int x = 0, y = 0;//初始化为0,任何数字和0异或都得本身
for(int j = 0; j < s[i].size() ; j ++ ){
x ^= a[s[i][j]];
}
//这里需要分开来,S和T长度有可能不同
for(int j = 0; j < t[i].size() ; j ++ ){
y ^= a[t[i][j]];
}
return x == y;
}
最后就是判断部分:
for(int i = 1; i <= m ; i ++ ){
if(f1(i) == f2(i))cout << "correct" << endl;
else cout << "wrong" << endl;
}
注意到异或就是相同为0不同为1,我们也可以直接利用异或来判断,题目用异或来难我们,我们反过来用异或反将题目一军(虽然没有任何实际作用)
for(int i = 1; i <= m ; i ++ ){
//f1和f2不同为1(true),相同为0(false)
cout <<( f1(i) ^ f2(i) ? "wrong" : "correct")<< endl;
}
第二题
题目解读:
1.题目描述过程:不管f和g函数具体是什么,我们先弄懂题目在描述一个什么过程,给定一个输入f0带入函数g(f0,k1),将结果记作f1,再代入g(f1,k2),以此类推,计算m次,得到最后的结果;
2.g函数的实现:同样的,不管f函数是什么,这个函数就是取高三位,中三位,低三位作为a,b和c,再按照公式计算之后得到新的3个数,按照顺序复原到对应的位置上去,怎么取二进制下的某连续的几位呢?回忆怎么取十进制数的某连续几位,如果我们要获得十进制数下的十百位,我们先将该书除以10,消除了个位,再对100取模,就得到了十,百为的数字,这里也是一样,还可以使用高效的位运算;反过来看f函数,带公式就可以
3.枚举:题目并不是求正向结果,而是告知结果,反求输入,怎么做?要求逆变换吗?不用,题目已经明显告诉你了,枚举所有输入,一个一个找就好,这看似是一个善意的提醒,但是其实封锁了你的优化思路;
代码
f函数的实现,代公式即可:
int f(int x, int k){
return ((x * x + k * k) % 8 ) ^ k;
}
g函数的实现:
int g(int x, int k){
int a = (x / 64) % 8;//除以64(2的6次方),消除后6位,对8取模,得到后3位
int b = (x / 8) % 8;//除以8(2的3次方),消除后3位,对8取模,得到后3位
int c = x % 8;//直接对8取模,得到后3位
return ((b * 64) + ((c ^ f(b, k)) * 8) + (a ^ f(c, k)));//乘64/8,前移6/3位,相加即可
}
使用位运算更加简便易懂:
int g(int x, int k){
//7二进制是111,与7做和运算就是得到低三位
int a = (x >> 6) & 7;
int b = (x >> 3) & 7;
int c = x & 7;
return ((b << 6) + ((c ^ f(b, k)) << 3) + (a ^ f(c, k)));
}
枚举:
for(int i = 0; i < 512; i ++ ){//2的9次方就是512
int e = i;
//m次迭代,当然用一个数组也可以
for(int j = 1; j <= m ; j ++ ){
e = g(e, k[j]);
}
//找到输入
if(e == a){
cout << i << endl;
return ;
}
}
不难发现,有n个询问,每次询问两个时间复杂度,f和g是常数运算,所以时间复杂度为:O(512nm),最大运算次数达到了2的11次方,对于所有数据肯定超时,这时候我们反过来看看代码的运行过程,不难发现,每次询问我们都在做同样的循环,一模一样的操作我们一直在重复做,我们为什么不在询问之前提前做一遍,把所有答案都记下来,然后每次询问我们直接输出呢?
int ans[N];//ans[i]就是当输出为i的时候,对应的输入为ans[i]
void init(){
cin >> n >> m;
for(int i = 1; i <= m ; i ++ ){
cin >> k[i];
}
for(int i = 0; i < 512; i ++ ){
int e = i;
for(int j = 1; j <= m ; j ++ ){
e = g(e, k[j]);
}
ans[e] = i;
}
}
这样的话每次的询问直接输出答案即可:
cin >> a;
cout << ans[a] << endl;
时间复杂度:O(n)
这就是为什么之前说题目的温馨提醒是对你优化思路的封闭,不看这个提示,说不定你能直接想到这样做,拿到满分
第三题
题目解读
第三题给出两种操作:
旋转操作,是这道题比较难的部分
每次操作是先顺时针旋转一个小矩阵(给出小矩阵的左上角坐标u,v,矩阵大小L和旋转角度d),再逆时针旋转整个大矩阵(给出旋转次数r)
接下来是翻转操作,容易实现:
每次操作是都是一个简单翻转(给出翻转左上角坐标u,d,矩阵右下角坐标l,r以及上下和左右翻转的标志o)
题目很简单,核心操作就是给出一些上述操作之后的结果矩阵,找到最初的矩阵,而这些操作的逆操作也很好找到,按部就班实现就好
注意:序列是正向序列,我们复原需要反向复原
代码
开一个结构体,储存操作的内容:
typedef struct action{
int op;
int u, v, l, d, r, o;
}P;
P p[N];
对应的读入操作:(逆向读入)
for(int i = n; i >= 1; i -- ){//逆向读入
cin >> p[i].op;
//需要什么读什么就好,不用大分类
if(p[i].op == 1){
cin >> p[i].u >> p[i].v >> p[i].l >> p[i].d >> p[i].r;
}
else{
cin >> p[i].u >> p[i].d >> p[i].l >> p[i].r >> p[i].o;
}
}
实现旋转操作:
void f1(int i){
int z = (p[i].d / 90) % 4;//z就是顺时针旋转的次数(有循环,对4取模)
swap1(1, 1, m, p[i].r % 4);
swap1(p[i].u, p[i].v, p[i].l,4 - z);
return ;
}
swap1函数就是翻转函数,注意操作是先顺时针转小矩阵,再逆时针转大矩阵,逆操作应该是:先顺时针转大矩阵,再拟时针转小矩阵,我们把swap1函数实现为顺时针旋转,那么逆时针旋转z次,就是顺时针旋转4-z次
在纸上推出旋转后的坐标变换公式,先将矩阵复制到一个临时矩阵中,旋转临时矩阵,最后复制回去,这样做既防止旋转时新数据覆盖掉旧数据,也方便旋转公式,最后函数如下:
void swap1(int x0, int y0, int l, int z){
//转90°,一次
if (z == 1){
for(int i = 1; i <= l ; i ++ ){
for(int j = 1 ; j <= l; j ++ ){
t[j][l - i + 1] = g[x0 + i - 1][y0 + j - 1];
}
}
}
//转180°,两次
else if(z == 2){
for(int i = 1; i <= l ; i ++ ){
for(int j = 1 ; j <= l; j ++ ){
t[l - i + 1][l - j + 1] = g[x0 + i - 1][y0 + j - 1];
}
}
}
//转270°,三次
else if(z == 3){
for(int i = 1; i <= l ; i ++ ){
for(int j = 1 ; j <= l; j ++ ){
t[l - j + 1][i] = g[x0 + i - 1][y0 + j - 1];
}
}
}
//4次0次都是不变,最后复制回去
for(int i = 1; i <= l ; i ++ ){
for(int j = 1 ; j <= l; j ++ ){
g[x0 + i - 1][y0 + j - 1] = t[i][j];
}
}
}
这个函数也是第三题的难点,坐标的变换和临时矩阵的细节很多
接下来实现翻转操作:
void f2(int i){
swap2(p[i].u, p[i].l, p[i].d, p[i].r, p[i].o);
return ;
}
为了整体代码风格统一,我们也多写一层swap2具体实现,当然直接实现也可以,题目中给的上边界,下边界,左边界和有边界就是四个顶点的坐标,swap2实现如下:
void swap2(int x0, int y0, int x1, int y1, int z){
if(z == 1){
while(x0 < x1){
for(int i = y0; i <= y1; i ++ ){
swap(g[x0][i], g[x1][i]);
}
x0 ++ ; x1 -- ;
}
}
else{
while(y0 < y1){
for(int i = x0; i <= x1; i ++ ){
swap(g[i][y0], g[i][y1]);
}
y0 ++ ; y1 -- ;
}
}
}
最后就是调用函数操作和最后的处理输出了:
//调用函数操作
for(int i = 1; i <= n ; i ++ ){
if(p[i].op == 1) f1(i);
else f2(i);
}
//最后的处理,找到不是?的区域输出
int x = -1, y = -1;
for(int i = 1; i <= m ; i ++ ){
if(g[1][i] != '?')y = i;
if(g[i][1] != '?')x = i;
}
cout << x << " " << y << endl;
for(int i = 1; i <= x; i ++ ){
for(int j = 1; j <= y ; j ++ ){
cout << g[i][j];
}
cout << endl;
}
根据很多大佬的讲述,这个题目的数据比较水,这样直接的模拟就以拿到满分,是近几年以来最简单的第三题
第四题
题目解读
这个题目我们要想拿下满分是很难的,需要注意到积性函数最后使用线性筛和快速幂,只要注意模操作,我们不需要拿满分,我们只偷分,观察子任务:
子任务一的数据很小,所以我们可以使用很暴力的算法来做,同时因为数据很小,所以答案不需要取模,这个时候这个题目就变成了纯数学题,在纸上推式子,找规律:
题目大致为:有七个正整数A,B,C,D,E,F,G满足:
先考虑op == 0的情况,即特殊无要求,求在B不大于N的条件下,所有满足这三个式子的D的和是多少,将式子先做转换:
(2.1)和(2.2)带入(2.3)得到:
进一步使用立方差公式:
即:
此时我们令:
即可得到剩余数:
这样,题目就变成了求所有n满足(4.1)(4.2)和(4.3)的D的和,这下思路就清晰了:枚举x,y加和所有的D,进一步,由于CGD都是正整数,所以x和y都一定是n三次方的因数,所以做法就是枚举n三次方的所有因数,二重循环枚举所有组合计算D加和就好!
这里还有一个坑:还要保证C和G是正整数,所以枚举的范围不是三次方,是平方,最后的算法:
枚举n三次方的所有不超过n的平方因数,二重循环枚举所有组合计算D加和
那op == 1呢?要求有6个不重复的数字,那么开一个set去判断就好
30分代码
sum函数计算对于具体某一个n计算所有的D
int sum(int m){
int res = 0;
//找到所有不超过n^2的因数
for(int i = 1; i <= m * m; i ++ ){
if(m * m * m % i == 0 )v. pb(i);
}
//枚举所有组合
for(auto i : v){
for(auto j : v){
if(op == 1){
if(check(m, i, j))res += m * m * m / (i * j);
}
else{
res += m * m * m / (i * j);//累加D
}
}
}
//记得清除v,以便下次使用
v.clear();
return res;
}
check()函数就用set实现:
bool check(int m, int i, int j){
//将所有的七个数放到set中
st.insert(i); st.insert(m); st.insert(m * m / i);
st.insert(m * m * m / (i * j));
st.insert(j); st.insert(m * m / j);//B和F相同,放一次就好
int num = st.size();
st.clear();
//返回是否满足6种
return num == 6;
}
最后就是solve函数:
void solve(){
cin >> op >> n;
//遍历所有小于n的B,累加价值
for(int i = 1; i <= n ; i ++ ){
ans += sum(i);
}
cout << ans << endl;
return ;
}
总结
本次认证的题目前三题偏简单,后两题难度很大,而前三题的题目信息很多,模拟程序复杂,导致很多同学在考场上没有思路,所以本次考试的分数很低:

浙公网安备 33010602011771号