Skip to content
On this page

凹语言版CSP-J 2025真题详解


CSP-J是由中国计算机学会(CCF)主办的计算机软件能力认证,分为**CSP-J(入门级)和CSP-S(提高级)**两个级别。CSP-J主要考察选手的编程基础和算法能力,适合初一的学生,比赛分为初赛(笔试)和复赛(上机编程)两个阶段。参与CSP-J的选手有机会获得重点中学和大学的特招资格,部分高校在“强基计划”中优先录取。

2025年CSP-J/S复赛在11月1日上午结束。本文尝试通过凹语言来讲解CSP-J的真题,通过不同的视角体会算法问题和工程问题的差异。

1. CSP 2025入门组第二轮 第1题

这道题目的背景是给出一个包含大小写字母和数字(1 到 9)的混合字符串。任务是从中提取出所有有效的数字字符,然后用这些数字,不遗漏地、按任意顺序排列组合,最终要拼出一个最大的正整数。

比如“290es1q0”可以拼出最大的数字是92100。

2. 凹语言版本的准备工作

当前凹语言的最新版本为 v1.3.0,尚未提供本地文件的读写功能。因此首先需要解决的是为凹语言设计和实现输入函数。

2.1 包装导入函数(英文版)

目前的凹语言底层基于WebAssembly引擎执行,因此可以给宿主增加2个读标准输入的函数:

  • func getStdinSize() => i32:获取stdin标准数据的总字节长度。
  • func getStdinData(ptr: i32):读取stdin标准数据到指定的wasm内存地址。

为何要这样设计?这是为了通过最简单的方式满足凹语言读取标准输入字符串的需求,也是解CSP-J第一题的必要前提。而且竞赛本身也是从stdin读取数据,这样可以避免读文件时,文字名字符串带来的复杂度(比如wasm的宿组函数参数全部都是整数类型,不能原生支持字符串)。

有了以上的宿组函数之后,可以通过以下代码包装为凹语言的函数:

go
#wa:import cspj getStdinSize
func getStdinSize() => i32

#wa:import cspj getStdinData
func getStdinData(ptr: i32)

其中#wa:import导入宿组函数的指令,cspj getStdinSize表示“cspj”环境的“getStdinSize”函数。目前还没有“cspj”环境,还只是在设计阶段。

有了以上2个函数就可以包装读取输入的函数:

wa
import "unsafe"

func readInput() => []byte {
    n := getStdinSize()
    d := make([]byte, n)
    getStdinData(unsafe.Raw(d))
    return d
}

代码中unsafe.Raw(d)是为了获取字节切片的地址。

2.2 包装导入函数(中文版)

以上是凹语言英文的写法。首先是用中文名字来设计导入函数:

  • func 获取标准输入数据大小() => i32:获取stdin标准数据的总字节长度。
  • func 获取标准输入数据(ptr: i32):读取stdin标准数据到指定的wasm内存地址。

我们也可以通过凹语言中文来编写这个导入代码:

#凹:导入 信奥初级组 获取标准输入数据大小
函数·获取标准输入数据大小() => 整型

#凹:导入 信奥初级组 获取标准输入数据
函数·获取标准输入数据(地址: 整型)

其中#凹:导入导入宿组函数的指令,信奥初级组 获取标准输入数据大小表示“信奥初级组”环境的“获取标准输入数据大小”函数。目前还没有“信奥初级组”环境,还只是在设计阶段。

有了以上2个函数就可以包装读取输入的函数:

引入 "洪荒"

函数·读取输入 => []字节:
    长 := 获取标准输入数据大小()
    地 := 获取标准输入数据([]字节, 长)
    获取标准输入数据(洪荒·原生(地))
    返回 地
完毕

3. 凹语言的解题思路

刚设计完凹语言的标准输入读取函数,现在还没有真实的能执行的凹语言解题环境。我们先整理下解题的思路。

这道“拼数”问题的算法并不复杂,它依赖于计算机科学中基础的贪心策略。要使拼成的数最大,核心思想是最大化高位上的数值。因此,整个解题流程被拆解为两个关键步骤:数据筛选降序排序。首先,我们通过遍历输入字符串,排除所有字母和数字 0,只保留 1~9 的有效数字,确保拼成的数位数最多。

以上是算法工程师的思维。如果是码农或者汤普森的思维,我们可以暴力求解:第一次找全部9的数字,然后是8、7、6一次下降;可以针对10个数字写10个函数,也可以包装为1个函数。当然,字符串很长的话会有点浪费视角。码农的改进思路是做一个10个数字的数组,一次遍历完成把0~9每个数字的个数统计下来。

凹语言版本代码如下:

wa
func solve(s: string) {
	digits := make([]int, 10)

	// 统计数字的个数
	for _, c := range s {
		if c >= '0' && c <= '9' {
			digits[c-'0']++
		}
	}

	// 倒序输出
	for i := 9; i >= 0; i-- {
		for k := 0; k < digits[i]; k++ {
			print(string('0' + i))
		}
	}
	println()
}

中文版本如下:

函数·解题(输入: 字串):
	数字表 := 构建([]整型, 10)

	注: 统计数字的个数
	循环 _, 某 := 迭代 输入:
		如果 某 >= '0' && 某 <= '9':
			数字表[某-'0']++
		完毕
	完毕

	注: 倒序输出
	循环 甲 := 9; 甲 >= 0; 甲--:
		循环 乙 := 0; 乙 < 数字表[甲]; 乙++:
			打印(字串('0' + 甲))
		完毕
	完毕
	输出()
完毕

3. 结语:算法之外的实战价值

通过对 CSP-J “拼数”问题的解析与设计思路,我们不仅回顾了简单的贪心算法思想,更重要的是,我们找到了在实战中打通了凹语言支持此类竞赛的整个技术链路路径。在实际应用中,打通凹语言支持 CSP 等实战应用的 I/O 输入输出链路,其意义远大于算法本身的优化。 我们在这一过程中解决了本地文件读取等关键障碍。凹语言开发组的探索方向,正是希望让孩子们能够以母语思维进行编程创作,最大化地发挥他们的创造力。用最简单、最贴近中文习惯的语法去触达计算机科学的本质,这是母语编程的最终目标。