2025-04-26 20:12:23
树状数组是挺不好教学的一个知识点。它需要以下前置知识:
在教学的时候,我们的教学顺序如下:
那么让我们来开始。
P3374 树状数组 1 是一道标准的树状数组问题:该题目给我们了一个数列,我们需要解决以下两个问题:
我们很容易想到用暴力的方法来做此题。于是我们可以估计一下暴力的时间复杂度:
题目中提到,求和的次数最多为 M 次,所以最坏情况下,时间复杂度为 O(M*N)
。而由于 M 和 N 的最大范围为 5*10^5
,所以最大运算次数高达 (5*10^5) * (5*10^5) = 2500亿
次,而竞赛中估算 1000 万次的运算时间就接近 1 秒了,这个时间肯定会超时。
数列的区间求和有一个 O(1)的办法,就是提前求出前缀和。假如 Sum(i) 表示前 i 个数的和,那么区间 (i,j]
的和就可以通过 Sum(j) - Sum(i)
来得出。可惜的是,本题还有一个操作是更新某一个数。如果更新的是第一个数,那么整个前缀和数组 Sum 都需要更新,这样更新的时间复杂度会变成 O(N),最坏情况下会有 O(M*N)
次更新,造成运算同样超时。
由此,我们需要一个更优秀的数据结构来解决这类问题,这就是树状数组。
在讲解树状数组前,我们先学习一下 lowbit 函数。
lowbit 函数实现的功能是:求 x 的二进制最低位 1
以及后面的 0
组成的数。例如:
所以,我们需要找到目标数的二进制中的最后那个 1
的位置。有两种实现方式:
x^(x-1) & x
方法一相对比较好理解,我拿二进制数 1100
举例解释如下:
(x-1)
的效果,相当于把二进制的最后一个1
变成 0
,比如某数 1100
减 1
之后,就变成了 1011
x^(x-1)
,就会得到 1100^1011=0111
x&
刚刚的 x^(x-1)
,就相当于把x
的最后一个1
留下来了,前面的1
都抹掉了:1100 & 0111 = 0100
x&-x
我们还是拿二进制数 1100
举例,由于负数是用补码表示,所以对于 1100
,它的负数:
11100
(最高为 1 为符号位)10011
(反码符号位不变,其余位取反)10100
(补码=反码+1)这样一操作,x&-x
就等于 01100 & 10100 = 0100
,同样把最后的 1
取出来了。
在实现中,我们用方法二的更多,因为更短。参考代码如下:
1 |
int lowbit(int x) { |
对于一个长度为 N 的序列,为了满足上面提到的更快的区间求和和更新的需求,我们可以构造一个树状数组。
树状数组(Binary Index Tree,简称 BIT)通过构造另一个长度为 N 的数组,来做到:
O(log N)
O(log N)
因为树状数组需要另外创建一个长度为 N 的数组,所以它的空间复杂度为O(N)
。
我们先创建出这个数组 b ,然后再引入它的元素间的树状逻辑关系。
我们有了数组 b,我们让数组 b 相对于原始序列 a,按如下的关系来保存范围和:
b[1]
保存 a[1]
的值b[2]
保存区间 [a[1], a[2]]
的和b[3]
保存 a[3]
的值b[8]
保存区间 [a[1], a[8]]
的和我们先不管如何做到的,先假设我们按上面的逻辑,初始化好了这个数组,那么它怎么能快速求出前缀和呢?
我们假设要求 a[1] ~ a[7]
的和,如下图所示,我们知道这段和满足:Sum(7) = b[4] + b[6] + b[7]
那么,我们观察一下 b[4],b[6],b[7]
这几个下标有什么特点:
如果结合上我们刚刚教的 lowbit 函数,我们就可以发现如下规律:
4 = 6 - lowbit(6)
6 = 7 - lowbit(7)
于是,如果我们要求 Sum(7),就可以用 b[7] 开始累加,然后用 7 - lowbit(7)
得到 6,再用 6 - lowbit(6)
得到 4,最后 4 - lowbit(4) = 0
,就结束整个求和累加过程。
把以上逻辑转换成代码,是这样的:
1 |
int query(int range) { |
有人可能要问了,这个求和都是从序列开头开始的,如果我们想求序列中间一段,比如从 x 到 y 的区间和,应该怎么办呢?这种情况,我们可以:
query(y) - query(x-1)
得到区间 [x,y]
的和树状数组也支持更新数据,像P3374 树状数组 1题目中要求的那样,我们可以将某个数加上 x,这种情况应该如何更新数组呢?
我们以更新 a[1]
为例,通过观察,我们发现涉及 a[1]
的数组有:b[1],b[2],b[4],b[8]
,如下图所示:
你有观察出来规律吗?这刚好是我们构建的这个树从叶子结点到根结点的一条路径。
那同样的问题来了,我们如何求解出b[1],b[2],b[4],b[8]
这个路径呢?我们来观察一下:
2 = 1 + lowbit(1)
4 = 2 + lowbit(2)
8 = 4 + lowbit(4)
我们再验证一个中间结点的更新,比如更新 a[5],如下图所示:
我们看看规则是不是一样:
6 = 5 + lowbit(5)
8 = 6 + lowbit(6)
至此,我们总结出更新方法:从数列的下标 idx 开始,不停地更新,并且用 idx += lowbit(idx)
获得下一个更新的下标,直到更新到下标超过上界(N)为止。
1 |
void add(int idx, int val) { |
最暴力的初始化方法是:我们假设原序列全是 0,这样树状数组的初始状态也全是 0 即可正常表达上面的树型关系。然后,我们把每一个 a 序列中的数用更新的方式来放入树状数组中。
至此,我们完成了例题P3374 树状数组 1中的所有细节讨论,完整的代码如下:
1 |
/** |
但是,以上的这种初使化方法,时间复杂度为 O(N*logN)
,如果数据刚好卡在初始化中,我们可以用以下这种方法来将初始化时间复杂度优化到 O(N)
。
为了讲明白这种初始化,我们需要观察树状数组 b 中的每个元素代表的数据范围有什么规律。为什么 b[5] 只代表 a[5] 一个元素,但是 b[8]代表的是[a[1],a[8]]
区间的 8 个元素的和 ?
最终我们可以发现,一个数组元素代表的区间范围大小就是它的 lowbit 函数求出来的值。
例如:
[a[1],a[8]]
共 8 个元素01011000
,lowbit(88)=8
,所以它代表的区间为 8 个元素。进一步的,我们可以观察出,对于一个 b[x],它代表的区间为[x-lowbit(x)+1, x]
。
这对初始化有什么用呢?
因为 b[x] 代表的区间和是[x-lowbit(x)+1, x]
,所以:b[i] = s[i] - s[i-lowbit(i)]
至此,我们可以将例题P3374 树状数组 1的代码更新如下:
1 |
/** |
上面讲到,树状数组中的元素 b[x] 管辖的区间和是[x-lowbit(x)+1, x]
,因此,我们更能理解树状数组的更新逻辑:
举例来说,如果我们要更新 a[2] 的值,lowbit(2) 的值是 0010,所以,我们要更新:
[1, 2]
[1, 4]
[1, 8]
再举一个例子,如果我们要更新 a[5] 的值,lowbit(5) 的值是 0001,所以我们要更新:
[5, 5]
[5, 6]
[1, 6]
可以看到,对于每一个 b[x],它代表的范围右边界始终是 x,而它的左边界,则随着更新的节点往上移动,在不停扩大。
有些时候,题目会让我们一次更新一段区间,这个时候,我们可以引入差分数组来替代原数组。
差分数组中的每一个元素,是原数组相邻两个数的差。
例如:
我们对差分数组求前缀和,就可以还原出原数组。
这个时候,如果我们把原数组的第 3 个数到第 5 个数都加上 2,我们看看效果:
我们观察到,原数组的一个区间都加上 2 之后,在差分数组那里,只有第 3 个数和第 6 个数有变化,其它都没有变化。所以,如果我们用差分数组来代替原数组,就可以只更新两个数值来代表原来的范围更新。
P3368 【模板】树状数组 2此题可以很好地练习差分数组与数状数组的结合运用,相关代码如下:
1 |
/** |
刚刚讲到,对于一个 b[x],它代表的区间为[x-lowbit(x)+1, x]
那么对于一个二维的树状数组 b[x, y],它代表的区间就是 a(x-lowbit(x)+1, y-lowbit(y)+1) - a(x, y)
形成的矩阵的总和。如下图所示:
对于二维的树状数组,更新就需要用两层的循环了。示例代码如下:
1 |
void add(int x, int y, int v) { |
查询前缀和同样需要用循环,示例代码如下:
1 |
int query(int x, int y) { |
如果题目要求区间和,则需要用容斥原理来求解,这里不再展开介绍。
什么是逆序对?逆序对是指一个序列中,a[i] > a[j]
且 i < j
的有序对。
比如一个序列是 3 2 1
,它的逆序对就有:3 2
,3 1
,2 1
三组。
树状数组如何和逆序对的数量扯上关系呢?
拿序列 3 2 1
举例,我们知道,树状数组是可以用前缀和的。如果我们:
update(3, 1)
,同时记录插入了 1 个数例题:P1908 逆序对
在此题中,我们先要解决两个问题,才能借用上面的思想:
问题1、题中的数据范围太大,我们如何解决?
答案:我们可以用离散化的思想,把 2 10000 1
变成 2 3 1
,因为逆序对是统计相对大小,所以这样更改之后,逆序对的数量是不变的。
具体如何离散化呢?我们可以将数据依次标记上编号,然后排序。例如:
100 200 50
, 我们把它分别标上编号 (100,1), (200,2), (50,3)
(50,3), (100,1), (200,2)
(50,3,1), (100,1,2), (200,2,3)
(100,1,2), (200,2,3), (50, 3, 1)
2,3,1
因为 N 最多是 5*10^5
,所以离散化之后,树状数组的大小也缩减到了 5*10^5
在实现的时候,我们可以用结构体来保存上面的三元组。
1 |
struct Node { |
问题2、如果有两个相等的元素,会不会计算错误?
我们假设元素是 200 300 200
,按我们刚刚的操作:
(200,1) (300,2) (200,3)
(200,1) (200,3) (300,2)
(200,1,1) (200,3,2) (300,2,3)
(200,1,1) (300,2,3) (200,3,2)
1,3,2
这种是没问题的,但是,如果我们排序的时候不是用的稳定排序,把第二个 200 排到了前面,就会得到 2,3,1
,这样逆序对就会多一个 2 1
,而这本来是不存在的。
所以,为了解决这个问题,我们可以用稳定排序stable_sort
,或者保证排序的时候,值相同的情况下,标号大的在后面。
以下是完整的参考程序:
1 |
/** |
2025-04-13 15:27:30
深度优先搜索(DFS)是学生学习算法的第一道门槛,因为它的主要形式是递归。而递归中需要将搜索的相关信息通过参数传递,这一点需要学生深刻理解 DFS。
DFS 有比较标准的模版,如下所示:
1 |
void dfs(int pt) // pt 表示层数 |
我们将运用该模版,完成后面的题目。
递归的时候,程序会占用栈空间来保存函数的环境变量。根据编译器以及编辑参数的不同,栈空间的大小也不同。通常情况下,竞赛中的编译器设定的栈空间为 8M 或者 16M。
假如,我们在一个递归函数中使用了 10 个 int 变量,那么每个递归函数就需要 4*10=40
字节的栈空间。8M 一共可以支持 8*1000*1000/40=200000
层调用。考虑到栈空间还需要保存当前函数执行的地址等变量,可供支持的调用层数会更小一点。
同学们也可以做如下的递归函数栈空间的测试:
1 |
/** |
在我的本地,以上程序调用了约 13 万次后栈溢出。为了保险,我们在比赛中如果调用深度小于 1 万层,那应该是稳妥的;否则我们需要考虑是否用别的解法来解题。
题目名 | 说明 |
---|---|
P1036 选数 | NOIP 2002 普及组 |
P1219 八皇后 Checker Challenge | USACO 1.5 |
P1596 Lake Counting S | USACO10OCT |
P2036 PERKET | COCI 2008/2009 #2 |
P12139 黑白棋 | 蓝桥杯 2025 省 A,写起来较繁琐 |
P7200
P10483
这是八皇后的变种,N 皇后问题。可以作为基础练习。具体解法是:
v[15]
表示每个皇后的列值。1 |
bool check(int pt) { |
完整的代码如下:
1 |
/** |
此题需要从小到大取数求和,然后再判断是否是素数。用递归的方式来进行枚举。
1 |
/** |
此题既可以用 DFS,也可以用 BFS。考虑到 N 和 M 最大值为 100,所以递归的层次最多为 1 万层,所以我们可以试试 DFS。
以下是参考代码:
1 |
/** |
因为 N 最多为 10,每种食材可以选或者不选两种情况,所以最多情况数为 2^10=1024
种。搜索时间满足要求。
所以,此题用 DFS 可以非常方便解决。在搜索的时候,我们可以将食材的相关信息带到 DFS 函数的参数中,方便最后答案的求解。
1 |
/** |
此题是搜索题,需要在中间尽可能检查状态来剪枝,以节省搜索次数。
题目有三类限制,分别可以用在不同的剪枝环节。
限制一:在每一行和每一列中,黑色棋子和白色棋子的数量必须相等(即为 3)。
限制二:不能有超过两个相同颜色的棋子连续排列。
限制三:行列唯一性
另外,这个棋盘有几个位置已经设定了值,我们需要标记下来,搜索的时候跳过对这些位置的尝试,但需要在这些位置做合法性检查。
1 |
/** |
2025-04-12 22:00:46
STL 库是 C++ 语言的标准库,我们在比赛中主要用到的有如下内容。
函数 | 调用示意 | 说明 |
---|---|---|
sort | sort(v.begin(), v.end()) |
快速排序 |
stable_sort | stable_sort(v.begin(), v.end()) |
稳定排序 |
unique | unique(v.begin(), v.end()) |
去重,返回的是去重后的元素末地址。可以结合 erase 函数来把多余数据删除 |
next_permutation | next_permutation(v, v+n) |
返回全排列的下一个值,当没有下一个排列时,函数返回 false |
prev_permutation | prev_permutation(v, v+n) |
返回全排列的上一个值,当没有上一个排列时,函数返回 false |
nth_element | nth_element(v.begin(), v.begin() + k, v.end()), |
函数执行后,v.begin()+k 位置的数为排序后的最终位置,即左边的数都小于它,后面的数都大于它 |
lower_bounds | lower_bounds(v, v+n, a) |
查找大于或等于 a 的第一个位置,如果没找到则返回 end() |
upper_bounds | upper_bounds(v, v+n, a) |
查找大于 a 第一个位置,如果没找到则返回 end() |
equal_range | equal_range(v, v+n, a) |
equal_range 返回一个 pair,first 元素是查找到的匹配 a 值的左边界,second 元素是匹配到的 a 值的右边界,边界为左闭右开原则。当 first == second 的时候,相当于没找到目标值 |
__gcd | __gcd(a, b) |
返回 a 和 b 的最大公约数 |
reverse | reverse(v.begin(), v.end()) |
将原序列逆序 |
2025-04-12 21:40:39
数学题是信息学竞赛中重要的一类题目,通常包括几何、数论、容斥原理等。
本文将相关的题目归纳整理,用于教学。
本题解法:每个矩形(包括正方形)都可以由一段左边线段和一段上边线段确定。因此,我们只需要枚举所有可能的线段。
对于一个长是 N 宽是 M 的棋盘。
所以:
(1+2+3+...+N)= N*(N+1)/2
个。(1+2+3+...+M)= M*(M+1)/2
个。N*(N+1)/2 * M*(M+1)/2
个矩形。用相同的办法可以推导正方形的数量,方法如下:
N*M
个边长为 1 的正方形。同理:
(N-1)*(M-1)
个边长为 2 的正方形。以此类推,可以构造出 N*M + (N-1)*(M-1) + (N-2)*(M-2) + (N-M+1)*1
个正方形(N>M)。
另外,需要注意使用 long long
来保存结果。完整的代码如下:
1 |
|
这道题可以先用暴力的办法把前面几个数打出来,然后我们能发现数的规律是:1,1,2,5,14,42,132,429,1430,….
这是计算组合中很常见的卡特兰数,卡特兰数有两种公式,第一种公式是:
f(n) = f(n-1) * (4 * n - 2) / (n + 1)
我个人觉得这个公式不太好记。另一个公式是:
这个递推式相对好记一点:即C(n) = C(0)*C(n-1) + C(1)*C(n-2) ... C(n-1)*C(0)
以下是用该递推式实现的答案:
1 |
/** |
2025-04-06 11:20:47
枚举就是把所有情况都尝试一遍。比较简单的用 for 循环即可,较复杂的枚举,需要用到递归。
此题直接枚举每个合数拆解成两个质数和的所有可能性。为了避免重复计算质数,我们用一个 map 将其运算结果保存下来。
1 |
/** |
此题初看起来 N 很大,但是每种配料最多 3 克,一共 10 种,总克数最多为 30 克。所以超过 30 克的情况答案都为 0。
每种配料 3 种情况,一共 10 种配料,所以暴力枚举的时间复杂度 3^10 约为 59000,不会超时。
枚举的参考代码如下:
1 |
/** |
全排列的问题有多种写法,此题可以直接用 STL 中的 next_permutation
函数。
1 |
/** |
其实组合也可以用 next_permutation
来实现。以 n=5,r=3 为例,具体方法是:
0 0 0 1 1
,这样对应输出的是 1, 2, 3
0 0 1 0 1
, 输出 1, 2, 4
1 1 0 0 0
,输出 3, 4, 5
以下是实现代码:
1 |
/** |
更多全排列的练习:
我们先预保存下来每行的各种颜色的色块数量,这样检查的时候就可以快速求解。
1 |
/** |
直接枚举每个起点。但是 k==1
时需要特判,因为 k==1
意味着向下和向右重复计算,需要除以 2。
1 |
/** |
NOIP 2008 提高组第二题。推导如下:
枚举办法:
用以上的枚举之后,我们将所有答案输出,发现 A 其实在 N 最大(N=24)的时候也不会超过 1000,测试如下(只输出了 A<=B 的情况)。所以我们就可以将 A 的范围改小,或者直接打表输出答案。
1 |
0+8=8 |
完成的程序如下:
1 |
/** |
思路如下:
在选两根木棒的时候,我们又可以转化为:选一根木棒,然后选另一根大于等于它的木棒。
因为 a 的值在 5000 以内,而 N 最大是 10 万,所以可以把值放到一个计数的桶里面。这样枚举的时候效率更高。
解法:
参考代码如下:
1 |
/** |
NOIP 2001 普及组 题目。在暴力枚举的时候,需要记住重复的计算。
参考代码:
1 |
/** |
需要用到高精度。
参考代码:
1 |
/** |
2025-03-29 23:06:22
模拟是最有效的练习编程熟练度的基础算法,也是有效的掌握各种编程技巧的练习方式。
本文将把各种模拟技巧与题目结合,用题目带着学生掌握这些模拟技巧。
有些时候,我们在处理二维数组的时候,需要处理 x,y 坐标的边界。这样写起来会比较麻烦,但是,如果我们将数据从下标 1 开始保存,那么就人为在数据外面留了一圈缓冲带。这个时候,在处理 x,y 周围坐标的时候,就不会出现数据下标越界的情况了。
该题如果正常写,需要判断每个格子周围 8 个格子的状态。如果我们把数据从 1 开始读入,在判断的时候就容易很多。以下是参考代码。
1 |
/** |
本题也可以用包边的技巧,保证数据在检查的时候不会越界。参考代码如下:
1 |
|
此题需要求枚举从坐标(x,y)到坐标(a,b)的 1 的个数。我们先用将从(0,0)到(a,b)的 1 的个数保存在一个数组 s[110][110]
中,然后再通过容斥原理来进行快速求(i,j)到(a,b)中 1 的个数。具体方法如下:
第一步:对于每一个 s[i][j]
,满足:s[i][j] = s[i-1][j] + cnt
,其中 cnt 为第 i 行前 j 个数中 1 的个数。于是,我们就可以递推求出所有的 s[i][j]
,代码如下:
1 |
for (int i = 1; i <= n; i++) { |
以上代码使用了“包边”的技巧,因为我们下标是从 1 开始的,所以下标 i-1
不会越界。
第二步:根据容斥原理。从坐标(i,j)到坐标(a,b)的 1 的个数为:s[a][b] - s[i-1][b] - s[a][j-1] + s[i-1][j-1]
。如下图所示:
以上公式如果使用“包边”技巧,让有效坐标从 1 开始,也会帮助 i-1 的值不会越界。
完整代码如下:
1 |
/** |
有一种模拟题,要求我们把人围成一个圈,在圈上数数,然后问你数到的是谁。类似于小时候玩的“点兵点将”游戏,可能是顺时针数,也可能是逆时针数。
对于这种数数题目,最简单的做法是:直接用加减来进行目标的选择。加减之后,下标可能变负数或者超过总数,这个时候进行简单的取模调整,就可以将下标调整正常。
此题我们:
idx = (idx + b) % n;
来完成顺时针数idx = (idx - b + n) % n;
来完成逆时针数通过这样的简单的加减和取模,保证能够快速跳到目标位置,完成模拟操作。完整代码如下:
1 |
/** |
此题有个技巧:就是走的时候可能绕多圈,这个时候我们先把要走的步数模 n: step % n
, 这样就把前面的多圈跳过了,也不会把坐标减成特别特别小的负数。
参考代码如下:
1 |
/** |
更多练习:
绕圈一类的问题不止是以上那种真实的圈,也可能是像星期几这样逻辑上的圈(日期就像是一个圈,从星期一到星期日,然后又回到星期一)。
B3921 GESP202312 一级 小杨的考试这道题让我们计算日期。最简单的办法,是让星期几先落到 0-6 的表示法(0 表示星期一,6 表示星期日)。然后我们就可以用简单的加 N 天,然后模 7,快速定位到未来是星期几。对于过去,我们也可以简单通过减 N%7 天,然后减掉差的天数后 +7 再模 7,让结果落到 0-6 上。
参考代码如下:
1 |
/** |
矩阵操作这类模拟题,会要求我们在一个二维(或三维)的数组上进行各种操作,包括填充,旋转,查找,合并等。需要我们熟悉各种矩阵操作的技巧。
此题是一道基础的填充题。
示例代码如下:
1 |
/** |
此题我们需要熟练使用递归来进行标记。参考代码如下:
1 |
/** |
蛇形方阵是一道基础题,用于练习二维数组上的操作。我使用的模拟技巧是:
相关代码是:
1 |
int order; |
每次移动,先判断是否越界或者已经填充过值:
关键代码如下:
1 |
if (nx < 1 || nx > n || ny < 1 || ny > n || tu[nx][ny] != 0) { |
因为要填充 n*n
个数,所以循环一共执行 n*n
次。
完整的参考代码如下:
1 |
/** |
本题涉及矩阵的旋转,实际操作起来还是有点麻烦。这里我们按旋转的中心来重建坐标系的话,可以观察到如下规律:
(a, b) -> (b, -a)
(a, b) -> (-b, a)
这样,我们就可以构建关键的旋转代码了,假如我们是基于中心点 (x, y)
半径是 r 的顺时针旋转的话,那么,对于坐标 (a, b)
,我们:
(x, y)
为中心:(a-x, b-y)
(b-y, x-a)
(x, y)
的偏移,还原成原始坐标:(b-y+x, x-a+y)
以上逻辑写成代码是:g[b-y+x][x-a+y]=f[a][b]
同理,如果是逆时针旋转:
(x, y)
为中心:(a-x, b-y)
(y-b, a-x)
(x, y)
的偏移,还原成原始坐标:(y-b+x, a-x+y)
以上逻辑写成代码是:g[y-b+x][a-x+y]=f[a][b]
本题保证了数据不会在旋转时越界,整体的参考代码如下:
1 |
/** |
此题需要推导矩阵旋转的规律。我们可以把原坐标和新坐标写下来,做成一个表格。
然后,我们把坐标的变化写成下面的表格形式:
通过观察,我们发现:
新 x = 原 y
原 x + 新 y = n - 1
=> 新 y = n - 原 x - 1
综上,坐标变换公式为:新(y, n-x-1)=原(x, y)
。
所以,坐标变换相关代码为:
1 |
for (int x = 0; x < n; ++x) { |
与此类似,我们可以推出“反射”的代码关系是 新(x,n-y-1)=原(x,y)
,相关变换代码为:
1 |
for (int x = 0; x < n; ++x) { |
完整的参考代码如下(可以把 debug
变量设置成 true
来查看执行过程):
1 |
/** |
更多练习:
游戏模拟类的题目通常会告诉你一个相对复杂一点的游戏规则,然后让你用程序将这个游戏规律实现,最终将游戏的结果输出出来。
这种题目一方面考查了读题能力,需要对游戏规则的理解清楚,另一方面则是要对游戏规则进行建模,用合适的数据结构实现游戏中的模拟。
以下是一些相关的题目。
题号 | 描述 |
---|---|
P1042 | NOIP 2003 普及组 乒乓球 |
P1328 | NOIP 2014 提高组 生活大爆炸版石头剪刀布 |
P1518 | USACO2.4 两只塔姆沃斯牛 The Tamworth Two |
P1089 | NOIP 2004 提高组 津津的储蓄计划 |
P1161 | 数组标记 |
此题的解法是:构造一个“滑动的窗口”。先求出前 m 个数的和,这相当于窗口的原始位置。之后每次让窗口往右移动一格。每次移动的时候,会将最左侧的数字剔除,同时纳入一个新数字。如下图所示:
我们在滑动窗口的时候,需要用这个变量,分别指向:
以下是关键代码:
1 |
p1 = 0; |
完整代码如下:
1 |
/** |
有一些模拟需要我们有比较复杂的输入和输出操作技巧。在模拟输入和输出的时候,常用的两个函数是 sscanf
和 snprintf
,其中:
sscanf
允许我们从一个字符串中读入内容。snprintf
允许我们将输出内容先输出到一个字符串中。以下我们用例题来演示其用法。
此题的输入长度不固定,我们需要先判断输入的长度。同时,输出的时候,我们还需要输出“输出内容”的长度。这对我们处理输入和输出都带来了挑战。
我们可以把表达式整行整行读入,再用 sscanf
和 snprintf
来进行分析处理。以下是相关的示意:
1 |
int a, b; |
另外,我们还需要一次读入一整行,我用的方法是用 scanf
, 代码如下:
1 |
scanf("%[^\n]", s); |
需要注意,以上代码每读入一行,需要用 getchar()
将行末的换行给读掉。
我们也可以用 cin.getline(s, sizeof(s));
来读取数据。
以下是完整的示意代码:
1 |
/** |
以上的 scanf 部分如果替换成 cin,示意代码如下:
1 |
int main() { |
此题是 NOIP 2009 普及组的题目。此题练习了 snprintf
的使用。同时,此题用 printf 的 %+d
可以保证正数输出带+号。
1 |
/** |
此题的技巧是利用递归来循环处理。特殊情况是 2^1 写作 2,而不是 2(0)。参考代码如下:
1 |
/* |
更多练习:
待补充。
题号 | 描述 |
---|---|