【GESP题解】游戏/小杨和整数拆分/数组划分/ 括号序列
GESP 考级题目分析
题目一:[GESP202403 六级] 游戏
1.1 题目描述
你有四个正整数 \(n, a, b, c\),并准备用它们玩一个简单的小游戏。
在一轮游戏操作中,你可以选择将 \(n\) 减去 \(a\),或是将 \(n\) 减去 \(b\)。游戏将会进行多轮操作,直到当 \(n \leq c\) 时游戏结束。
你想知道游戏结束时有多少种不同的游戏操作序列。两种游戏操作序列不同,当且仅当游戏操作轮数不同,或是某一轮游戏操作中,一种操作序列选择将 \(n\) 减去 \(a\),而另一种操作序列选择将 \(n\) 减去 \(b\)。如果 \(a=b\),也认为将 \(n\) 减去 \(a\) 与将 \(n\) 减去 \(b\) 是不同的操作。
由于答案可能很大,你只需要求出答案对 \(10^9 + 7\) 取模的结果。
输入格式: 一行四个整数 \(n, a, b, c\)。
输出格式: 输出一行一个整数表示答案。
样例:
| 输入 | 输出 |
|---|---|
1 1 1 1 |
1 |
114 51 4 1 |
176 |
114514 191 9 810 |
384178446 |
数据范围:
- 对 \(20\%\) 的数据,\(a=b=c=1\),\(n \leq 30\)
- 对 \(40\%\) 的数据,\(c=1\),\(n \leq 10^3\)
- 对全部的测试数据,保证 \(1 \leq a, b, c \leq n \leq 2 \times 10^5\)
1.2 题意分析
这是一道计数类动态规划问题。核心要点:
- 从 \(n\) 开始,每轮可以减 \(a\) 或减 \(b\),直到 \(n \leq c\) 时停止
- 求所有可能的操作序列数量
- 注意:即使 \(a=b\),两种选择也视为不同操作
关键观察:
- 设 \(f[i]\) 表示从数字 \(i\) 开始,到达 \(i \leq c\) 的操作序列数
- 边界条件:当 \(i \leq c\) 时,\(f[i] = 1\)(不需要任何操作,只有一种"空"序列)
- 状态转移:\(f[i] = f[i-a] + f[i-b]\)(如果 \(i-a > c\) 则加上 \(f[i-a]\),如果 \(i-b > c\) 则加上 \(f[i-b]\))
等等,这里需要仔细思考。实际上:
- 当 \(i \leq c\) 时,游戏结束,\(f[i] = 1\)
- 当 \(i > c\) 时,可以选择减 \(a\) 或减 \(b\):
- 如果 \(i - a \leq c\),则减 \(a\) 后直接结束,贡献 1 种方案
- 如果 \(i - a > c\),则减 \(a\) 后还有 \(f[i-a]\) 种方案
- 同理处理减 \(b\) 的情况
所以转移方程为:
简化:当 \(i > c\) 时,如果 \(i-a \leq c\),贡献 1;否则贡献 \(f[i-a]\)。同理处理 \(b\)。
1.3 数据规模与策略分析
| 子任务 | 数据特点 | 可用策略 | 复杂度 |
|---|---|---|---|
| 20% | \(a=b=c=1, n \leq 30\) | 直接递推/递归 | \(O(n)\) |
| 40% | \(c=1, n \leq 10^3\) | 一维DP递推 | \(O(n)\) |
| 100% | \(n \leq 2 \times 10^5\) | 一维DP递推 | \(O(n)\) |
策略选择:
- 由于 \(n\) 最大 \(2 \times 10^5\),\(O(n)\) 的一维DP完全可行
- 从 \(c+1\) 开始递推到 \(n\),每个状态 \(O(1)\) 转移
- 注意取模运算
1.4 参考代码
#include <bits/stdc++.h>
using namespace std;
const int MOD = 1e9 + 7;
const int MAXN = 2e5 + 10;
long long f[MAXN];
int main() {
int n, a, b, c;
cin >> n >> a >> b >> c;
// 边界:i <= c 时 f[i] = 1
for (int i = 0; i <= c; i++) {
f[i] = 1;
}
// 递推:i > c 时
for (int i = c + 1; i <= n; i++) {
f[i] = 0;
// 选择减 a
if (i - a <= c) {
f[i] = (f[i] + 1) % MOD;
} else {
f[i] = (f[i] + f[i - a]) % MOD;
}
// 选择减 b
if (i - b <= c) {
f[i] = (f[i] + 1) % MOD;
} else {
f[i] = (f[i] + f[i - b]) % MOD;
}
}
cout << f[n] << endl;
return 0;
}
题目二:[GESP202409 六级] 小杨和整数拆分
2.1 题目描述
小杨有一个正整数 \(n\),小杨想将它拆分成若干完全平方数的和,同时小杨希望拆分的数量越少越好。
小杨请你编写程序计算出总和为 \(n\) 的完全平方数的最少数量。
输入格式: 输入只有一行一个正整数 \(n\)。
输出格式: 输出一行一个整数表示答案。
样例:
| 输入 | 输出 |
|---|---|
18 |
2 |
样例解释: \(18 = 9 + 9 = 16 + 1 + 1\),其中最少需要 2 个完全平方数。
数据范围:
- 对 \(20\%\) 的数据,\(n \leq 20\)
- 对 \(40\%\) 的数据,\(n \leq 1000\)
- 对全部数据,\(1 \leq n \leq 10^5\)
2.2 题意分析
这是一道经典的完全背包问题(恰好装满,求最少物品数)。
问题转化:
- 物品:所有完全平方数 \(1, 4, 9, 16, \ldots\)(每个可以选多次)
- 背包容量:\(n\)
- 目标:恰好装满背包,且物品数量最少
数学背景(拉格朗日四平方和定理):
- 每个正整数都可以表示为至多 4 个完全平方数的和
- 所以答案只可能是 \(1, 2, 3, 4\)
动态规划思路:
- 设 \(dp[i]\) 表示凑出数字 \(i\) 最少需要的完全平方数个数
- 初始化:\(dp[0] = 0\),其余为无穷大
- 转移:\(dp[i] = \min(dp[i], dp[i - j^2] + 1)\),其中 \(j^2 \leq i\)
2.3 数据规模与策略分析
| 子任务 | 数据范围 | 可用策略 | 复杂度 |
|---|---|---|---|
| 20% | \(n \leq 20\) | 暴力枚举/DFS/BFS | \(O(4^n)\) 或 \(O(n \sqrt{n})\) |
| 40% | \(n \leq 1000\) | 完全背包DP | \(O(n \sqrt{n})\) |
| 100% | \(n \leq 10^5\) | 完全背包DP | \(O(n \sqrt{n})\) |
策略选择:
- \(n \leq 10^5\),\(\sqrt{n} \approx 316\)
- 完全背包DP复杂度 \(O(n \sqrt{n}) \approx 3 \times 10^7\),可以通过
- 也可以利用四平方和定理特判:
- 如果 \(n\) 是完全平方数,答案为 1
- 如果 \(n\) 可以表示为两个平方数之和,答案为 2
- 如果 \(n\) 满足 \(n = 4^a(8b+7)\),答案为 4(勒让德三平方和定理的推论)
- 否则答案为 3
- 但DP方法更通用,适合教学
2.4 参考代码
方法一:完全背包DP(推荐教学用)
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 10;
const int INF = 0x3f3f3f3f;
int dp[MAXN];
int main() {
int n;
cin >> n;
// 初始化
for (int i = 1; i <= n; i++) {
dp[i] = INF;
}
dp[0] = 0;
// 完全背包:外层遍历物品(平方数),内层遍历容量
for (int j = 1; j * j <= n; j++) {
int square = j * j;
for (int i = square; i <= n; i++) {
dp[i] = min(dp[i], dp[i - square] + 1);
}
}
cout << dp[n] << endl;
return 0;
}
方法二:数学定理特判(竞赛技巧)
#include <bits/stdc++.h>
using namespace std;
// 判断是否为完全平方数
bool isSquare(int n) {
int r = sqrt(n);
return r * r == n;
}
int main() {
int n;
cin >> n;
// 答案为1:n是完全平方数
if (isSquare(n)) {
cout << 1 << endl;
return 0;
}
// 答案为2:n = a^2 + b^2
for (int i = 1; i * i <= n; i++) {
if (isSquare(n - i * i)) {
cout << 2 << endl;
return 0;
}
}
// 答案为4:n = 4^a * (8b + 7)
int m = n;
while (m % 4 == 0) m /= 4;
if (m % 8 == 7) {
cout << 4 << endl;
return 0;
}
// 其余情况答案为3
cout << 3 << endl;
return 0;
}
题目三:[GESP202609 六级] 数组划分
3.1 题目描述
给定 \(n\) 个整数构成的数组 \(A = [a_1, a_2, \ldots, a_n]\)。
你需要将数组 \(A\) 划分为若干非空连续子段。对于划分得到的某个子段,它的偏差值定义为子段内整数和的平方。划分方案的偏差值定义为所有子段偏差值之和。
你需要最小化划分方案的偏差值。
形式化地,你可以将 \(A\) 划分为若干非空连续子段 \(A_1, A_2, \ldots, A_k\),使得 \(A = A_1 + A_2 + \ldots + A_k\)(这里的 \(+\) 代表数组的连接)。对于 \(1 \leq i \leq k\),设数组 \(A_i = [a_1^{(i)}, \ldots, a_{m_i}^{(i)}]\) 包含 \(m_i\) 个整数。你需要最小化:
输入格式:
- 第一行,一个正整数 \(n\),表示数组 \(A\) 的长度
- 第二行,\(n\) 个整数 \(a_1, a_2, \ldots, a_n\),表示数组 \(A\)
输出格式: 一行,一个整数,表示划分方案偏差值的最小值。
样例:
| 输入 | 输出 |
|---|---|
4 1 2 -3 4 |
6 |
6 -1 -1 4 -5 -1 4 |
0 |
样例解释:
- 样例1:分为 \([1], [2], [-3, 4]\) 三段,偏差值 \(= 1^2 + 2^2 + 1^2 = 6\)
- 样例2:整个数组和为 \(-1-1+4-5-1+4 = 0\),不划分,偏差值 \(= 0^2 = 0\)
数据范围:
- 对 \(40\%\) 的测试点,\(0 \leq a_i \leq 50\)
- 对所有测试点,\(1 \leq n \leq 2000\),\(-100 \leq a_i \leq 100\)
3.2 题意分析
这是一道区间DP或一维DP问题,核心是把"划分"问题转化为"最后一段从哪里切开"的决策。
关键观察:
- 设 \(f[i]\) 表示前 \(i\) 个数划分后的最小偏差值
- 需要预处理前缀和 \(pre[i] = a_1 + a_2 + \cdots + a_i\)
- 枚举最后一段的起点 \(j+1\)(即最后一段是 \([j+1, i]\)),则:
- 最后一段的和为 \(pre[i] - pre[j]\)
- 最后一段的偏差值为 \((pre[i] - pre[j])^2\)
- 前 \(j\) 个数的最小偏差值为 \(f[j]\)
状态转移方程:
边界条件: \(f[0] = 0\)(空数组的偏差值为 0)
3.3 数据规模与策略分析
| 子任务 | 数据特点 | 可用策略 | 复杂度 |
|---|---|---|---|
| 40% | \(0 \leq a_i \leq 50\)(非负) | 贪心:尽量让每段和接近0 | \(O(n^2)\) |
| 100% | \(-100 \leq a_i \leq 100\),\(n \leq 2000\) | 一维DP | \(O(n^2)\) |
策略选择:
- \(n \leq 2000\),\(O(n^2)\) 的DP完全可行,计算量约 \(4 \times 10^6\)
- 必须使用
long long:子段和最大 \(2000 \times 100 = 2 \times 10^5\),平方后 \(4 \times 10^{10}\),远超int范围 - 前缀和预处理将区间和查询优化到 \(O(1)\)
3.4 参考代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
const long long INF = 0x3f3f3f3f3f3f3f3f;
long long a[MAXN], pre[MAXN], f[MAXN];
int main() {
int n;
cin >> n;
// 读入并计算前缀和
for (int i = 1; i <= n; i++) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
// DP
f[0] = 0;
for (int i = 1; i <= n; i++) {
f[i] = INF;
for (int j = 0; j < i; j++) {
long long sum = pre[i] - pre[j];
f[i] = min(f[i], f[j] + sum * sum);
}
}
cout << f[n] << endl;
return 0;
}
题目四:[GESP202609 七级] 括号序列
4.1 题目描述
对于字符串 \(S\) 与 \(T\),如果从 \(S\) 中删除任意多个字符可以得到 \(T\),那么 \(T\) 是 \(S\) 的子序列。换言之,\(T\) 是选取 \(S\) 中的若干字符按下标顺序连接而成的。两个子序列不同当且仅当所选取的下标不同。
例如 sun 是 sequence 的子序列;sequence 有 \(2^8\) 个不同的子序列,其中有空字符串,也有三个不同的子序列 e(因为第 2, 5, 8 个字符都为 e)。
对于字符串 \(S\),如果 \(S\) 满足以下条件那么 \(S\) 是合法括号序列:
- \(S\) 是空字符串,或者
- \(S\) 可由
(、合法括号序列、)三者连接得到,或者 - \(S\) 可由两个合法括号序列连接得到
例如 ()、()()、(()) 和 (()()) 都是合法括号序列。但是 (()、)( 不是合法括号序列。
给定一个长度为 \(n\) 的仅包含 ( 与 ) 的字符串 \(S\)。请你求出 \(S\) 所有 \(2^n\) 个子序列中有多少个合法括号序列。由于答案可能很大,请你输出答案对 \(10^9\) 取模的结果。
输入格式:
- 第一行,一个正整数 \(n\),表示字符串 \(S\) 的长度
- 第二行,长度为 \(n\) 的仅包含
(与)的字符串 \(S\)
输出格式: 输出一行一个整数,表示 \(S\) 的合法括号子序列的数量对 \(10^9\) 取模的结果。
样例:
| 输入 | 输出 |
|---|---|
6 ))(()( |
3 |
样例解释: 合法子序列有 3 个:空字符串;选取下标 \((3, 5)\) 组成的 ();选取下标 \((4, 5)\) 组成的 ()。
数据范围:
- 对 \(40\%\) 的测试点,\(1 \leq n \leq 400\)
- 对所有测试点,\(1 \leq n \leq 2000\)
4.2 题意分析
这是一道计数类动态规划问题,需要统计合法括号子序列的数量。
关键概念:
- 子序列:可以删除任意字符,保持相对顺序;下标不同则子序列不同
- 合法括号序列:任意前缀中
(数量 \(\geq\))数量,且总数相等
核心观察(平衡度/余额):
- 定义"余额"为当前子序列中
(的数量减去)的数量 - 合法括号序列要求:任意时刻余额 \(\geq 0\),且最终余额 \(= 0\)
DP状态设计:
- 设 \(f[i][j]\) 表示只考虑 \(S\) 的前 \(i\) 个字符,选出的子序列余额为 \(j\) 的方案数
- 注意:这里的子序列必须满足"任意前缀余额非负"
状态转移:
对于第 \(i\) 个字符,有三种决策:
- 不选它:\(f[i][j] \mathrel{+}= f[i-1][j]\)
- 选它且是
(:余额从 \(j-1\) 增加到 \(j\),需要 \(j > 0\),\(f[i][j] \mathrel{+}= f[i-1][j-1]\) - 选它且是
):余额从 \(j+1\) 减小到 \(j\),需要 \(j+1 \leq n\),\(f[i][j] \mathrel{+}= f[i-1][j+1]\)
边界条件: \(f[0][0] = 1\)(空子序列)
答案: \(f[n][0]\)(余额为 0,即合法括号序列)
4.3 数据规模与策略分析
| 子任务 | 数据范围 | 可用策略 | 复杂度 |
|---|---|---|---|
| 40% | \(n \leq 400\) | 二维DP | \(O(n^2)\) |
| 100% | \(n \leq 2000\) | 二维DP | \(O(n^2)\) |
策略选择:
- \(n \leq 2000\),\(O(n^2)\) 的DP计算量约 \(4 \times 10^6\),完全可行
- 状态数 \(n \times (n+1)\),每个状态 \(O(1)\) 转移
- 可以用滚动数组优化空间到 \(O(n)\)
- 注意 \(10^9\) 不是质数,但本题只需要加法取模,不需要除法
4.4 参考代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
const int MOD = 1e9;
int n;
char s[MAXN];
int f[MAXN][MAXN];
int main() {
cin >> n;
cin >> (s + 1); // 从1开始存储
// 边界:空子序列
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= n; j++) {
// 决策1:不选第i个字符
f[i][j] = f[i - 1][j];
// 决策2:选'(',余额从j-1增加到j
if (s[i] == '(' && j > 0) {
f[i][j] = (f[i][j] + f[i - 1][j - 1]) % MOD;
}
// 决策3:选')',余额从j+1减小到j
if (s[i] == ')' && j < n) {
f[i][j] = (f[i][j] + f[i - 1][j + 1]) % MOD;
}
}
}
cout << f[n][0] << endl;
return 0;
}
滚动数组优化版本:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 2005;
const int MOD = 1e9;
int n;
char s[MAXN];
int f[2][MAXN];
int main() {
cin >> n;
cin >> (s + 1);
f[0][0] = 1;
for (int i = 1; i <= n; i++) {
int cur = i & 1, prev = (i - 1) & 1;
// 清空当前层
memset(f[cur], 0, sizeof(f[cur]));
for (int j = 0; j <= n; j++) {
// 不选
f[cur][j] = f[prev][j];
// 选'('
if (s[i] == '(' && j > 0) {
f[cur][j] = (f[cur][j] + f[prev][j - 1]) % MOD;
}
// 选')'
if (s[i] == ')' && j < n) {
f[cur][j] = (f[cur][j] + f[prev][j + 1]) % MOD;
}
}
}
cout << f[n & 1][0] << endl;
return 0;
}
总结对比
| 题目 | 考点 | 核心算法 | 时间复杂度 | 空间复杂度 | 难度 |
|---|---|---|---|---|---|
| 游戏 | 计数DP | 一维线性DP | \(O(n)\) | \(O(n)\) | ★★☆ |
| 整数拆分 | 完全背包 | 完全背包DP / 数学定理 | \(O(n\sqrt{n})\) | \(O(n)\) | ★★☆ |
| 数组划分 | 区间DP | 一维DP+前缀和 | \(O(n^2)\) | \(O(n)\) | ★★★ |
| 括号序列 | 计数DP | 二维DP+平衡度 | \(O(n^2)\) | \(O(n^2)\) 或 \(O(n)\) | ★★★ |
建议:
- 游戏:先思考清楚"减\(a\)或减\(b\)"两种选择如何转化为DP转移,强调边界条件
- 整数拆分:了解四平方和定理
- 数组划分:前缀和优化和int数据溢出问题
- 括号序列:理解"平衡度"的概念,这是括号类问题的通用技巧
本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。

浙公网安备 33010602011771号