第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.题目描述过程:不管fg函数具体是什么,我们先弄懂题目在描述一个什么过程,给定一个输入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个询问,每次询问两个时间复杂度,fg是常数运算,所以时间复杂度为: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满足:

\[\frac{A}{B} = \frac{B}{C} ~~~~ (1.1)\\ \frac{E}{F} = \frac{F}{G} ~~~~(1.2)\\ ABC = CDE = EFG ~~~~ (1.3) \]

先考虑op == 0的情况,即特殊无要求,求在B不大于N的条件下,所有满足这三个式子的D的和是多少,将式子先做转换:

\[AC = B^2 ~~~~ (2.1)\\ EG = F^2~~~~(2.2)\\ ABC = EFG~~~~(2.3)\\ \]

(2.1)和(2.2)带入(2.3)得到:

\[B^3 = F^3 ~~~~(3.1) \]

进一步使用立方差公式:

\[B^3 - F^3 = (B-F)(B^2 + BF + F^2 ) = 0~~~~ (3.2) \]

即:

\[B=F~~~~ (3.3) \]

此时我们令:

\[F = B= n ~~~~(4.1)\\ A = x~~~~(4.2)\\ E = y~~~~(4.3) \]

即可得到剩余数:

\[C = \frac{n^2}{x}~~~~(4.4)\\ G = \frac{n^2}{y}~~~~(4.5)\\ D = \frac{n^3}{xy}~~~~(4.6)\\ \]

这样,题目就变成了求所有n满足(4.1)(4.2)和(4.3)的D的和,这下思路就清晰了:枚举x,y加和所有的D,进一步,由于CGD都是正整数,所以xy都一定是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 ;
    }

总结

本次认证的题目前三题偏简单,后两题难度很大,而前三题的题目信息很多,模拟程序复杂,导致很多同学在考场上没有思路,所以本次考试的分数很低:

posted @ 2025-12-09 17:48  Oaths  阅读(230)  评论(0)    收藏  举报