预期的难度设定(同档难度按照编号排序)—— IG 有所偏离

  • 签到题:E

  • 较易题:CJK

  • 中等偏易题:AFM

  • 中等偏难题:DHI

  • 较难题:BGL

实际通过率情况:

完成提交的队伍共计 支,至少通过一题的队伍共计 支,占比

通过题数 1 2 3 4 5 6 7 8 9 10 11 12 13
队伍数量

Fun fact: 所有 tester 在 小时都没有成功的过题(我愿称之为三四魔咒),而在 小时通过的题都是 D 。

编号 题目标题 总通过队伍数 提交通过率(正确提交 / 总有效提交) 队伍通过率(通过队伍 / 总尝试队伍)
A 加法和异或 43.18% (95 / 220) 10.92% (95 / 870)
B 见好就收 6.06% (2 / 33) 2.11% (2 / 95)
C 是毛毛虫吗? 80.72% (360 / 446) 31.69% (360 / 1136)
D 几只毛毛虫? 73.68% (14 / 19) 20.90% (14 / 67)
E 三角形 98.91% (723 / 731) 89.26% (723 / 810)
F 能有多乱? 70.73% (58 / 82) 22.14% (58 / 262)
G 神秘的竞赛 16.67% (5 / 30) 5.43% (5 / 92)
H 体测 42.11% (16 / 38) 9.88% (16 / 162)
I 春游 9.09% (1 / 11) 4.55% (1 / 22)
J 冷酷的数 94.40% (658 / 697) 46.97% (658 / 1401)
K 友善的数 41.91% (259 / 618) 5.36% (259 / 4828)
L 我的地盘 0.00% (0 / 7) 0.00% (0 / 57)
M 光纤 68.67% (57 / 83) 24.26% (57 / 235)

题解部分

签到题

E. 三角形

题意: 三种边长可以拼成多少种三角形?

既然我们有边长的列表 ,我们不妨用 表示一个三角形,我们只需三重循环枚举所有可能的 再判断能否构成三角形即可。

判断条件只需用最短的两条边和第三条边比较即可,即

简评: 最好的算法是遍历。

Tester info: 这题所有 tester 都在 12 分钟内无伤通关了。(虽然题序跟正赛不一样)


较易题

C. 是毛毛虫吗?

题意: 对于一棵树,如果存在一条路径,使得不在路径上的点到路径的距离都不超过 ,则是毛毛虫。判断一棵树是否是毛毛虫。

实际上这个定义是早就出现过的,也可以搜索 Catepillar Graph 获取相应的信息。

不得不承认的是,这题是一道经典的原题,如果你善用搜索应该能找到。但本题的出现只是为了引出后一题的计数问题。

接下来回到题的解法。既然定义的核心是一条路径,我们想想这条路径是什么。

容易发现,对于一条毛毛虫而言,这条路径一定可以选为这棵树的直径。具体说明:如果选的路径两端并非度数为 的结点,则可以进一步往外增长这条路径;如果两端度数都为 ,则这条路径一定是整棵树内最长的一条路径,即直径。

对于每一棵毛毛虫树,任选一条直径也都是满足条件的路径。

于是找到树的直径,对直径上的点进行标记,再查看是否每个不在直径上的点是否都距直径距离为 即可。

时间复杂度为

另解: 事实上,只要有一个点连出了三条没有公共边的长度为 的路径,就一定返回 'NO' 。所以只需查看每个点的邻接节点中度数大于等于 的点的个数,一旦个数不小于 ,返回 'NO' 即可。Python 等语言用这种方法就完全无需担心时限了。

简评: 引子罢了。


J. 冷酷的数

题意: 给定两个数 ,构造一个非 的数与 都互质。(新增了条件:在 范围内找,Tester 做的是原题)

本来是没有加后面这个条件的,但是发现跟牛客寒假第一场第一题有撞车嫌疑,于是改题了。我们先聊聊原题。原范围是输入 ,输出

这题有很多种不同的方法。下面简单介绍其中的三种。

构造 1: 对于一个数而言,其 倍加 一定和它互质(可以考虑辗转相除法)。于是直接输出 。类似地,也可以输出

由于正整数关于 取模的结果是循环的, 一定是整体的循环节,于是 这个构造也可以理解为在 这个合法构造的基础上,过了一个循环节。

构造 2: 因为 的数据范围较小,所以直接输出一个大素数就行,比如

