0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1091,日期见页头。两边不一致时信原站。
题目描述
n 位同学站成一排,音乐老师要请其中的 n-k 位同学出列,使得剩下的 k 位同学排成合唱队形。
合唱队形是指这样的一种队形:设 k 位同学从左到右依次编号为 1, 2, …, k,
他们的身高分别为 t₁, t₂, …, t_k,则他们的身高满足
t₁ < ⋯ < tᵢ > tᵢ₊₁ > ⋯ > t_k (1 ≤ i ≤ k)
你的任务是,已知所有 n 位同学的身高,计算最少需要几位同学出列,
可以使得剩下的同学排成合唱队形。
输入格式
共二行。第一行是一个整数 n (2 ≤ n ≤ 100)。
第二行有 n 个整数,第 i 个整数 tᵢ (130 ≤ tᵢ ≤ 230) 是第 i 位同学的身高(厘米)。
输出格式
一个整数,最少需要几位同学出列。
数据规模与约定
对于 50% 的数据,n ≤ 20;对于全部的数据,n ≤ 100。
输入输出样例
输入
8 186 186 150 200 160 130 197 220
输出
4
八个人里请走 4 位(186 186 150 200 160 130 197 220 ⇒ 留下 186 200 160 130 之类)。
★ 注意这组样例里有一对相同的身高(两个 186)—— 第 ④ 步会说这一点让它挡住了什么。
同一轮的三页量的是同一件事 ——「严格 / 非严格」那个坑的抓获率
由 n / 值域 的比值决定。这道题的比值是题面直接给的:
n ≤ 100,身高 130 ~ 230(101 种) ⇒ n / 值域 ≈ 1.0⇒ 按那条曲线,这里的抓获率应该在八成上下 —— 第 ④ 步在另一道题上把整条曲线又跑了一遍, 形状完全一样。
1★ 关键一步:跑两遍 LIS
f[i] = 以第 i 个人结尾的最长严格上升子序列长度(从左往右跑一遍);
g[i] = 以第 i 个人开头的最长严格下降子序列长度(从右往左跑一遍)。
枚举最高的那位是第 i 个 ⇒ 队形最长 = f[i] + g[i] − 1,答案 = n − max(…)。
⚠⚠ 那个 −1 是这道题最容易漏的一笔:第 i 个人在 f[i] 和 g[i] 里各被数了一次。
// P1091 [NOIP 2004 提高组] 合唱队形 —— ★ 这一版就能 AC//// 题意:n 个人站一排,请走 n-k 位,剩下的 k 位要满足 `t₁ < … < tᵢ > … > t_k`// (**严格**上升再**严格**下降,`1 ≤ i ≤ k`)。求最少走几位。//// ★ 关键一步:**跑两遍 LIS**。// `f[i]` = 以第 i 个人**结尾**的最长严格上升子序列长度(从左往右);// `g[i]` = 以第 i 个人**开头**的最长严格下降子序列长度(从右往左跑一遍 LIS)。// 枚举「最高的那个人」是第 i 个 ⇒ 队形最长 = `f[i] + g[i] - 1`。// 答案 = `n − max(f[i] + g[i] - 1)`。//// ⚠⚠ **那个 `-1` 是这道题最容易漏的一笔**:第 i 个人在 `f[i]` 和 `g[i]` 里**各被数了一次**// (页面第 ③ 步量了漏掉它会怎样)。//// ★ 题面写的是 `1 ≤ i ≤ k` ⇒ **单调上升或单调下降本身也是合唱队形**// (最高的那个人可以站在最左边或最右边)—— 上面这个式子天然覆盖了这两种情况。//// ★★ 这道题的身高范围是 `130 ≤ tᵢ ≤ 230`(只有 101 种),而 `n ≤ 100`// ⇒ **`n / 值域 ≈ 1.0`** —— 按同一轮 [P1020](/sol/p1020/) 那条曲线,// 「严格 / 非严格」那个坑在这道题上**几乎必然现形**(页面第 ④ 步实测)。//// 复杂度 `O(n²)`,`n ≤ 100` 绰绰有余。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0; vector<int> t(n); for (int& x : t) cin >> x;
vector<int> f(n, 1), g(n, 1); for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (t[j] < t[i]) f[i] = max(f[i], f[j] + 1); // ★ 严格上升 for (int i = n - 1; i >= 0; i--) for (int j = n - 1; j > i; j--) if (t[j] < t[i]) g[i] = max(g[i], g[j] + 1); // ★ 严格下降
int best = 0; for (int i = 0; i < n; i++) best = max(best, f[i] + g[i] - 1); // ★ 减 1 cout << n - best << "\n"; return 0;}点「运行 ▶」看结果
2参照物:枚举所有子集,照题面那条不等式链去验
// P1091 的参照物:**枚举所有子集**,逐个检查是不是合唱队形//// ★ 它把题面那个不等式链 `t₁ < … < tᵢ > … > t_k` 照抄一遍去验,// 不需要「跑两遍 LIS」,也不用想那个 `-1`。// ⚠ 只跑得动 `n ≤ 20` 左右。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0; vector<int> t(n); for (int& x : t) cin >> x;
int best = 0; for (int mask = 1; mask < (1 << n); mask++) { vector<int> v; for (int i = 0; i < n; i++) if (mask >> i & 1) v.push_back(t[i]); int k = v.size(); // 找峰:先严格升,再严格降;峰可以在两端 int p = 0; while (p + 1 < k && v[p] < v[p + 1]) p++; int q = p; while (q + 1 < k && v[q] > v[q + 1]) q++; if (q == k - 1) best = max(best, k); // 整条链都合法 } cout << n - best << "\n"; return 0;}点「运行 ▶」看结果
它不需要「跑两遍 LIS」,也不用想那个 −1 —— 直接照抄题面那条链去检查。
300 组(n ≤ 14) |
|
|---|---|
两遍 LIS vs 2ⁿ 枚举子集 |
不一致 0 组 |
3★★ 忘了那个 −1:说清楚它「算了什么」
// P1091 错法一:忘了那个 `-1`//// 枚举「最高的那个人」是第 i 个时,他在 `f[i]`(左边那段)和 `g[i]`(右边那段)里// **各被数了一次** —— 不减掉就把他数了两遍。//// ★★ 而它错得非常规整:**队形长度恒好 1,于是答案恒少 1**// (页面第 ③ 步实测逐组相等)—— 除非 `n - best` 已经被压到 0 以下那种边界。// ⇒ 「说清楚一个 bug 算了什么」比说它「错了」有用得多。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0; vector<int> t(n); for (int& x : t) cin >> x; vector<int> f(n, 1), g(n, 1); for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (t[j] < t[i]) f[i] = max(f[i], f[j] + 1); for (int i = n - 1; i >= 0; i--) for (int j = n - 1; j > i; j--) if (t[j] < t[i]) g[i] = max(g[i], g[j] + 1); int best = 0; for (int i = 0; i < n; i++) best = max(best, f[i] + g[i]); // ← 少了 -1 cout << n - best << "\n"; return 0;}点「运行 ▶」看结果
| 300 组 | |
|---|---|
| 它的答案 恒好比正解少 1 | ★ 300 / 300 逐组相等 |
⇒ 它不是「有时候错」,是每一组都恰好少 1 —— 因为最高的那个人被数了两遍,
队形长度恒好多 1,答案(n − 队形长度)就恒好少 1。
★ 「先说清楚 bug 算的是什么,再说它错在哪」—— 本书这一轮已经用了四次(P1332 / P1048 / P5019 / 这里)。说清楚之后,它的所有表现都是白送的推论。
4★★★ 同一条曲线,在另一道题上再跑一遍
// P1091 错法二:把「严格」写成了「非严格」(`<=` 而不是 `<`)//// 题面写的是 `t₁ < … < tᵢ > … > t_k` —— **两边都是严格的**,// 相邻两个人身高一样是**不允许**的。//// ★★ 而这道题的数据让它躲不掉:身高只有 `130 ≤ tᵢ ≤ 230`(**101 种**),而 `n ≤ 100`// ⇒ `n / 值域 ≈ 1.0`。按同一轮 [P1020](/sol/p1020/) 那条曲线,// 比值到 1.0 时这个坑的抓获率已经在 **87%** 上下了(页面第 ④ 步实测)。// ⇒ 和 [P1439](/sol/p1439/)(题面保证没有重复 ⇒ 精确的 0)正好是两个极端。//// ★ 官方样例里有一对相同的身高(两个 186)—— 第 ④ 步会说它挡没挡住。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0; vector<int> t(n); for (int& x : t) cin >> x; vector<int> f(n, 1), g(n, 1); for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (t[j] <= t[i]) f[i] = max(f[i], f[j] + 1); // ← <= for (int i = n - 1; i >= 0; i--) for (int j = n - 1; j > i; j--) if (t[j] <= t[i]) g[i] = max(g[i], g[j] + 1); int best = 0; for (int i = 0; i < n; i++) best = max(best, f[i] + g[i] - 1); cout << n - best << "\n"; return 0;}点「运行 ▶」看结果
题面那条链 t₁ < ⋯ < tᵢ > ⋯ > t_k 两边都是严格的。
把 < 写成 <=,抓获率沿比值走一遍(n 固定 14,只拧身高的种类数):
n / 值域 |
0.007 | 0.070 | 0.350 | 1.000(题面) | 3.500 |
|---|---|---|---|---|---|
| 有相同身高的轮数 | 13 | 120 | 283 | 300 | 300 |
| 非严格那版被抓 | 2 | 25 | 106 | 215 | 300 |
P1020 是在「导弹高度」上拧的,这一页是在「同学身高」上拧的, 两条曲线的形状一模一样:
- 抓获率随比值单调上升(2 → 25 → 106 → 215 → 300);
- 而「有没有相同的数」这一层在比值 1.0 就已经饱和到 300,之后不再提供任何信息。
⇒ 这一轮四道题把这条曲线钉在了五个点上:
| 比值 | 那个坑被抓 | |
|---|---|---|
| P1439 | 题面保证无重复 | ★ 精确的 0 |
| B3637 | 0.005 | 2 / 300 |
| 这道题 | 1.0 | 215 / 300 |
| P1020 | 2.0 | 几乎必抓 |
| (压到极限) | 3.5 | 300 / 300 |
样例 186 186 150 200 160 130 197 220 里有一对相同的身高(两个 186),
而且它们恰好卡在最优队形的路上 ⇒ 非严格那版输出 3,答案是 4。挡住。
⚠ 对比 P1020 的样例:8 个高度没有一个重复 ⇒ 同一类 bug 被放过。 ⇒ 同一个坑,两道题的样例一个挡住一个放过 —— 差别只在样例数据里有没有重复。
5★ 顺带:题面那句 1 ≤ i ≤ k 是有内容的
题面写的是 t₁ < ⋯ < tᵢ > ⋯ > t_k,并且 1 ≤ i ≤ k ——
也就是说「最高的那位」可以站在最左边(整个队形单调下降)或最右边(单调上升)。
★ 上面那个式子 f[i] + g[i] − 1 天然覆盖了这两种情况(那时 f[i] 或 g[i] 等于 1)。
实测 300 轮里,最优队形是纯下降的有 13 轮、纯上升的有 1 轮 ——
⇒ 这句话不是废话,它真的会被用到。
6度量程序和生成器
// P1091 的度量程序 —— 这一页所有数字都出自这一份。//// `./p1091Count` 人看的版本// `./p1091Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用//// 四段:// ① ★ 「跑两遍 LIS」≡ `2ⁿ` 枚举子集逐个验队形;// ② ★★ 忘了那个 `-1` 那版**算了什么**:答案恒少 1(逐组相等);// ③ ★★★ 「严格写成非严格」的抓获率 —— 沿 `n / 身高种类数` 这个比值走一遍,// 和同一轮 [P1020](/sol/p1020/) 那条曲线是同一条;// ④ ★ 题面 `1 ≤ i ≤ k` ⇒ **单调上升或单调下降本身也算队形**:数一数这种局面有多少。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
static bool CSV = false;static void row(const char* key, const vector<ll>& v) { if (!CSV) return; printf("%s", key); for (ll x : v) printf(",%lld", x); printf("\n");}
/** 正解:两遍 LIS。loose=true 时把严格换成非严格;minus=false 时忘了 -1 */static int solve(const vector<int>& t, bool loose = false, bool minus = true, int* peakSide = nullptr) { int n = t.size(); vector<int> f(n, 1), g(n, 1); for (int i = 0; i < n; i++) for (int j = 0; j < i; j++) if (loose ? t[j] <= t[i] : t[j] < t[i]) f[i] = max(f[i], f[j] + 1); for (int i = n - 1; i >= 0; i--) for (int j = n - 1; j > i; j--) if (loose ? t[j] <= t[i] : t[j] < t[i]) g[i] = max(g[i], g[j] + 1); int best = 0, at = 0; for (int i = 0; i < n; i++) { int cur = f[i] + g[i] - (minus ? 1 : 0); if (cur > best) { best = cur; at = i; } } if (peakSide) *peakSide = (f[at] == 1 ? -1 : (g[at] == 1 ? 1 : 0)); // -1 峰在最左,1 峰在最右 return n - best;}/** 参照物:枚举所有子集,照题面那条不等式链验 */static int brute(const vector<int>& t) { int n = t.size(), best = 0; for (int mask = 1; mask < (1 << n); mask++) { vector<int> v; for (int i = 0; i < n; i++) if (mask >> i & 1) v.push_back(t[i]); int k = v.size(), p = 0; while (p + 1 < k && v[p] < v[p + 1]) p++; int q = p; while (q + 1 < k && v[q] > v[q + 1]) q++; if (q == k - 1) best = max(best, k); } return n - best;}static vector<int> gen(mt19937& rng, int n, int kinds) { vector<int> t(n); for (int i = 0; i < n; i++) t[i] = 130 + (int)(rng() % (unsigned)kinds); return t;}
int main(int argc, char** argv) { CSV = argc > 1 && string(argv[1]) == "csv";
/* ① 两遍 LIS ≡ 2^n 暴力,② 忘了 -1 那版恒少 1 */ { mt19937 rng(20260829u); int groups = 0, bad = 0, offByOne = 0; for (int rep = 0; rep < 300; rep++, groups++) { vector<int> t = gen(rng, (int)(rng() % 13) + 2, 14); int ok = solve(t); if (brute(t) != ok) bad++; if (solve(t, false, false) == ok - 1) offByOne++; // 恒少 1 } if (!CSV) printf("① %d 组(n ≤ 14):两遍 LIS vs 2^n 枚举子集不一致 %d 组\n" "② 忘了 -1 那版:答案**恒好少 1** 的轮数 %d / %d(逐组相等)\n", groups, bad, offByOne, groups); row("ref", {groups, bad, offByOne}); }
/* ③ ★★★ 「严格写成非严格」的抓获率,沿比值走 */ { const int KINDS[] = {2000, 200, 40, 14, 4}; // n 固定 14 ⇒ 比值 0.007 / 0.07 / 0.35 / 1.0 / 3.5 vector<ll> out; for (int k : KINDS) { mt19937 rng(k * 7919u + 91u); int dup = 0, caught = 0; for (int r = 0; r < 300; r++) { vector<int> t = gen(rng, 14, k); vector<int> s = t; sort(s.begin(), s.end()); if (adjacent_find(s.begin(), s.end()) != s.end()) dup++; if (solve(t, true) != solve(t)) caught++; } out.push_back(caught); out.push_back(dup); if (!CSV) printf("③ n = 14,身高 %4d 种(比值 %.3f):非严格那版被抓 %d / 300," "有相同身高的 %d / 300\n", k, 14.0 / k, caught, dup); } row("loose", out); }
/* ④ 单调上升 / 单调下降本身也算队形 */ { mt19937 rng(4444u); int rounds = 300, left = 0, right = 0; for (int r = 0; r < rounds; r++) { vector<int> t = gen(rng, 14, 14); int side = 0; solve(t, false, true, &side); if (side < 0) left++; else if (side > 0) right++; } if (!CSV) printf("④ %d 轮:最优队形里「最高的人站在最左边」(纯下降)%d 轮," "「站在最右边」(纯上升)%d 轮 ⇒ 题面 1 ≤ i ≤ k 允许这两种\n", rounds, left, right); row("mono", {rounds, left, right}); } return 0;}点「运行 ▶」看结果
// P1091 对拍生成器:`./p1091Gen <seed> [n 上限] [身高种类数]`// 默认 `n ≤ 14`、身高从 `130..230` 里取(101 种)—— **照题面的比值**。//// ★ 题面是 `n ≤ 100`、`130 ≤ tᵢ ≤ 230`(101 种)⇒ `n / 值域 ≈ 1.0`;// 默认档 `n ≤ 14` 时把种类数一起缩到 14,比值仍然是 1.0 ——// ⇒ 又一次「[照抄题面的比值,不是绝对规模](/sol/p1020/)」(n 小了才能用 `2ⁿ` 暴力当参照物)。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1; int nHi = argc > 2 ? atoi(argv[2]) : 14; int kinds = argc > 3 ? atoi(argv[3]) : 14; nHi = max(2, min(100, nHi)); kinds = max(1, min(101, kinds));
mt19937 rng(seed * 2654435761u + 1091u); int n = (int)(rng() % (unsigned)(nHi - 1)) + 2; printf("%d\n", n); for (int i = 0; i < n; i++) printf("%u%c", 130 + (unsigned)(rng() % (unsigned)kinds), i + 1 == n ? '\n' : ' '); return 0;}点「运行 ▶」看结果
★ 生成器默认 n ≤ 14、身高 14 种 —— 比值仍然是 1.0,和题面一致
(又一次「照抄题面的比值,不是绝对规模」:n 小了才能用 2ⁿ 暴力当参照物)。
7一页纸
| 关键的一步 | 跑两遍 LIS(正着一遍、倒着一遍),枚举最高的那位;f[i] + g[i] − 1 |
| 哪一版能 AC | p1091.cpp,O(n²)(n ≤ 100) |
| ⚠⚠ 最容易漏的一笔 | 那个 −1 —— 忘了它,答案恒好少 1(300 / 300 逐组相等) |
| ★★★ 这一页的位置 | 「严格 / 非严格」那条曲线的第四个点:题面比值 1.0 ⇒ 被抓 215 / 300 (曲线:0.007 → 3.5 时 2 → 300,而「有相同的数」在比值 1.0 就饱和了) |
| ★ 样例 | 挡住了非严格那版(样例里有两个 186)—— 而 P1020 的样例放过了同一类 bug |
| ★ 题面的边界 | 1 ≤ i ≤ k ⇒ 纯上升 / 纯下降也算队形(实测 300 轮里出现 14 次) |
| 参照物 | 2ⁿ 枚举子集,照题面那条不等式链验 —— 300 组不一致 0 组 |