题单 · 习题解析

洛谷 P1220 关路灯

★★ 状态要多记一维「人站在哪一端」;★★★ 而题面替你写好的那个贪心恒 ≥ 正解、少一维那版恒 ≤ 正解 —— 一页之内两个方向都齐了

原题:洛谷 P1220出自 第 26 章 区间 DP:石子合并 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

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 ≤ 501 ≤ c ≤ n1 ≤ Wᵢ ≤ 1001 ≤ 路灯位置 ≤ 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★ 第一版:题面「替你写好」的那个错觉

这道题很少见地把「大多数人的第一反应」直接印在了题面里:

开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率, 然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此

p1220Greedy.cpp✗ 第一版:先关功率大的那一边
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 它到底差多少 —— 和背包题里那些「只错一点点」的贪心完全不是一回事
≥ 正解 300 / 300(它给出的是一个合法方案
300 轮被抓 85
错的时候平均多花 48.67%
最多多花 323.01%

⇒ 对照第 20 章 P1048(性价比贪心错时只差 10.96%)和 本章 P1507(8.64% ~ 18.16%): ★★ 同样叫「贪心」,错的幅度能差一个数量级 —— 这一轮已经量到第二次 (另一次是第 25 章 P5322 的 56%)。

⚠ 而「起点在哪儿」是一把能把这个 bug 藏起来的旋钮
起点 c 最左(c = 1 正中
那个贪心被抓 精确的 0 115 / 300

★ 左边那个 0 是结构性的c = 1 时根本没有「两边」可选, 「先关功率大的一边」退化成了唯一的走法。 ⇒ 又一次「生成器的一个顺手默认值,正好是某个 bug 的藏身处」—— 这一轮(第 25、26 两章)已经是第四次了。

2★★ 关键的一步:状态在「区间」之外还要多记一维

★ f[l][r][人在左端 / 右端]
    f[l][r][0/1] = 已经把第 l..r 盏灯全关掉、此刻人站在 l(0)或 r(1),
                   到目前为止已经消耗的电量最小值

为什么必须多这一维:下一步要么去左边、要么去右边, 而走多远取决于人现在在哪一头 —— 只记区间的话,这个信息就丢了。

★★ 第二件要想清楚的事:走路的时候,还亮着的灯一直在耗电。 所以每走一段距离 d,代价是 d × (区间外还亮着的灯的总功率) —— 注意是区间外(已关的不再耗电),前缀和 O(1) 算出来。

p1220.cpp★ 这一版就能 AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★★ 少了那一维会怎样 —— 它和贪心的方向正好相反

p1220NoSide.cpp✗ 只记区间,默认人就在刚关的那一头
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 一页之内,两个错法的方向正好相反
和正解的关系 300 轮
题面那个贪心 ★ 恒 正解 300/300 ≥,被抓 85
只记区间(少一维) ★ 恒 正解 300/300 ≤,被抓 155

为什么:贪心给出的是一个真能走出来的方案 ⇒ 只会比最优差; 而「只记区间」偷偷把人瞬移到了近的那一端 ⇒ 它算的是一个根本走不出来的方案, 所以只会比最优小。

⇒ ★★★ 这就是本轮反复用到的那条判据的第三次现场 (P1775 的哈夫曼 vs 相邻贪心P1880 的没破环): 「答案偏大还是偏小」,看你解的是「收紧了的问题」还是「放宽了的问题」,不用跑就能判。

4★ 第三个错法:耗电算成了「已经关掉那一段」

p1220Inside.cpp✗ 前缀和取反了
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它算的是一道镜像的题:「灯关掉之后才开始耗电」。300 轮被抓 267 次,样例也挡住了(440 vs 270)。

5★ 参照物、规模和上界

参照物不用 DP:已关掉的永远是一段连续区间,所以每一步只有「去关左边那盏」和 「去关右边那盏」两种选择 ⇒ 枚举 2^(n−1) 条路径,照题面一步一步算电费。

300 轮:正解 vs 枚举关灯顺序 不一致 0 轮
顶格 n = 50 ⇒ 状态数 50 × 50 × 2,转移 O(1)
一步代价的上界 总功率 × 最长距离 495 000 ⇒ 走不到 50 步,int
p1220Brute.cpp参照物:枚举关灯顺序(300 轮不一致 0 轮)

6度量程序和生成器

p1220Count.cpp度量程序(本页所有数字都出自它)
p1220Gen.cpp数据生成器

7一页纸

★★ 关键的一步 状态是 f[l][r][人在左端/右端] —— 走多远取决于人在哪一头
第二件事 每走一段距离,乘的是区间外(还亮着)的总功率
★ 第一版 题面替你写好了:「先关功率大的一边」——恒 ≥ 正解,被抓 85/300,★ 错时平均多花 48.67%、最多 323%
⚠ 而它有藏身处 起点 c = 1 时是精确的 0(没有两边可选);起点在正中 115/300
★★★ 少一维 正解(走不出来的方案),被抓 155 —— 和贪心方向相反
耗电算反 被抓 267/300,样例挡住
参照物 枚举关灯顺序(2^(n−1)),不用 DP;300 轮不一致 0 轮
规模 n ≤ 5050 × 50 × 2 个状态;一步上界 495 000 ⇒ int