构造 3: 如果你上面两个方法都没想出来,也可以直接枚举从 开始的每一个数,看它是否满足条件。因为两个数的总质因子数量不多,所以一定存在一个很小的质数与已知的两数互质,于是一定能在这个数前退出循环,复杂度仍然满足要求。如果循环结束了还找不到答案,就输出

任何一种都是比较容易完成代码的。

对于新的一题: 使用上述构造 3 的思路,直接暴力找到第一个满足条件的数即可,注意提前退出。事实上也可以只使用质数来进行构造。最多只需查到第 个质数 ,时间复杂度满足要求。

简评: 冷酷的数还是太温柔了。


K. 友善的数

题意: 输入两个正整数 ,输出一个最小的正整数与它们都不互质。

首先,如果 中存在 ,显然无法构造,应直接输出

否则一定可以构造,至少 是满足条件的。

一种直接的想法是直接对 进行质因数分解,从小到大遍历所有因子查看是否满足要求。这种做法不是预期解,不清楚是否能够通过,也没有尝试造卡这种解法的数据。

另一种想法是进一步思考问题:不互质意味着构造的数至少有 的一个质因子,也至少有 的一个质因子。

如果这两个质因子不同,我们应该选择 的最小质因子和 的最小质因子进行相乘。

否则,我们选择的数是 的质因子,也必然选择最小质因子。当然 互质时,无法用这种方式构造。

上述两种方式取最小值即可。取一个数的最小质因子可以使用筛法预处理得到。

值得注意的是,输入两个质数时,需要输出两个较大的质数的乘积,结果会突破 int 的限制,所以请记得开 long long 。

时间复杂度为 ,前半部分是筛法(当然你可以选择线性筛),后半部分来源于求最大公因数。

简评: 求同存异。

Tester info: 这题差点没有一发过的 tester —— 位朋友中, 位都没有在 时取最小值而只是找了 的最小质因子;第一次问 DeepSeek-R1 时,它犯了同样的错误,且在我告诉它 “存在反例” 时质疑我弄错了答案。另外错了两次以上的朋友一般都输出过 相关的变量,但这题似乎跟 并没有关系。


中等偏易题

A. 加法和异或

题意: 两个 位以内的二进制整数,给定异或结果 ,两数和的二进制表示有 个位是 ,求满足条件的二进制整数对数。两数交换认为是相同的数对。

时,两数一定不相等,因此考虑认为 不相同的问题,最后除以 即可。否则, ,答案无需调整。接下来不考虑 “两数交换认为是相同的数对” 这一条件。

根据一个数以及异或的结果,可以直接推出另一个数,进而可以直接推出两者的和。因此这是一个只涉及一个整数的数位 DP 问题。

但求和后的二进制数有几个 并不能直接得到刻画。对于每一位而言,求和后是否为 受到两个因素影响:这一位本身的求和与下一位是否发生进位。

于是设计 DP ,从最高位考虑到和的第 位,有 个位是 ,且下一位发生进位的状态是 (如果发生进位则 ,否则 )。

如何考虑状态转移呢?枚举第一个数在第 位的取值是 还是 ,以及更下一位是否发生进位,则可以很方便地确定当前位是 还是 且当前位是否发生进位。此时再枚举两数之和到前一位为止的 的数量即可。

注意,加法可能进位,使得两数之和从低到高的第 位是 ,因此可以考虑从第 位作为 DP 起点,满足

同时,遍历到最低位时,不可能再从后面进位,因此所有从后面一位进位的方案都是不合法的。因此最终只需输出 即可。

时间复杂度为 。可以类似滚动数组的实现。

另外,这题从最低位开始 DP 更符合直觉且更容易完成代码,相当于模拟了加法计算中的进位过程。想过对数对中的数添加上界来增加这种做法的难度,但感觉从低位往高位数位 DP 也是有点意思的思路,就没有进行改动。

简评: 小学就觉得加法进位超难。


F. 能有多乱?

题意: 取一个数组的子数组,求打乱后的期望逆序对数量乘以数组长度的阶乘。多次查询。

先不考虑多次查询的事。我们只考虑求一个数组打乱后的逆序对数量的期望。

考虑数组中的任意两个元素对结果的贡献。假设两个元素是 ,则 前的概率等于 前的概率。于是,只要这两个元素数值不同,产生逆序对的概率就是 ,也就是期望产生 个逆序对 。而如果这两个元素相同,那么无论如何无法产生逆序对。

