2022年CSP-J复赛真题解析

2022年CSP-J-T1-乘方(pow)

题目描述

小文同学刚刚接触了信息学竞赛,有一天她遇到了这样一个题:给定正整数 a 和 b,求 a^b 的值是多少。a^b 即 b 个 a 相乘的值,例如 2^3 即为 3 个 2 相乘,结果为 2 × 2 × 2 = 8。

“简单!”小文心想,同时很快就写出了一份程序,可是测试时却出现了错误。小文很快意识到,她的程序里的变量都是 int 类型的。在大多数机器上,int 类型能表示的最大数为 2^31 − 1 ,因此只要计算结果超过这个数,她的程序就会出现错误。由于小文刚刚学会编程,她担心使用 int 计算会出现问题。因此她希望你在 a^b 的值超过 10^9 时,输出一个 ‐1 进行警示,否则就输出正确的 a^b 的值。然而小文还是不知道怎么实现这份程序,因此她想请你帮忙。

输入格式

从文件 pow.in 中读入数据。

输入共一行,两个正整数 a, b 。

输出格式

输出到文件 pow.out 中。

输出共一行,如果 a^b 的值不超过 10^9 ,则输出 a^b 的值,否则输出 ‐1 。

输入输出样例

输入样例1:

10 9

输出样例1:

1000000000

输入样例2:

23333 66666

输出样例2:

-1

说明

【数据范围】

对于 10% 的数据,保证 b = 1。

对于 30% 的数据,保证 b ≤ 2。

对于 60% 的数据,保证 b ≤ 30,a^b ≤ 10^18。

对于 100% 的数据,保证 1 ≤ a, b ≤ 10^9。

耗时限制1000ms  内存限制128MB

