【题解】CSP2022-J第二轮题解

概述 & 前言

看到许多评价,说这次 CSP-J \texttt{CSP-J} CSP-J 的比赛题目质量甚至不如你谷的模拟赛,再加上 whk 实在落下太多了,所以就没打算补题。但是学校的老师非让我补不可,说要决定一下以后课程的安排。于是,就补了。于是,就 AK 了。

总体来讲,我感觉这次比赛的题目质量虽然不算很好,但也说的过去。除了第二题裸的一元二次方程差评外,其它的题目出的还是挺好的。(第一题直接 A B A^B AB 是不是有点过水?)

主观的难度评价(满分 5 5 5,相对与 PJ 其它题目来说)如下:

题目思维难度细节处理难度代码复杂度
A. 乘方111
B. 解密111
C. 逻辑表达式343
D. 上升点列422

A. 乘方

题目链接:洛谷 P8813 [CSP-J 2022] 乘方

使用 暴力/快速幂 a b a^b ab 即可。值得一提的是,这里使用暴力并不会超时。因为最坏情况底数为 2 2 2 的话,也只需要乘 30 30 30 次就会超过 1 0 9 10^9 109。但是实际运行下来,快速幂比暴力要快个几十毫秒,所以就贴快速幂的代码好了。

#include <iostream>
using LL = long long;
const LL MOD = 1e9;
LL n, m;
LL qpow(LL a, LL b) {
    LL ans = 1;
    for (; b; b >>= 1) {
        if (a > MOD) return -1;
        if (b & 1) {
            if (ans*a > MOD)
                return -1;
            ans = ans * a;
        }
        a *= a;
    }
    return ans;
}
int main() {
    scanf("%lld%lld", &n, &m);
    printf("%lld", qpow(n, m));
    return 0;
}

B. 解密

题目链接:P8814 [CSP-J 2022] 解密

题意太清晰了,一眼就能看出是一道数学题。