所以一个数组打乱后的逆序对个数的期望,等于数组中不同元素对的个数除以

而接下来就是区间查询这件事。我们考虑求反面,即相同元素对的个数。

区间内,一定是一系列完整的相等数字段 + 两端。我们只需二分找到完整段的起点和终点,利用前缀和计算这些完整段的相等元素对个数,再加上开头和结尾两段的相等元素对个数即可。

时间复杂度为

需要注意的还有一件事:这题的 的最大数值是 ,因此取的子数组的长度最大是 ,需要预处理到 的阶乘的取模的结果。

简评: 确实,能有多乱?


M. 光纤

题意: 在一个圆柱体内粒子不断在侧面反射,且反射 次后从对侧射出。其平行于底面的速度分量方向和大小确定,求垂直于底面的速度分量的大小的取值范围。

注意到,垂直于底面和平行于底面的运动是相互独立的,因此分开考虑即可。

而垂直于底面的速度一直保持不变,而总路程为 ,因此只需计算出运动的总时间即可。

关于粒子第一次碰撞所需的时间,只需使用 求解二次方程即可。

之后每一次碰撞后的路径到圆心的距离都相等,因此之后,相邻两次碰撞所需的时间都是一样的。实际上用时也就是上面那个二次方程的两根之差(该方程的两个解就是相邻两次碰撞的时刻)。

设第一次碰撞的时间为 ,后面相邻两次的碰撞之间的时间间隔为 ,而碰撞次数是 次,意味着总用时在 之间。于是用 分别除以两者即可得到速度的范围。

时间复杂度为

Fun fact: 这题一开始没加 ,但发现第一次碰撞时间可能过短,这种情况下会导致除以这个时间的精度误差过大,加上 就应该没有太大的精度问题啦!一开始也不想出几何题了,但是主队说不想做几何,我就叛逆啦!

简评: 降维打击。


中等偏难题

D. 几条毛毛虫?

题意: 毛毛虫定义同 F 题,问一棵树有多少子图是毛毛虫。

还是跟 F 一样,我们主要关注的是中间那条路径,也就是毛毛虫的直径。

这里我们强行去掉直径的两个端点。即使得我们选择的直径长度减

此时特判两种情况:大小为 的毛毛虫和直径为 的毛毛虫。

对于前者,每条边对应一条毛毛虫,结果为

对于后者,我们枚举直径终点,我们最终的树一定由这个点引出若干条边(边数不小于 )。设点的度数为 ,则计数结果为 ,即所有方案 减去选 条边的方案。

否则,去掉直径的两个端点后,还有一条长度为正整数的路径。而对于路径上的每一点,有多少种扩充成毛毛虫的方法呢?

对于路径的端点,我们至少得往外多连一条边。设度数为 ,则非路径上的边的数量为 ,因此总方案数为

对于路径上非端点的点,其可以任意往路径外连边。设度数为 ,则路径外的边有 条,因此总方案数为

对于每条路径,我们需要对上述结果乘积后求和,最后再对所有路径求和。

容易想到树上 DP 。既然我们前面已经提到了只考虑中间的一条 “关键路径” ,我们就在有根树上,计算从 位置往下的所有 “关键路径” 能长成多少种不同的毛毛虫段——我们称为 “段” 。(即从 “关键路径” 从子树中的一个位置开始,到 尚未结束的这部分的方案数)

这件事的维护可以使用树上 DP ,将子结点的结果乘以当前结点作为 “路径上非端点的点” 的方案数,再加上子结点作为 “路径的端点” 的方案数,即可完成转移。

接下来考虑毛毛虫的计数,考虑每一条毛毛虫对应的 “关键路径” 。

如果它的两个端点一个是另一个的祖先,则设两个端点为 ,且 的祖先,则此时考虑 的也是 祖先的结点 ,我们可以根据 的毛毛虫 “段” 数,乘以 点的可选方案数,进而得到这种路径的方案总数。

否则,两个端点 的 LCA 不为 中任何一个,设 LCA 为 ,则相当于 出发选择了两个子结点,两个子结点对应的毛毛虫 “段” 数相乘(需要加上子结点作为 “关键路径” 端点的情况),再乘以 本身作为 “路径上非端点的点” 的方案数。最后这部分可以一起乘,前面部分相当于对于任意两个子结点相乘后再相加。这件事可以这么算:

