0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1220,日期见页头。两边不一致时信原站。
题目描述
某一村庄在一条路线上安装了 n 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。
老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关, 但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯, 然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率, 然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此, 因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为 1 m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位:m)、功率(W),
老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序, 使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
输入格式
第一行是两个数字 n(表示路灯的总数)和 c(老张所处位置的路灯号);
接下来 n 行,每行两个数据,表示第 1 盏到第 n 盏路灯的位置和功率。数据保证路灯位置单调递增。
输出格式
一个数据,即最少的功耗(单位:J,1 J = 1 W × s)。
说明/提示
样例解释
此时关灯顺序为 3 4 2 1 5。
数据范围
1 ≤ n ≤ 50,1 ≤ c ≤ n,1 ≤ Wᵢ ≤ 100,1 ≤ 路灯位置 ≤ 100
输入输出样例
输入
5 3 2 10 3 20 5 20 6 30 8 10
输出
270
五盏灯,老张站在第 3 盏。按 3 4 2 1 5 的顺序关,总功耗 270。
★ 这一组样例把本页三个错法全挡住了(题面那个贪心 310、只记区间 190、耗电算反 440)。 ⚠ 小提醒:原站样例输出那一行末尾带着两个空格,本页这一框是仓库里的正解真跑出来的 (样例输出不许手写),所以没有那两个空格 —— 评测不关心行尾空白。
1★ 第一版:题面「替你写好」的那个错觉
这道题很少见地把「大多数人的第一反应」直接印在了题面里:
开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率, 然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此。
// ✗ P1220 的第一版:**题面替你写好的那个错觉**。//// 题面原话:「开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,// 然后**选择先关掉功率大的一边**,再回过头来关掉另一边的路灯,**而事实并非如此**,// 因为在关的过程中适当地调头有可能会更省一些。」//// ⇒ 这一版就是把那句话写成代码:比一比两边的总功率,先一路关掉大的那边,再回头关另一边。// ★ 它给出的是**一个合法方案** ⇒ 答案**恒 ≥ 正解**。页面上量了它到底差多少。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { int n, c; if (scanf("%d %d", &n, &c) != 2) return 0; vector<ll> pos(n + 2, 0), w(n + 2, 0); for (int i = 1; i <= n; i++) if (scanf("%lld %lld", &pos[i], &w[i]) != 2) return 0;
ll leftW = 0, rightW = 0; for (int i = 1; i < c; i++) leftW += w[i]; for (int i = c + 1; i <= n; i++) rightW += w[i];
// 先关功率大的那一边,再回头关另一边 vector<int> order; if (leftW >= rightW) { for (int i = c - 1; i >= 1; i--) order.push_back(i); for (int i = c + 1; i <= n; i++) order.push_back(i); } else { for (int i = c + 1; i <= n; i++) order.push_back(i); for (int i = c - 1; i >= 1; i--) order.push_back(i); }
ll cost = 0, here = pos[c]; vector<char> off(n + 2, 0); off[c] = 1; for (int idx : order) { ll rest = 0; for (int i = 1; i <= n; i++) if (!off[i]) rest += w[i]; cost += llabs(here - pos[idx]) * rest; here = pos[idx]; off[idx] = 1; } printf("%lld\n", cost); return 0;}点「运行 ▶」看结果
| 它 ≥ 正解 | ★ 300 / 300(它给出的是一个合法方案) |
| 300 轮被抓 | 85 |
| 错的时候平均多花 | ★ 48.67% |
| 最多多花 | ★ 323.01% |
⇒ 对照第 20 章 P1048(性价比贪心错时只差 10.96%)和 本章 P1507(8.64% ~ 18.16%): ★★ 同样叫「贪心」,错的幅度能差一个数量级 —— 这一轮已经量到第二次 (另一次是第 25 章 P5322 的 56%)。
起点 c |
最左(c = 1) |
正中 |
|---|---|---|
| 那个贪心被抓 | ★ 精确的 0 | 115 / 300 |
★ 左边那个 0 是结构性的:c = 1 时根本没有「两边」可选,
「先关功率大的一边」退化成了唯一的走法。
⇒ 又一次「生成器的一个顺手默认值,正好是某个 bug 的藏身处」——
这一轮(第 25、26 两章)已经是第四次了。
2★★ 关键的一步:状态在「区间」之外还要多记一维
f[l][r][0/1] = 已经把第 l..r 盏灯全关掉、此刻人站在 l(0)或 r(1),
到目前为止已经消耗的电量最小值为什么必须多这一维:下一步要么去左边、要么去右边, 而走多远取决于人现在在哪一头 —— 只记区间的话,这个信息就丢了。
★★ 第二件要想清楚的事:走路的时候,还亮着的灯一直在耗电。
所以每走一段距离 d,代价是 d × (区间外还亮着的灯的总功率)
—— 注意是区间外(已关的不再耗电),前缀和 O(1) 算出来。
// P1220 关路灯 —— ★ 这一版就能 AC。//// ★★ 关键的一步:**状态在「区间」之外还要多记一维 —— 人现在站在左端还是右端。**//// f[l][r][0/1] = 已经把第 l..r 盏灯全关掉、此刻人站在 **l(0)/ r(1)**,// 到目前为止**已经消耗**的电量最小值。//// 为什么必须多这一维:下一步要么去左边、要么去右边,而**走多远取决于人在哪头** ——// 只记区间的话,这个信息就丢了(见 p1220NoSide.cpp)。//// ★★ 第二个要想清楚的:**走路的时候,还亮着的灯一直在耗电**。// 所以每走一段距离 d,代价是 `d × (区间外还亮着的灯的总功率)` ——// 注意是**区间外**(已关的那些不再耗电),用前缀和 O(1) 算出来。//// ⚠ 题面还替你把一个错觉写出来了:「他以为先算左右总功率、先关功率大的一边,// **而事实并非如此**,因为在关的过程中适当地调头有可能会更省」。// ⇒ 那个贪心值多少,页面第 ① 步量了。//// 规模:`n ≤ 50` ⇒ 状态 `50 × 50 × 2`,转移 O(1),几千次就跑完。
#include <bits/stdc++.h>using namespace std;
int main() { int n, c; if (scanf("%d %d", &n, &c) != 2) return 0; vector<long long> pos(n + 2, 0), w(n + 2, 0), s(n + 2, 0); for (int i = 1; i <= n; i++) { if (scanf("%lld %lld", &pos[i], &w[i]) != 2) return 0; s[i] = s[i - 1] + w[i]; } long long total = s[n]; auto outside = [&](int l, int r) { return total - (s[r] - s[l - 1]); };
const long long INF = LLONG_MAX / 4; vector<vector<array<long long, 2>>> f(n + 2, vector<array<long long, 2>>(n + 2, { INF, INF })); f[c][c][0] = f[c][c][1] = 0;
for (int len = 2; len <= n; len++) for (int l = 1; l + len - 1 <= n; l++) { int r = l + len - 1; // 站在 l:上一步刚关掉的是 [l+1, r],人在 l+1 或 r f[l][r][0] = min(f[l + 1][r][0] + (pos[l + 1] - pos[l]) * outside(l + 1, r), f[l + 1][r][1] + (pos[r] - pos[l]) * outside(l + 1, r)); // 站在 r:上一步刚关掉的是 [l, r-1],人在 l 或 r-1 f[l][r][1] = min(f[l][r - 1][1] + (pos[r] - pos[r - 1]) * outside(l, r - 1), f[l][r - 1][0] + (pos[r] - pos[l]) * outside(l, r - 1)); } printf("%lld\n", min(f[1][n][0], f[1][n][1])); return 0;}点「运行 ▶」看结果
3★★ 少了那一维会怎样 —— 它和贪心的方向正好相反
// ✗ P1220 的第二版:**只记区间,不记人站在哪一端**。//// 状态写成 f[l][r],转移时默认「人就在**刚刚关掉的那一头**」:// f[l][r] = min( f[l+1][r] + (pos[l+1] − pos[l]) × 外面的功率,// f[l][r−1] + (pos[r] − pos[r−1]) × 外面的功率 )//// ⚠ 它丢掉的正是这道题的全部门道:**人可能在另一头**,得先走过来。// 于是它算出来的电费比真实的**少**(它偷偷把人瞬移到了近的那一端)// ⇒ 答案**恒 ≤ 正解**,是一个**做不到的**方案。//// ★ 这和「贪心那一版恒 ≥ 正解」正好是一对:// 一个给出合法但不最优的方案(偏大),一个算的是根本走不出来的方案(偏小)。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { int n, c; if (scanf("%d %d", &n, &c) != 2) return 0; vector<ll> pos(n + 2, 0), w(n + 2, 0), s(n + 2, 0); for (int i = 1; i <= n; i++) { if (scanf("%lld %lld", &pos[i], &w[i]) != 2) return 0; s[i] = s[i - 1] + w[i]; } ll total = s[n]; auto outside = [&](int l, int r) { return total - (s[r] - s[l - 1]); };
const ll INF = LLONG_MAX / 4; vector<vector<ll>> f(n + 2, vector<ll>(n + 2, INF)); f[c][c] = 0; for (int len = 2; len <= n; len++) for (int l = 1; l + len - 1 <= n; l++) { int r = l + len - 1; ll best = INF; if (f[l + 1][r] < INF) best = min(best, f[l + 1][r] + (pos[l + 1] - pos[l]) * outside(l + 1, r)); if (f[l][r - 1] < INF) best = min(best, f[l][r - 1] + (pos[r] - pos[r - 1]) * outside(l, r - 1)); f[l][r] = best; } printf("%lld\n", f[1][n]); return 0;}点「运行 ▶」看结果
| 和正解的关系 | 300 轮 | |
|---|---|---|
| 题面那个贪心 | ★ 恒 ≥ 正解 | 300/300 ≥,被抓 85 |
| 只记区间(少一维) | ★ 恒 ≤ 正解 | 300/300 ≤,被抓 155 |
为什么:贪心给出的是一个真能走出来的方案 ⇒ 只会比最优差; 而「只记区间」偷偷把人瞬移到了近的那一端 ⇒ 它算的是一个根本走不出来的方案, 所以只会比最优小。
⇒ ★★★ 这就是本轮反复用到的那条判据的第三次现场 (P1775 的哈夫曼 vs 相邻贪心、P1880 的没破环): 「答案偏大还是偏小」,看你解的是「收紧了的问题」还是「放宽了的问题」,不用跑就能判。
4★ 第三个错法:耗电算成了「已经关掉那一段」
// ✗ P1220 的第三版:耗电算成了「**已经关掉**那一段的功率」。//// 状态和转移都写对了,只有一处:乘上去的应该是**区间外还亮着的**灯的总功率,// 这一版乘的是**区间内**(已经关掉的)那些 —— 正好反了。//// ★ 它算的是一道**镜像**的题:「灯关掉之后才开始耗电」。// ⚠ 而官方样例挡不挡得住它,页面上量过。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
int main() { int n, c; if (scanf("%d %d", &n, &c) != 2) return 0; vector<ll> pos(n + 2, 0), w(n + 2, 0), s(n + 2, 0); for (int i = 1; i <= n; i++) { if (scanf("%lld %lld", &pos[i], &w[i]) != 2) return 0; s[i] = s[i - 1] + w[i]; } auto inside = [&](int l, int r) { return s[r] - s[l - 1]; }; // ✗ 反了
const ll INF = LLONG_MAX / 4; vector<vector<array<ll, 2>>> f(n + 2, vector<array<ll, 2>>(n + 2, { INF, INF })); f[c][c][0] = f[c][c][1] = 0; for (int len = 2; len <= n; len++) for (int l = 1; l + len - 1 <= n; l++) { int r = l + len - 1; f[l][r][0] = min(f[l + 1][r][0] + (pos[l + 1] - pos[l]) * inside(l + 1, r), f[l + 1][r][1] + (pos[r] - pos[l]) * inside(l + 1, r)); f[l][r][1] = min(f[l][r - 1][1] + (pos[r] - pos[r - 1]) * inside(l, r - 1), f[l][r - 1][0] + (pos[r] - pos[l]) * inside(l, r - 1)); } printf("%lld\n", min(f[1][n][0], f[1][n][1])); return 0;}点「运行 ▶」看结果
它算的是一道镜像的题:「灯关掉之后才开始耗电」。300 轮被抓 267 次,样例也挡住了(440 vs 270)。
5★ 参照物、规模和上界
参照物不用 DP:已关掉的永远是一段连续区间,所以每一步只有「去关左边那盏」和
「去关右边那盏」两种选择 ⇒ 枚举 2^(n−1) 条路径,照题面一步一步算电费。
| 300 轮:正解 vs 枚举关灯顺序 | ★ 不一致 0 轮 |
顶格 n = 50 ⇒ 状态数 |
50 × 50 × 2,转移 O(1) |
一步代价的上界 总功率 × 最长距离 |
495 000 ⇒ 走不到 50 步,int 够 |
6度量程序和生成器
7一页纸
| ★★ 关键的一步 | 状态是 f[l][r][人在左端/右端] —— 走多远取决于人在哪一头 |
| 第二件事 | 每走一段距离,乘的是区间外(还亮着)的总功率 |
| ★ 第一版 | 题面替你写好了:「先关功率大的一边」——恒 ≥ 正解,被抓 85/300,★ 错时平均多花 48.67%、最多 323% |
| ⚠ 而它有藏身处 | 起点 c = 1 时是精确的 0(没有两边可选);起点在正中 115/300 |
| ★★★ 少一维 | 恒 ≤ 正解(走不出来的方案),被抓 155 —— 和贪心方向相反 |
| 耗电算反 | 被抓 267/300,样例挡住 |
| 参照物 | 枚举关灯顺序(2^(n−1)),不用 DP;300 轮不一致 0 轮 |
| 规模 | n ≤ 50 ⇒ 50 × 50 × 2 个状态;一步上界 495 000 ⇒ int 够 |