推导过程如下:
{ n = p × q   ① e × d = ( p − 1 ) ( q − 1 ) + 1   ② \begin{cases} n = p \times q \ \ ①\\ e \times d = (p-1)(q-1) + 1 \ \ ② \end{cases} {n=p×q  e×d=(p1)(q1)+1  
化简 ② ② 式,得:
e d = p q − p − q + 2   ③ ed = pq - p - q + 2 \ \ ③ ed=pqpq+2  
① ① 代入 ③ ③ 中,得:
e d = n − p − q + 2 ed = n - p - q + 2 ed=npq+2
变形后得:
p + q = n − e d + 2 p + q = n - ed + 2 p+q=ned+2
按照题目「数据范围」中的字母,我们将 n − e d + 2 n - ed + 2 ned+2 替换为 m m m,即
p + q = m   ④ p + q = m \ \ ④ p+q=m  
代入 ① ①
p ( m − p ) = n p(m-p) = n p(mp)=n
展开并整理为关于 p p p 的一元二次方程:
p 2 − m p + n = 0 p^2 - mp + n = 0 p2mp+n=0
这个方程有解,当且仅当 Δ \Delta Δ 为完全平方数。即:
Δ = b 2 − 4 a c = m 2 − 4 n \Delta = b^2 - 4ac = m^2 - 4n Δ=b24ac=m24n
所以原方程的解为:
p = m ± m 2 − 4 n 2 p = \frac{m\pm \sqrt{m^2 - 4n}}{2} p=2m±m24n
由于 p , q p,q p,q 必须要是整数,所以当 m ± m 2 − 4 n m\pm \sqrt{m^2 - 4n} m±m24n 为奇数时也应该输出 NO

需要注意的是 n n n 的取值范围是 n ≤ 1 0 18 n\leq 10^{18} n1018,注意开 long long

#include <iostream>
#include <cmath>
using LL = long long;
int T;
LL n, d, e;
int main() {
    scanf("%d", &T);
    while (T--) {
        scanf("%lld%lld%lld", &n, &d, &e);
        LL m = n + 2 - e*d;
        LL delta = m*m - 4*n;
        if (delta < 0) {
            puts("NO");
            continue;
        }
        LL x = sqrt(delta);
        if ((double)x!=sqrt(delta) or ((m-x)&1)) {
            puts("NO");
            continue;
        }
        printf("%lld %lld\n", (m-x)/2, (m+x)/2);
    }
    return 0;
}

C. 逻辑表达式

题目链接:洛谷 P8815 [CSP-J 2022] 逻辑表达式

这道题呢,说简单也不简单,说难呢也没啥难的,思路十分好想,但是实现起来有一定难度。

本题的思路很简单, [ 1 , n ] [1,n] [1,n] 的值,就是 [ 1 , p − 1 ] [1,p-1] [1,p1] 的值与 [ p + 1 , n ] [p+1,n] [p+1,n] 的值运算后的结果,其中 p p p [ 1 , n ] [1,n] [1,n] 括号外第一个逻辑运算符的下标。不难发现这就是一个递归的过程,所以直接模拟上述操作即可。

根据这个思路,我们不难设计出递归的参数: s o l v e ( l , r , c ) solve(l,r,c) solve(l,r,c) 代表当前递归要计算 [ l , r ] [l,r] [l,r] 的值,并且以操作符 c c c 作为这一轮递归的分界点。

现在的问题是每一层递归中应该干什么事情。由于「或」的优先级比「与」低,所以我们应该先找到 [ l , r ] [l,r] [l,r] 中第一个或运算符的位置(为表示方便,下文将其用 p c p_c pc 代替),这样 [ l , r ] [l,r] [l,r] 就可以被分割为 [ l , p c − 1 ] [l,p_c-1] [l,pc1] [ p c + 1 , r ] [p_c+1,r] [pc+1,r] 这两个部分。由于找的是或运算符的位置,所以从 [ l , p c − 1 ] [l,p_c-1] [l,pc1] 中括号外的运算符一定全部为「与」。计算出 [ l , p c − 1 ] [l,p_c-1] [l,pc1] 的值(即 s o l v e ( l , p c − 1 , ′ & ′ ) solve(l,p_c-1,'\&') solve(l,pc1,&) )即可。

这样描述实在是太抽象了,再加上我表达能力有限,可能一些原本会做的同学都被我整不会了。所以我画了几张图,可能会更清晰一点。同时,在模拟样例的过程中,我们还可以发现一些前面没有提到的细节。

痛苦的模拟

这张图已经说明了一切。

这里再补充一点。由于每次我们要跳过括号内的内容,所以我们要知道每一个括号与谁配对。如果每次都遍历字符串来找括号的话,那时间复杂度为 O ( N ) \mathcal{O}(N) O(N),妥妥的超时。所以我们可以先预处理出与每个左括号配对的右括号的下标,然后用 O ( 1 ) \mathcal{O}(1) O(1) 的时间复杂度找到下标。

#include <iostream>
#include <cstring>
const int N = 1e6 + 9;
char a[N];
int n, cntAnd, cntOr, nxt[N];
int tp, stk[N];
// nxt[i] 表示第 i 个位置的左括号匹配的右括号所在的下标是几
int find(int pos, int r, char c) {
    while (a[pos]!=c and pos<=r)
        if (a[pos] == '(') {
            pos = nxt[pos] + 1;
        } else {
            pos++;
        }
    return pos;
}
int solve(int l, int r, char tgt = '|') {
    while (nxt[l] == r) l++, r--, tgt = '|';
    if (l == r) return a[l] == '1';
    int p = find(l, r, tgt);
    int res = solve(l, p-1, '&');
    for (int t; p < r; p = t) {
        t = find(p+1, r, tgt);
        if (a[p]=='&' and res==0) {
            cntAnd++;
        } else if (a[p]=='|' and res) {
            cntOr++;
        } else {
            res = solve(p+1, t-1);
        }
    }
    return res;
}
int main() {
    scanf("%s", a + 1);
    n = strlen(a+1);
    for (int i = 1; i <= n; i++)
        if (a[i] == '(') stk[++tp] = i;
        else if (a[i] == ')') nxt[stk[tp--]] = i;
    printf("%d\n", solve(1, n));
    printf("%d %d\n", cntAnd, cntOr);
    return 0;
}

D. 上升点列

题目链接:P8816 [CSP-J 2022] 上升点列

很显然是一道 DP 题。一开始我想到的是 DP 每个坐标,但是这样肯定不行( 1 0 9 10^9 109),于是就想着先打骗分 DP。然后写到循环后发现我重复做了非常多重复的操作。只需要 DP 每个点就可以了。再看了一眼点数的数据范围,小的可怜。于是正解就这么想出来了。

n , k n,k n,k 的数据范围给我们了一个非常重要的提示,就是设计 DP 状态的时候可以依赖于这两个值。结合抄原题大法,很容易设计出 DP 状态:
f i , j  表示以第  i  个点结尾并且加  j  个点能构成的序列的最大长度 f_{i,j}\ 表示以第\ i\ 个点结尾并且加\ j\ 个点能构成的序列的最大长度 fi,j 表示以第 i 个点结尾并且加 j 个点能构成的序列的最大长度
然后就根据这个状态,设计一个状态转移方程:
f i , l + d i s ( i , j ) − 1 = m a x { f j , l + d i s ( i , j ) } f_{i, l+dis(i,j)-1} = max\{f_{j,l}+dis(i,j)\} fi,l+dis(i,j)1=max{fj,l+dis(i,j)}
其中, l l l 表示第 j j j 个点前加了 l l l 个点

不要问我这个方程是怎么推出来的,问就是点这里

#include <iostream>
#include <algorithm>
const int N = 509, K = 109;
struct Node {
    int x, y;
} a[N];
int n, k, ans, dp[N][K];
// f[i][j] 表示以第 i 个点结尾并且加 j 个点能构成的序列的最大长度
int dis(int p1, int p2) { return abs(a[p1].x-a[p2].x)+abs(a[p1].y-a[p2].y); }
int main() {
    scanf("%d%d", &n, &k);
    for (int i = 1; i <= n; i++)
        scanf("%d%d", &a[i].x, &a[i].y);
    std::sort(a + 1, a + n + 1, [](const Node A, const Node B) {
        return A.x==B.x ? A.y<B.y : A.x<B.x;
    });
    for (int i = 1; i <= n; i++) { // 到第 i 个点
        dp[i][0] = 1;
        for (int j = 1; j < i; j++) // 接到第 j 个点后面
            if (a[j].y <= a[i].y)
                for (int l = 0; l+dis(i,j)-1 <= k; l++) { // 第 j 个点前加了 l 个点
                    int t = l + dis(i, j) - 1;
                    dp[i][t] = std::max(dp[i][t], dp[j][l] + dis(i, j));
                }
    }
    for (int i = 1; i <= n; i++)
        for (int j = 0; j <= k; j++)
            ans = std::max(ans, dp[i][j]+k-j);
    printf("%d", ans);
    return 0;
}
  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值