在遍历子结点时维护一个前缀和即可。

时间复杂度为

抱歉这题没有提供较大的样例,主要是因为想在样例解释中提供每个子毛毛虫。如果想找到一棵树进行 debug ,可以使用前一问中的第二棵树,那棵树除了树本身都是毛毛虫,因此只需计算连通块种类数量再减去 就行。

Fun fact: 本来毛毛虫系列还有一题,是计算不同形态的有 个结点的毛毛虫数量,但计算完成后发现 OEIS 上直接就能搜到,于是放弃了。感兴趣的朋友可以自己推一推,大概需要根据对称性去重来计数。

简评: 表面上看着人畜无害的。


H. 体测

题意: 给定一个排好序的数组 ,在数组中插入最少个数的 范围内的整数,使得数组中两数之差可以覆盖 中的每一个数。

应该是中等题里比较容易意识到可做的一题,也容易以为实现十分容易进而被识别为简单题,但本题相当容易得到 WA 。

题目中给了我们一个提示:我们有时候可能在添加一个数字后同时满足多个要求。

而我们总共需要满足三个需求。考虑需求是如何满足的。注意到 ,因此可以将 个要求捆绑为一组;也可以将 个需求分为两组,一组 个需求,一组 个需求,每一组的需求一起满足;也可以选择每一个需求都单独满足。

因此需要完成以下几个步骤:

  • 满足 个需求:直接检查是否有两个数差是满足需求的,如果有,返回 ,否则返回

  • 满足 个需求:两个条件同时满足,只能是存在 的间隔。(或者拆为 ,这种情况已经判断过了,不作考虑。)注意两个数间隔 时候不总能满足要求,需要构造的新点仍在 范围内。

  • 同时满足 个需求。一定有一个已有的点作为某一段的起点 / 终点。从这个点出发进行 DFS ,可以确定剩余三个要找的点的所有可能,只需检查每一种可能下,与已有的数字重合的个数即可。

枚举所有分割方式,取最小答案即可。时间复杂度为 ,看你判断过程的复杂度。上述状态搜索中,实际的不同状态不会超过 种,是较大的一个系数。

Fun fact: 本次小羊杯题序原来是随机的,这题被随机到了 A 的位置,为了防止大家起手就做这道容易写假的题,就和签到题互换了位置。希望有效提升了大家的做题体验并降低大家的罚时。

简评: 又一个表面上看着人畜无害的。这一切都是包容的代价。(你猜猜这题为啥给了 10 秒.jpg)

Tester info: 位测试者中 位积极避开了这道题,另一位朋友因为这题卡了 小时(使用了分类讨论的做法),最终纯分类讨论达到了约 毫秒的优秀用时,让我们向其致以敬意!

#include <bits/stdc++.h>

using namespace std;

int t;
int n, a, b, c, m;
int p[100005];

bool check1(int x, int L = 0, int R = m) {
	if (x <= 0)
		return false;
	for (int l = 0, r = 0; r <= n; r++) {
		while (p[r] - p[l] > x)
			l++;
		if (p[r] - p[l] == x && (L <= p[r] || p[r] <= R))
			return true;
	}
	return false;
}

bool check2(int x, int y, int L = 0, int R = m) {
	for (int i = 0, l = 0, r = 0; i <= n; i++) {
		while (p[i] - p[l] > x)
			l++;
		while (r <= n && p[r] - p[i] < y)
			r++;
		if (p[i] - p[l] == x && r <= n && p[r] - p[i] == y && L <= p[i] && p[i] <= R)
			return true;
	}
	return false;
}

