T1
能量水晶
crystal
思路偏了,样例是凑出来的
0 / 100
题意:n 颗水晶,最多 m 次「把某颗 +1」,求最多几颗能变成同一个值。\(n\le 5\times10^{5}\),\(m\le 10^{18}\),能量值可为负。
浩然当时在算什么: 先排序,再对 \(s=0\ldots m-1\) 找满足 \(a[j]+s=a[j+1]\) 的相邻对 ,每命中一对 \(\text{num}\gets\text{num}+1\) 。而题目要的是「选一个目标值,看一个区间里有几颗数能补到它」——「数配对」和「数个数」是两件事 ,这个 num 无论怎么算都推不出正确答案。
错在哪几行: ① 第 13 行 \(\texttt{sort(a, a+1)}\) 只覆盖 \(a[0]\),数组从未排序,「相邻」这个前提不成立;② 第 14、16 行循环上界是 \(m\) (最长 \(10^{18}\)),不是 n(最长 \(5\times10^{5}\));③ 第 17 行 j 走到 m 时读 \(a[j+1]\) 越界;④ 第 19 行 \(m\gets m-s\) 在循环里改上界,第 15 行 \(s=i\) 在 i 超过 \(2^{31}\) 后溢出;⑤ 最要命的一条——末尾的 \(\text{num}+1\) 是拿输出反推出来的 。
浩然的原文(crystal.cpp 第 13–23 行)
sort (a, a + 1); // 第 13 行:区间只含 a[0]
for (int i = 0; i < m; i++) { // 上界是 m,不是 n
int s = i;
for (int j = 1; j <= m; j++) { // j = m 时 a[j+1] 越界
if (a[j] + s == a[j + 1]) { // 数的是"配对",不是"能凑几颗"
num++;
m -= s; // 循环上界被就地改掉
break ;
}
}
}
if (num != 0) { cout << num + 1; ... } // 第 24 行:这个 +1 从哪来?
官方样例与最小规模输入上的实际输出
输入 正确答案 输出 说明
官方样例 1 3 3 num = 2,加 1 才对
官方样例 2 4 4 num = 3,加 1 才对
n = 1,m = 0(题面允许) 1 0 m = 0 时循环一次不进
$ 断点停在第 24 行之前,看 num
num = 2 ← 样例 1;+1 之后输出 3
num = 3 ← 样例 2;+1 之后输出 4
$ 再跑 n=1, m=0
0 ← 应为 1:一颗水晶自己就成一组
这道题该怎么想:先定目标值,再数区间(\(O(n\log n)\))
sort (a + 1, a + n + 1);
for (int i = 1; i <= n; i++) pre[i] = pre[i-1] + a[i];
int l = 1, ans = 1;
for (int r = 1; r <= n; r++) {
// 把 [l, r] 全部补到 a[r],需要多少步
while (l < r && (long long )(r-l+1) * a[r] - (pre[r] - pre[l-1]) > m) l++;
ans = max (ans, r - l + 1); // 区间长度 = 能凑成同一值的颗数
}
cout << ans;
核心式子:把区间 \([l,r]\) 全部补到 \(a[r]\),要花的步数是
$$cost(l,r)=(r-l+1)\,a_r-\sum_{i=l}^{r}a_i$$
前缀和让这个代价 \(O(1)\) 就能算出来,l 才不必回退——这也是为什么"数配对"永远得不到答案:浩然要的量是这个区间的长度,不是配对的次数。
这一课的重点不在算法,在"读自己的输出"。 写出 \(\text{num}+1\) 的那一刻,其实已经发现结果差了一点;正确反应是回去问「num 到底在数什么」,而不是给结果加一。加一能让样例变对,也能让真正的错藏得更深——这是浩然这次最该改的习惯。
知识点:枚举目标值 + 前缀和 + 双指针 易错:sort 区间 易错:循环上界取非规模变量 易错:用输出反推答案
T2
地铁
subway
没进入题目
0 / 100
题意:地铁上不花步行时间,只有进出站、换乘花时间;每次查询求两点间最少总步行时间。线路数 \(n\le 500\),每站最多 \(d\le 20\) 条线,查询 \(q\le 10^{5}\)。
浩然当时在做什么: 读 c、n、m、d、q 五个数,按 m 次读 \(w[\ ]\) 、按 n 次读 \(ww[\ ]\) ,然后把 "32" 和 "17" 打出去。这两个数正是样例 1 的答案。
两处都没走通。 读入:题面每条线路先给 \(k_i\),再给 \(2k_i\) 个数(站号、入站时间交替),浩然只按「m 个单值 + n 个单值」读,格式整个错位。处理:w[] 和 ww[] 读进来以后一次都没被用过 ,第 10 行的 \(\texttt{long long u = n}\) 也没用过——程序里没有建图、没有最短路、没有循环处理那 q 组查询。
$ 在 subway.cpp 里搜 w 的全部出现位置
第 4 行 long long w[N], ww[N]; ← 声明
第 12 行 cin >> w[i]; ← 读入
第 15 行 cin >> ww[i]; ← 读入
此后无任何引用 ← 数据进来就断了
$ ./subway ← 任意输入
32
17 ← 恒等于样例 1 的两行答案
第一步不是建图,是"把输入读对并原样打印"
struct Line { int k; int st[1005], t[1005]; } L[505];
for (int i = 1; i <= n; i++) {
cin >> L[i].k; // 每条线路先读站数 kᵢ
for (int j = 1; j <= L[i].k; j++)
cin >> L[i].st[j] >> L[i].t[j]; // 再读 2kᵢ 个数:站号 + 入站时间
}
// 先只做这一件事:把读到的内容原样打印,确认格式真读对了
for (int i = 1; i <= n; i++)
for (int j = 1; j <= L[i].k; j++)
printf("line %d stop %d time %d\n" , i, L[i].st[j], L[i].t[j]);
这一课:把"读进来"当成一道独立的题。 读不对,后面写什么都白写。等浩然能在 10 分钟内把一条线路的 \(k_i\)、站号、时间装进结构体并原样打印,再讲「线路当点、换乘当边」的建模——那才是这题的第二层。
知识点:输入格式解析 知识点:结构体存变长序列 后续:线路当点的建图 后续:Floyd / 分层图 Dijkstra
T3
迁徙
migrate
特判一条也没生效
0 / 100
题意:坐标严格递增的 N 个点,两点坐标差 \(\le d\) 时可直飞;统计「最短路恰好有两条」的点对数量。\(n,N\le 2\times10^{6}\),\(d\le 2\times10^{18}\)。
浩然当时在做什么: 读 c、t,然后每组读 n、m 和 n 个坐标,接着按 (n, m) 的四种组合各写一条 if,想用小数据特判骗点分。
两处硬伤,同时发作。 ① 第 14 行 \(\texttt{cin >> a[i]}\) :这里的 i 是外层"第几组数据",应该是内层的 j——n 个坐标全塞进 \(a[1]\),而且读完就再也没用过 。② 第 16、19、22、25 行的 \(\texttt{if (n == 4, m == 2)}\) 是逗号表达式,等价于 \(\texttt{if (m == 2)}\) ,n 的检查被求值后丢弃。③ 数组 \(N=10^{6}+10\) ,题面 n 可达 \(2\times10^{6}\),直接不够。
if ( n == 4 , m == 2 )
求值一次,结果丢掉
这个才是真正的条件
n 只在左边被读了一下,从此不参与任何判断。
$ 按代码直接推,每组数据的输出只由 m 决定
m == 2 → 第 16、19、25 行都命中 → 输出 1 / 2 / 1(三行)
m == 3 → 只有第 22 行命中 → 输出 0(一行)
其他 m → 四条 if 全不命中 → 输出文件里一个字都没有
正确答案是每组一行、共 t 行。三行、一行、零行,三种情况都对不上。
计数这件事只有一条规则:同一个距离层级上才累加 。
$$dist[v]=dist[u]+1\ \Longrightarrow\ cnt[v]\gets cnt[v]+cnt[u]$$
第一次走到 \(v\) 时直接继承 \(cnt[u]\);之后若再发现 \(dist[v]\) 恰好等于 \(dist[u]+1\),说明又找到一条同样长的路,把方案数加上去。
先把两行写对:读入下标 + 条件连接
for (int j = 1; j <= n; j++) cin >> a[j]; // 内层读入用内层变量 j
if (n == 4 && m == 2) { ... } // 多个条件一律 &&
// 再往上一层:枚举起点 BFS,记最短路长度与条数
dist[s] = 0; cnt[s] = 1;
if (dist[y] == -1) { dist[y] = dist[x] + 1; cnt[y] = cnt[x]; q.push(y); }
else if (dist[y] == dist[x] + 1) cnt[y] += cnt[x]; // 同层累加方案数
if (dist[v] != -1 && cnt[v] == 2) ans++;
这一课:特判骗分的前提是"特判条件写对"。 四条分支被一个逗号压成一条半,连"骗"都没骗成。而这四条分支,连"答案应该有几行"都没核对过 ——交卷前把自己的输出行数数一遍,这个错误当场就会暴露。
知识点:最短路计数(dist + cnt 双数组) 易错:逗号运算符 易错:内层读入用外层下标 易错:数组按小规模开
T4
研发基地
hub
这题没有动手写
0 / 100
题意:n 个点的树,选恰好 k 个点、形成不超过两个连通块,使所有点到最近选中点的最大距离最小。
这道题要最小化的量其实是一个式子:在 \(|S|=k\)、且被选中的点分成不超过两块的前提下,
$$\min_{S}\ \max_{1\le v\le n}\ \min_{u\in S}\ \operatorname{dist}(u,v)$$
正解是二分答案加树形 DP 检验;而 \(n\) 很小的一档,直接枚举 \(S\) 就能算出这个式子。
\(n\le 10^{6}\),\(t\le 5\)。
浩然当时在做什么: 9 行。开了数组、写了 freopen,然后 cout << 1 << "\n" << 2; 。c、t、n、k 一个都没读。
不是写错,是没写。 整个程序里没有一条输入语句,输出与数据无关;那两个数恰好是样例 1 的答案,说明浩然是照样例交的。这题的正解(二分 + 树形 DP)对浩然确实超纲,但题面留了 \(n\le 5\) 和 \(n\le 10\) 两档,枚举子集 + BFS 就能落地 ——这一档浩然没试。
$ grep "cin" hub.cpp
(无结果) ← 全文没有任何读入
$ ./hub ← 任意输入
1
2 ← 恒等于样例 1 的答案
最小一档够得着:枚举子集 + 判连通块 + 求最远距离
for (int mask = 0; mask < (1 << n); mask++) {
if (__builtin_popcount(mask) != k) continue ;
int comp = 0; // 选中点分成几块
for (int i = 0; i < n; i++)
if ((mask >> i & 1) && !vis[i]) comp++, dfsBlock(i, mask, vis);
if (comp > 2) continue ;
int far = 0;
for (int v = 0; v < n; v++) far = max (far, nearest(v, mask));
ans = min (ans, far);
}
这一课:不会做,也要让程序"读过题"。 以浩然现在的水平(能写 for、能写 dfs),枚举这一档是做得到的。四份代码里这份最可惜——它连失败的机会都没给自己。
知识点:枚举子集 知识点:连通块判定 + BFS 正解:二分 + 树形 DP(超纲) 现在够得着:\(n\le 10\) 那一档