解析
考点:模拟,计算。
#include 
using namespace std;
const int INF = 1e9;
//ans:标记 a 的 b 次方的结果
long long a,b,ans = 1;
int main(){
	 cin>>a>>b;
	 for(int i = 1;i <= b;i++){
		 ans = ans * a;
		 if(ans > INF){
			 cout<<-1;
			 return 0;
		 }
	 }
	 cout<

2022年CSP-J-T2-解密(decode)

题目描述

给定一个正整数 k,有 k 次询问,每次给定三个正整数 ni, ei, di,求两个正整数 pi, qi, 使 ni = pi × qi, ei × di = (pi − 1)(qi − 1) + 1。

输入格式

从文件 decode.in 中读入数据。

第一行一个正整数 k,表示有 k 次询问。

接下来 k 行,第 i 行三个正整数 ni, di, ei。

输出格式

输出到文件 decode.out 中。

输出 k 行,每行两个正整数 pi, qi 表示答案。为使输出统一,你应当保证 pi ≤ qi。

如果无解,请输出 NO。

输入输出样例

输入样例1:
10
770 77 5
633 1 211
545 1 499
683 3 227
858 3 257
723 37 13
572 26 11
867 17 17
829 3 263
528 4 109
输出样例1:
2 385
NO
NO
NO
11 78
3 241
2 286
NO
NO
6 88

说明

2022年CSP-J复赛真题解析_第1张图片

耗时限制1000ms   内存限制128MB

解析
考点:数学,二分查找

1.数学做法

分析:

n = p * qe * d = (p-1)(q-1) + 1

 e * d = pq  (p+q) + 2

 p + q = pq  ed + 2 = n  ed + 2

 m = n – ed + 2,则有:

p * q = n

p + q = m

那么本题就是分别解出 p 的值。

 (p + q)^2 = m^2

 p^2 + 2pq + q^2 = m^2

 p^2 - 2pq + q^2 + 4pq = m^2

 (p-q)2 + 4pq = m2

由于: pq = n

 (p-q)^2 = m^2  4n

 p-q = ±sqrt(m^2  4n)

由于正负号仅影响最终 pq 的顺序,因此可以认为: p-q = sqrt(m2 – 4n) 结合: p + q = m

可以得出: p = (sqrt(m^2 – 4n)+m) / 2

q = m - p

由于 pq 都是正整数,因此只需要再检验 sqrt(m^2 – 4n)是否为整数,就可以确认是否存在 整数解。

参考代码:

#include 
using namespace std;
/*
给定 n,e,d,求出 pq,使得 pq 满足:
pq = n
(p-1)(q-1)+1=ed
*/
long long m,n,k,p,q,e,d,t;
int main(){
     scanf("%lld",&k);
     while(k--){
         scanf("%lld%lld%lld",&n,&e,&d);
         m = n - e * d + 2;
         t = sqrt(m*m-4*n);
         //如果有解,sqrt()的结果一定是整数
         if(t * t == m * m - 4 * n){
             p = (t + m) / 2;
             q = m - p;
             printf("%lld %lld\n",min(p,q),max(p,q));
         }else{
            puts("NO");
         }
     }
     return 0;
}

二分做法:

2022年CSP-J复赛真题解析_第2张图片

p≤m/2

分析,易得:p∗q 在 p∈[1,m/2] 上,单调递增,因此可以对p 进行二分查找。

参考代码

#include 
using namespace std;
long long n, k, e, d, m, p, q;
int main(){    
    cin >> k;
    while(k--){
        cin >> n >> d >> e;
        long long m = n - e*d +2;
        long long l = 1, r = m/2, p, q;  // p <= q  所以 p <= m/2
        while(l <= r) {
            p = (l + r) / 2;
            q = m - p;
            if(n == p*q) {  // 在 p 的值域范围内,p*q 是单调递增的
                break;
            } else if(n < p*q) {
                r = p - 1;
            } else {
                l = p + 1;
            }
        }
        if(n == p*q) {
            cout << p << ' ' << q << '\n';
        } else {
            cout << "NO\n";
        }
    }
    return 0;
}

2022年CSP-J-T3-逻辑表达式(expr)

题目描述

   逻辑表达式是计算机科学中的重要概念和工具,包含逻辑值、逻辑运算、逻辑运算优先级等内容。在一个逻辑表达式中,元素的值只有两种可能:0 (表示假)和 1 (表示真)。元素之间有多种可能的逻辑运算,本题中只需考虑如下两种:“与”(符号为&)和“或”(符号为|)。其运算规则如下:0&0 = 0&1 = 1&0 = 0,1&1 = 1; 0|0 = 0,0|1 = 1|0 = 1|1 = 1。

   在一个逻辑表达式中还可能有括号。规定在运算时,括号内的部分先运算;两种运算并列时,& 运算优先于 | 运算;同种运算并列时,从左向右运算。

   比如,表达式 0|1&0 的运算顺序等同于0|(1&0) ;表达式 0&1&0|1 的运算顺序等同于 ((0&1)&0)|1。

   此外,在 C++ 等语言的有些编译器中,对逻辑表达式的计算会采用一种“短路”的策略。:在形如 a&b 的逻辑表达式中,会先计算 a 部分的值,如果 a = 0 ,那么整个逻辑表达式的值就一定为 0,故无需再计算 b 部分的值;同理,在形如 a|b 的逻辑表达式中,会先计算 a 部分的值,如果 a = 1 ,那么整个逻辑表达式的值就一定为 1,无需再计算 b 部分的值。

  现在给你一个逻辑表达式,你需要计算出它的值,并且统计出在计算过程中,两种类型的“短路”各出现了多少次。需要注意的是,如果某处“短路”包含在更外层被“短路”的部分内则不被统计,如表达式 1|(0&1) 中,尽管 0&1 是一处“短路”,但由于外层的 1|(0&1) 本身就是一处“短路”,无需再计算 0&1 部分的值,因此不应当把这里的0&1 计入一处“短路”。

输入格式

从文件 expr.in 中读入数据。

输入共一行,一个非空字符串 s 表示待计算的逻辑表达式。

输出格式

输出到文件 expr.out 中。

输出共两行,第一行输出一个字符 0 或 1 ,表示这个逻辑表达式的值;

第二行输出两个非负整数,分别表示计算上述逻辑表达式的过程中,形如 a&b 和 a|b 的“短路”各出现了多少次。

输入输出样例

输入样例1:
0&(1|0)|(1|1|1&0)
输出样例1:
1
1 2
输入样例2:
(0|1&0|1|1|(1|1))&(0&1&(1|0)|0|1|0)&0

输出样例2:

0
2 3

说明

2022年CSP-J复赛真题解析_第3张图片

2022年CSP-J复赛真题解析_第4张图片

耗时限制1000ms  内存限制128MB

解析
考点:二叉树,表达式,搜索

思路:

计算机来说,必须构建出表达式树,才可以进行计算。而输入是中缀表达式,因此,需要先把中缀表达式转后缀表达式,然后基于后缀表达式构建出表达式树,最后进行计算。

1. 中缀转后缀

需要借助栈来实现从中缀表达式到后缀表达式的转换。

这里明确一下使用栈转换的算法思想:

从左到右开始扫描中缀表达式,遇到数字, 直接输出

遇到运算符时:

a. 若为“(” 直接入栈

b. 若为“)” 将符号栈中的元素依次出栈并输出, 直到 “(“, “(“只出栈, 不输出

c. 若为其他符号, 将符号栈中的元素依次出栈并输出, 直到遇到比当前符号优先级更低的符号或者”(“。 将当前符号入栈。

扫描完后, 将栈中剩余符号依次输出。

2. 构建表达式树

逐次读取后缀表达式的每一个符号

如果符号是操作数,那么我们就建立一个单节点树并将一个指向它的指针推入栈中;

如果符号是操作数,则从栈中弹出两棵树 T1 和 T2(先弹出 T1),并形成一颗以操作符为根的树,其中 T1 为右儿子,T2 为左儿子;

然后将新的树压入栈中,继续上述过程。

3. 计算表达式值

DFS 遍历即可。表达式树中,叶子一定是数值 00 或 11,非叶子一定是 & 或者 |。DFS 过程:

  1. 遍历到叶子直接返回叶子的值。
  2. 遍历到非叶子时,先递归遍历左子树返回对应的子树的值。然后基于左子树的返回值和当前结点的运算符判断是否会短路:
    1. 1| 会发生“或短路”,并且返回 11
    2. 0& 会发生“与短路”,并且返回 00
    3. 非上面两种情况,计算右子树的值并返回其结果即可。
    4. 参考代码
    5. #include 
      using namespace std;
      const int N = 1e6+5;
      string s;
      struct Node {
          int v, l, r;
      } tr[N];
      int num, ans1, ans2;
      stack ops;
      stack sta;
      vector sf;   // suffix 后缀表达式
      /* 中缀转后缀:
      从左到右开始扫描中缀表达式
      遇到数字, 直接输出
      遇到运算符
      a.若为“(” 直接入栈
      b.若为“)” 将符号栈中的元素依次出栈并输出, 直到 “(“, “(“只出栈, 不输出
      c.若为其他符号, 将符号栈中的元素依次出栈并输出, 直到遇到比当前符号优先级更低的符号或者”(“。 将当前符号入栈。
      扫描完后, 将栈中剩余符号依次输出
      */
      void in2sf() {
          for(int i = 0; i < s.size(); i++) {
              if(s[i] == '0' || s[i] == '1')  // 数字直接写下
                  sf.push_back(s[i]);
              else if(s[i] == '(')  // 是 (
                  ops.push(s[i]);
              else if(s[i] == ')') {  // 是 )
                  while(!ops.empty() && ops.top() != '(') {
                      sf.push_back(ops.top());  // 一直输出,直到碰到左括号
                      ops.pop();
                  }
                  ops.pop();  // 弹出额外的 '('
              } else if(s[i] == '&') {   // 是 &
                  while(!ops.empty() && ops.top() == '&') {
                      sf.push_back(ops.top());
                      ops.pop();
                  }
                  ops.push('&');
              } else {   // 是 |
                  while(!ops.empty() && ops.top() != '(') {
                      sf.push_back(ops.top());
                      ops.pop();
                  }
                  ops.push('|');
              }
          }
          while(!ops.empty()) {
              sf.push_back(ops.top());
              ops.pop();
          }
      }
      
      void build() {
          for(int i = 0; i < sf.size(); i++) {
              if(sf[i] == '0' || sf[i] == '1') {
                  tr[++num] = {sf[i]-'0', -1, -1};
                  sta.push(num);
              } else {
                  int r = sta.top(); sta.pop();
                  int l = sta.top(); sta.pop();
                  int v = (sf[i]=='&'?2:3);
                  tr[++num] = {v, l, r};
                  sta.push(num);
              }
          }
      }
      
      int dfs(int u) {
          if(tr[u].v == 0 || tr[u].v == 1) return tr[u].v;  // 是叶子(数字)结点
          int l = dfs(tr[u].l);
          if(l == 0 && tr[u].v == 2) {  // 0&
              ans1++;
              return 0;
          }
          if(l == 1 && tr[u].v == 3) {  // 1|
              ans2++;
              return 1;
          }
          int r = dfs(tr[u].r); 
          return r;  // 只要不短路,结果肯定就取决于右值  1&  0|
      }
      
      int main(){
          cin >> s;
          in2sf();   // 在构建表达式树前,需要把中缀表达式转后缀
          build();   // 利用后缀表达式构建表达式树
          cout << dfs(num) << '\n';  // 后缀表达式下,根在末尾,从根 dfs
          cout << ans1 << ' ' << ans2;
          return 0;
      }
      

参考代码2

#include 
using namespace std;
const int N = 1e6 + 10;
struct node
{
	char c;//存储结点的运算符 & |
	int l,r;//左右孩子的编号
	int v;//结点的值
} a[N];
stack op;//存储运算符,辅助中缀转后缀的过程
stack st;//存储运算数,辅助后缀表达式计算的过程
char s[N];//存储中缀表达式
vector v;//存储后缀表达式
int k = 0;//表示结点编号
int c1,c2;//&短路的次数,  |短路的次数
//搜索求出短路的次数
void dfs(int x){
	if(a[x].l == 0 && a[x].r == 0) return;//叶子结点
	int t1 = a[x].l,t2 = a[x].r;
	dfs(t1);//搜索左孩子
	//左孩子搜到底返回的过程中判断短路的次数
	if(a[x].c == '&' && a[t1].v == 0){
		c1++;
		return;
	}
	if(a[x].c == '|' && a[t1].v == 1){
		c2++;
		return;
	}
	dfs(t2);
}
int main(){
	scanf("%s",s+1);
	int len = strlen(s+1);
	//1. 将中缀表达式转换为后缀表达式
	for(int i = 1; i <= len; i++){
		//如果是运算数
		if(isdigit(s[i])) v.push_back(s[i]);
		else if(s[i] == '(') op.push(s[i]);
		else if(s[i] == ')'){
			//弹出元素,直到遇到左括号
			while(!op.empty() && op.top() != '('){
				v.push_back(op.top());
				op.pop();
			}
			//左括号也要单独弹出
			op.pop();
		}else if(s[i] == '&'){
			//弹出元素直到遇到更低优先级的运算符或者(
			while(!op.empty() && op.top() == '&')
			{
			v.push_back(op.top());
			op.pop();
			}
			op.push(s[i]);//将&入栈
		}else{
			//|
			while(!op.empty()&&(op.top()=='&'||op.top()=='|')){
			v.push_back(op.top());
			op.pop();
			}
			op.push(s[i]);//将|入栈
		}
	}
	//如果栈中还有元素,依次弹出
	while(!op.empty()){
		v.push_back(op.top());
		op.pop();
	}
	//2. 后缀表达式计算, 通过计算的过程建树
	len = v.size();
	int x,y;
	for(int i = 0;i < len;i++){
		//如果遇到运算数, 入栈,遇到运算符,取出次顶和栈顶运算,结果入栈
		if(isdigit(v[i])){
			a[++k] = {0,0,0,v[i] - '0'};
			st.push(k);
		}else{
			x = st.top();//栈顶
			st.pop();
			y = st.top();//次顶
			st.pop();
			if(v[i] == '&'){
				a[++k] = {v[i],y,x,a[y].v & a[x].v};
				st.push(k);
			}else{
				a[++k] = {v[i],y,x,a[y].v | a[x].v};
				st.push(k);
			}
		}
	}
	cout<

2022年CSP-J-T4-上升点列(point)

题目描述

在一个二维平面内,给定 n 个整数点 (xi, yi),此外你还可以自由添加 k 个整数点。你在自由添加 k 个点后,还需要从 n + k 个点中选出若干个整数点并组成一个序列,使

得序列中任意相邻两点间的欧几里得距离恰好为 1 而且横坐标、纵坐标值均单调不减,即 x[i+1] − x[i] = 1, y[i+1] = y[i] 或 y[i+1] − y[i] = 1, x[i+1] = x[i]。请给出满足条件的序列的最大长度。

输入格式

从文件 point.in 中读入数据。

第一行两个正整数 n, k 分别表示给定的整点个数、可自由添加的整点个数。

接下来 n 行,第 i 行两个正整数 xi, yi 表示给定的第 i 个点的横纵坐标。

输出格式

输出到文件 point.out 中。

输出一个整数表示满足要求的序列的最大长度。

输入输出样例

输入样例1:
8 2
3 1
3 2
3 3
3 6
1 2
2 2
5 5
5 3

输出样例1:

8
输入样例2:
4 100
10 10
15 25
20 20
30 30
输出样例2:
103

说明

2022年CSP-J复赛真题解析_第5张图片

  • 耗时限制1000ms  内存限制512MB
解析
考点:动态规划,线性DP

思路1:

依据题意, 要形成的上升点列一定要满足 x  y 均单调不递减。因此不难想到先按 x 升序,x 相等按 y 升序。

如果没有能够增加 k 个点的条件,发现本题就是 LIS DP 模型。

考虑到最多可以添加 k 个点,可以将 LIS 中的一维状态设为二维状态。

即: f[i][j] 表示以第 i 个点结尾,添加了 j 个点的最大长度。

边界条件: f[i][0] = 1,表示以第 i 个点结尾, 即使没有添加任何点,也不能和其 他任何点拼接也能形成一个长度为 1 的上升点列。

可以再添加 j 个点, 那么 f[i][j] = j + 1,即:当前点和添加的 j 个点。

考虑 f[i][j] 的状态转移,  i 个点可以由 i 下角的任意点转移过来,那么可 以枚举第 i 个点左下角的所有点。

设: p 为两点之间最多需要补充的点。

c = a[i].x - a[t].x + a[i].y - a[t].y - 1;

如果: j >= c,则点 i 可以由点 t 转移过来:

f[i][j] = max(f[i][j], f[t][j-c]+c+1);

参考代码:

#include 
using namespace std;
/*
题目大意:在有 n 个点的情况下,补充 k 个点
选出最多的点,满足:
(1) 点是相邻的
(2) 坐标单调不递减
*/
const int N = 510,K = 110;
struct node{
	int x,y;
}a[N];
//f[i][j]:以第 i 个点结尾,补充 j 个点,形成最长上升点列的长度
int f[N][K];
int n,k;
//按 x 升序, x 相等按 y 升序
bool cmp(node n1,node n2){
	return n1.x>n>>k;
	for(int i = 1;i <= n;i++){
		cin>>a[i].x>>a[i].y;
	}
	//排序
	sort(a+1,a+n+1,cmp);
	//边界条件
	for(int i = 1;i <= n;i++){
		for(int j = 0;j <= k;j++){
			f[i][j] = j + 1;
		}
	}
	//从第 2 个点开始递推
	int c;
	for(int i = 2;i <= n;i++){
		//枚举每个点前面的点
		for(int t = 1;t < i;t++){
			//只能取第 i 个点左下角的点
			if(a[t].y > a[i].y) continue;
			//c:从第 t 个点到第 i 个点要补充点的数量
			c = a[i].x-a[t].x + a[i].y-a[t].y - 1;
			//枚举 j 的值:补充点的数量
			for(int j = c;j <= k;j++){
				f[i][j] = max(f[i][j],f[t][j-c]+c+1);
			}
		}
	}
	//求答案:求以哪个点结尾形成的上升点列的长度是最长的
	int ans = 0;
	for(int i = 1;i <= n;i++) ans = max(ans,f[i][k]);
	cout<

你可能感兴趣的:(算法,数据结构)