bool check3(int x, int L = 0, int R = m) {
	if (x <= 0)
		return false;
	for (int l = 0, r = 0; r <= n; r++) {
		while (p[r] - p[l] > x)
			l++;
		if (p[r] - p[l] == x && L <= p[r] && p[r] <= R)
			return true;
	}
	return false;
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	for (cin >> t; t--; ) {
		cin >> n >> c >> a >> b >> m;
		for (int i = 0; i <= n; i++)
			cin >> p[i];
		vector<int> v;
		if (!check1(a))
			v.push_back(a);
		if (!check1(b))
			v.push_back(b);
		if (!check1(c))
			v.push_back(c);
		if (v.size() < 2)
			cout << v.size() << '\n';
		else if (v.size() == 2)
			cout << (check1(v[0] + v[1]) || check1(v[1] - v[0], v[1], m - v[0]) ? 1 : 2) << '\n';
		else if (check2(v[1] - v[0], v[2] - v[1], v[1], m) || check2(v[2] - v[1], v[1] - v[0], 0, m - v[1]) ||
				 check2(v[0] + v[1], v[2] - v[1]) || check2(v[0] + v[1], v[2] - v[0]) || check2(v[0] + v[2], v[1] - v[0]) ||
				 check2(v[2] - v[1], v[0] + v[1]) || check2(v[2] - v[0], v[0] + v[1]) || check2(v[1] - v[0], v[0] + v[2]))
			cout << 1 << '\n';
		else if (check1(v[0] + v[1]) || check1(v[1] - v[0], v[1], m - v[0]) ||
				 check1(v[0] + v[2]) || check1(v[2] - v[0], v[2], m - v[0]) ||
				 check1(v[1] + v[2]) || check1(v[2] - v[1], v[2], m - v[1]) ||
				 check1(v[0] + v[2] - v[1], v[2], m - (v[1] - v[0])) || check1(v[1] + v[2] - v[0]) || v[0] + v[1] == v[2] ||
				 check3(v[0] + v[1] - v[2], v[0], m - (v[2] - v[0])) || check3(v[0] + v[1] - v[2], v[1], m - (v[2] - v[1])) ||
				 check1(v[2] - v[0] - v[1], v[2], m - v[0] - v[1]) || (v[0] + v[1] <= m - v[2] && check1(v[0] + v[1] + v[2])) ||
				 check3(v[2] - v[0] - v[1], v[2] - v[1], m - v[1]) || check3(v[2] - v[0] - v[1], v[2] - v[0], m - v[0]) ||
				 check1(v[0] + v[1] - v[2], v[0] + v[1], m - v[2]))
			cout << 2 << '\n';
		else
			cout << 3 << '\n';
		
	}
	return 0;
}


I. 春游

题意: 目前有一些人在无向连通图的某些结点上,每条边从 时刻才能同行且单位时间通行一人,问所有人都到达 结点的最早时间。

假设每条边没有通行时间的限制,此时是一个可以用网络流解决的经典问题。下面进行简单说明——

可以证明,最早到达时间不超过

  • 具体而言,我们可以去掉边,使得图仍然保持连通,直到图变成一棵树。在删边的过程中,到达 的用时是单调不减的(因为可行的路径变少了)。

  • 按照与结点 的距离进行排序,越近的越先行动。我们让每一个人一旦出发就不停下,求最晚的出发时间。可以用数学归纳法证明第 个人的出发时间不晚于 ,因此第 个人出发时间不晚于 ,因此至多需要 的时间就能所有人聚集到结点

进一步地,考虑二分最早集合时间。对于每个集合时间 可以使用网络流检查。

我们对图上的每个点 在不同时刻 设立一个网络流上的结点

则因为点待在原地没有限制,因此 可以以无限流量流到 ,而对于任意一条边 ,都可以让一个人在单位时间内从 走到 ,因此连边 ,容量为

但一条边我们既让 ,又让 ,会产生矛盾,即一条边上同时有两人通行吗?答案是否定的,因为同一时刻如果两个方向都走了,就相当于都没走(有点废话了)。

只需在最后时刻汇流到 即可,流量必须等于总人数。

于是我们解决了无通行时间限制的问题。

有通行时间限制的话,我们的集合时间就可能很晚了。

我们仍然考虑使用二分。但这样我们就不能建一个完全的 时刻到 时刻的网络流,怎么办呢?

直觉是,如果一条边相较于 时刻很早就开放了,那么它其实早开放也没用,不如干脆等到 再开放也来得及( 是一个设定的值,表示这条边最长实际有效的使用时间)。

利用这一点,我们将网络流从 进行建图,即可完成,且点数不会过多。

这里, 大概是 的量级,从直觉上解释,因为可以先花 的时间通过那些有效时长较长的边走到各自的中转站,再用 的时间走剩余有效时长较短的边。(事实上,由于出题人不太想得清楚极限情况,这里只要是 量级的基本都可以过)于是网络流中结点个数为 ,边数为

值得注意的是这个图流量很小,是 的,所以网络流复杂度只需考虑 轮在残留网络中找源到汇的路径,因此可以控制在

再搭配二分,时间复杂度需多一个 。总复杂度为

这里的 可以进一步优化。我们可以找到使得整个图的 “最小生成树”,则 “最小生成树” 中最大边权的边对应的时间 时间内,一定能完成任务(这里的 “最小生成树” 指将 和目标点相连的最大边权最小的树);而如果时间小于这个最大边权,一定不满足要求。因此我们将实际答案缩小到一个 长度的区间,这样这个 就被优化为 ,但这步优化不是必要的。

由于出题人没有什么网络流题目的出题经验,所以数据可能不够强,主要只卡了二分上下界出错的事情,请多包涵。

简评: 希望大家有空多出去走走!

Tester info: 有测试者用随机算法随机了 次,但是被一个神秘 case 单防了;有测试者这题因为使用的网络流板子太慢所以 TLE 了,比较可惜(但同时回推时间没有达到 所以也没有那么可惜),但因为这题 case 数量太多,所以就并没有办法开再大的时限啦!


较难题

B. 见好就收

题意: 已知一枚硬币正面朝上的概率为 次查询在 次反面前扔出 次正面的概率。

单次查询如何计算结果呢?

事实上,我们可以强行让小羊扔 次硬币,如果扔到了 次及以上的正面,我们就拿第 次正面前所有抛硬币结果作为真实结果。发现这跟原命题概率一致。

因此,要求的就是 次抛硬币中,至少抛出 次正面的概率。

用二项式定理计算对应结果,为

直接使用二项式定理计算这个概率是 的,并不符合要求。因此需要考虑多次查询之间存在的联系。

不妨设 次抛硬币中,至少抛出 次正面的概率为

注意到 上面的求和式都只跟 的差了一项,因此都可以在知道 的情况下 计算。

接下来考虑 ,即 次抛硬币有至少 次正面朝上。

考虑前 次,如果至少有 次正面朝上,则概率为 ;否则,要 次抛硬币有 次正面朝上,只能是前 次有 个正面朝上且又抛出了正面,因此有:

类似地,也可以得到 的关系。(这个式子也可以通过组合数的计算得到,但相对麻烦且需要比较好的数学观察)

视为一个二维平面上的点,则在这个平面上,上下左右移动一个单位的计算成本都是 ,我们要求这个平面中 个位置的数值。这就是莫队的思想,因此直接使用莫队算法即可。注意我们输出的变量不是概率,还要乘以 。(抱歉是我偷懒了,我不想解释分数的取模的含义)

当然,这题到这里还没有结束所有的坑点。

首先,这题涉及到组合数的计算,而组合数取模的数可能是一个不大的质数,因此分子分母可能都是这个质数的倍数。

此时,我们可以预处理每个数含该质因子的次数并求前缀和,这样我们就能得到一个数的阶乘中这个质因子的出现次数,进而得到分子、分母中该因子的出现次数以判断取模结果是否为

对于剩余的组合数,如何计算取模结果呢?我们直接把 中所有 因子都去掉求出取模结果,得到最大的数 的阶乘去掉质因子 后关于 的取模结果,再求逆元得到 ,再往回推即可。后半部分的逻辑类似于常规阶乘的逆元的求法

最后,使用莫队时,容易出现 不合法的情况(如 ),保证答案正确的一种处理方式是,先尽可能让 变大, 变小,再执行 变小, 变大的操作。这样可以保证中间的每一步都是合法的。

时间复杂度为

上面涉及到的一个经典问题是求 ,这个问题也可以简单想下咋写比较好。

简评: 小羊想见好就收,但为了做出题我们偏不让他见好就收。


G. 神秘的竞赛

题意: 道题,做完一题得 分,每一题要 单位时间。总共 单位时间。在 时间内,每一次成功提交有奖励分 分 ,而每做一道题就立即交,问最后最高的总得分。

我们需要关注到几件事情:

  • 我们题数为王。不然我们基础分少了一题的话,附加分也救不回来。

  • 在题数相同的情况下,我们总选择最简单的题来做,因为这更有利于我们分配时间。

  • 我们没必要在某一个非整数的时刻开始做题——往前往后都不影响答案。

  • 我们可以从一个时刻开始,连续地做题——奖励时间我们不应该浪费,如果没用满可以平移前面的做题动作 / 后面的做题动作。而非奖励时间可以任意平移不影响答案,因此干脆平移成一段连续的时间做题。

于是我们先将题目按照用时升序排序,选取最长的和不超过 的前缀。这就是我们真正要选择去做的题。接下来我们认为 是我们实际要做的题数, 是这些题分别需要的用时。

而我们多余的时间总共有 ,因此我们可以选择开头就休息 中的一个整数时间,再连续做题。

那么,怎么分配题目和做题顺序呢?

考虑最后一次得到附加分的时刻。在这个时刻之前,为了得到最多的附加分,我们应该先做用时长的题,再做用时短的题,这样可以把前面没有奖励分的时间尽快用完。

于是,可以将题目按照所需时间逆序排序,根据逆序序列的一个子序列,安排完所有拿附加分的事项,最后再任意排列剩余的所有题目。

后半部分不影响答案,因此,只考虑附加分前的部分,这部分可以使用背包 DP 。下面的讨论已经将题目按照用时降序排列。

假设考虑到第 题时,用时 的最大附加分为 。考虑第 道题,如果做了的话,总用时为 ,因此 可以更新为

但这样做的复杂度仍然有 ,无法通过本题。

但注意到 ,因此不同的 不超过 个,我们将这些 进行合并。

我们同时考虑一批相同用时的题目进行状态转移,就可以用单调队列优化这个类背包 DP 的转移过程。假设是从 位置转移到 位置,则新增的数量就是 之间完成题目得到附加分的时刻数量,这可以拆解为 之前完成题目得到附加分的时刻数量减去 之前完成题目得到附加分的时刻数量。

因此单调队列只需维护此前 位置的 DP 数值减去 之前完成题目得到附加分的时刻数量的最大值即可。

时间复杂度为

简评: 要是小羊能有点策略,水题做完一起交就好了。

Tester info: 有测试人员就差最后发现 意味着不同的 数量不多了,非常可惜啊!本题本来还加了限制 ,一旦这个条件直接给出了就很容易被提示到发现端倪了!


L. 我的地盘

题意: 一个 的矩阵,按照一个规则,将部分格子进行染色,染的颜色单调不减,求每一时刻的最大连通分量的大小。

本题实际上是按照不同的颜色分别考虑。

对于某一种颜色,实际上经历了以下过程:先可能被更小的数字染色,再有一些新格子变成当前颜色,最后又有一些格子被染为更大的颜色。

整体只有三次单调的变化。先减后增再减。

而求连通块大小的常见做法是使用并查集,并查集在增的时候更好用,于是考虑以前两轮变换的中间时刻为起点,往前往后推断前两轮变换的每一个时刻该颜色连通块的最大大小;同时从最终时刻开始,往前递推,得到第三轮变换的每一个时刻该颜色连通块的最大大小。

这样,格子在三段中,按照对应的方向进行遍历,就是不断新增的了,维护最大连通块就手到擒来了。

注意到,涉及到第 种颜色的格子数量等于原矩阵中 颜色的格子数量加上涉及到第 种颜色的操作数量。而这个变量关于 求和是 的,因此,总共需要考虑的格子的数量是不会超标的。

但这也意味着我们需要对于每个颜色进行维护时细致操作,保证维护的并查集大小等于涉及到该颜色的格子数量。

同时,对于某种颜色而言,时间不是完整的 而是离散的,因此我们实际上算的是其中一段区间上,该颜色的最大连通块。于是我们要对结果数组的 关于某个数值 取最大值。

由于这里 的数据范围是不超过 的,因此可以从大到小遍历 ,找到某一个 对应的所有赋值操作,对区间内尚未被赋值的位置进行赋值。

上述过程相当于经典问题区间染色,可以使用并查集解决。

时间复杂度为

简评: 这题疑似容易想到线段树分治?

致歉: 由于线段树分治并未用到染色递增的关键性质,且理论复杂度相对高一些(有一个可撤销并查集的 与线段树分治的 ),同时编码难度相对低一些,因此本题时间限制对于线段树分治而言相对较紧,但标程实际用时在 1200 ms 以内。

不过,本题对于这点也在题面中给出了一定提示:本题题目描述中明确加粗了染色的递增性质,数据范围中也明确指出了 递增并加粗了,都是为了提醒尝试写线段树分治的朋友——是否有什么没用到的东西?

这题如果实现得当,Pypy 也是可以通过的,虽然跑的并不快,大概用时 秒。

至此,所有题都可以用 Pypy 通过!你 Py 又赢!