标题
题目
P1220 关路灯


题目描述
某一村庄在一条路线上安装了 n n n 盏路灯,每盏灯的功率有大有小(即同一段时间内消耗的电量有多有少)。老张就住在这条路中间某一路灯旁,他有一项工作就是每天早上天亮时一盏一盏地关掉这些路灯。
为了给村里节省电费,老张记录下了每盏路灯的位置和功率,他每次关灯时也都是尽快地去关,但是老张不知道怎样去关灯才能够最节省电。他每天都是在天亮时首先关掉自己所处位置的路灯,然后可以向左也可以向右去关灯。开始他以为先算一下左边路灯的总功率再算一下右边路灯的总功率,然后选择先关掉功率大的一边,再回过头来关掉另一边的路灯,而事实并非如此,因为在关的过程中适当地调头有可能会更省一些。
现在已知老张走的速度为 1 m / s 1m/s 1m/s,每个路灯的位置(是一个整数,即距路线起点的距离,单位: m m m)、功率( W W W),老张关灯所用的时间很短而可以忽略不计。
请你为老张编一程序来安排关灯的顺序,使从老张开始关灯时刻算起所有灯消耗电最少(灯关掉后便不再消耗电了)。
输入格式
第一行是两个数字 n n n(表示路灯的总数)和 c c c(老张所处位置的路灯号);
接下来 n n n 行,每行两个数据,表示第 1 1 1 盏到第 n n n 盏路灯的位置和功率。数据保证路灯位置单调递增。
输出格式
一个数据,即最少的功耗(单位: J J J, 1 J = 1 W × s 1J=1W\times s 1J=1W×s)。
输入输出样例 #1
输入 #1
5 3
2 10
3 20
5 20
6 30
8 10
输出 #1
270
说明/提示
样例解释
此时关灯顺序为 3 4 2 1 5。
数据范围
1 ≤ n ≤ 50 1\le n\le50 1≤n≤50, 1 ≤ c ≤ n 1\le c\le n 1≤c≤n, 1 ≤ W i ≤ 100 1\le W_i \le 100 1≤Wi≤100。
解题思路
这道题是典型的动态规划 问题,核心思路 是通过状态定义 和转移方程 ,找到关灯的最优顺序 ,使得总耗电量最小 。
关键观察
耗电量计算:路灯的耗电量 = 功率 × 点亮时间 。点亮时间是从老张开始关灯到该路灯被关闭的总时间(包括走路时间)。
最优子结构:老张每次只能向左或向右关灯,且已关闭的路灯一定是连续的区间(因为如果中间有未关闭的灯,绕路去关其他灯会增加总时间)。因此,状态可以用 "当前关闭的路灯区间 [l, r]" 和 "老张当前所在位置(左端点 l 或右端点 r)" 来表示。
状态转移:对于区间 [l, r],老张要么从左端点 l 扩展到 l-1,要么从右端点 r 扩展到 r+1。转移时需要计算走路的时间,并乘以当前未关闭路灯的总功率(因为这些灯在走路过程中仍在耗电)。
具体步骤
输入处理与预处理:
读取路灯数量 n 和老张初始位置 c(注意:题目中路灯号从 1 开始,需转换为 0 索引方便数组操作)。
读取每盏路灯的位置和功率,存储在数组中。
计算功率前缀和数组,方便快速查询未关闭路灯的总功率。
状态定义:
dp[l][r][0]:关闭区间 [l, r] 的路灯后,老张在左端点 l 时的最小耗电量。
dp[l][r][1]:关闭区间 [l, r] 的路灯后,老张在右端点 r 时的最小耗电量。
初始化:
初始状态是老张只关闭了自己所在的路灯(区间 [c, c]),此时耗电量为 0,即 dp[c][c][0] = dp[c][c][1] = 0。
状态转移:
对于区间长度 k(从 1 到 n-1),枚举所有可能的左端点 l,右端点 r = l + k。
计算从 l+1 到 r 扩展到 l(老张从 l+1 走到 l)的耗电量:dp[l][r][0] = min(dp[l+1][r][0] + (pos[l+1] - pos[l]) * power_sum, dp[l+1][r][1] + (pos[r] - pos[l]) * power_sum)。
计算从 l 到 r-1 扩展到 r(老张从 r-1 走到 r)的耗电量:dp[l][r][1] = min(dp[l][r-1][0] + (pos[r] - pos[l]) * power_sum, dp[l][r-1][1] + (pos[r] - pos[r-1]) * power_sum)。
其中 power_sum 是当前未关闭路灯的总功率,即总功率减去区间 [l, r] 的功率和。
结果:
最终答案是关闭所有路灯(区间 [0, n-1])后,老张在左端点或右端点的最小耗电量,即 min(dp[0][n-1][0], dp[0][n-1][1])。
代码
cpp
#include <cstdio>
#include <cstring>
using namespace std;
const int MAXM = 60; // 适配 n≤50 的数据范围
int a[MAXM]; // 路灯位置(1-based索引)
int b[MAXM]; // 路灯功率(1-based索引)
int sum[MAXM]; // 功率前缀和:sum[i] = b[1]+b[2]+...+b[i]
int f[MAXM][MAXM][2]; // 动态规划数组
int n, c; // n:路灯总数,c:初始路灯号(1-based)
// 自定义min函数,提升效率
int min(int x, int y) {
return x < y ? x : y;
}
int main() {
// 输入数据
scanf("%d%d", &n, &c);
sum[0] = 0;
for (int i = 1; i <= n; ++i) {
scanf("%d%d", &a[i], &b[i]);
sum[i] = sum[i-1] + b[i]; // 计算前缀和
}
// 初始化:只关闭初始位置的路灯,功耗为0
memset(f, 0x3f, sizeof(f)); // 初始化为极大值(0x3f对应十进制1061109567,足够大)
f[c][c][0] = f[c][c][1] = 0;
// 枚举关闭的路灯区间长度l(从2到n,逐步扩展)
for (int l = 2; l <= n; ++l) {
// 枚举区间左端点i,右端点j = i + l - 1
for (int i = 1; i + l - 1 <= n; ++i) {
int j = i + l - 1;
// 状态转移:关闭区间[i,j]后,人在左端点i
// 两种来源:1. 从[i+1,j]的左端点i+1走到i;2. 从[i+1,j]的右端点j走到i
// 功耗 = 之前的功耗 + 走路时间 × 未关闭路灯的总功率
f[i][j][0] = min(
f[i+1][j][0] + (a[i+1] - a[i]) * (sum[n] - (sum[j] - sum[i])),
f[i+1][j][1] + (a[j] - a[i]) * (sum[n] - (sum[j] - sum[i]))
);
// 状态转移:关闭区间[i,j]后,人在右端点j
// 两种来源:1. 从[i,j-1]的左端点i走到j;2. 从[i,j-1]的右端点j-1走到j
f[i][j][1] = min(
f[i][j-1][0] + (a[j] - a[i]) * (sum[n] - (sum[j-1] - sum[i-1])),
f[i][j-1][1] + (a[j] - a[j-1]) * (sum[n] - (sum[j-1] - sum[i-1]))
);
}
}
// 最终答案:关闭所有路灯(区间[1,n])后,人在左端点或右端点的最小功耗
printf("%d\n", min(f[1][n][0], f[1][n][1]));
return 0;
}
代码核心解释(与原思路完全一致)
数组定义:
a[i]:第 i 盏路灯的位置(1-based 索引,符合题目输入习惯)
b[i]:第 i 盏路灯的功率
sum[i]:前缀和数组,快速计算区间功率和(sum[j]-sum[i-1] 是区间 [i,j] 的总功率)
f[i][j][0]:关闭区间 [i,j] 的路灯后,人在左端点 i 时的最小功耗
f[i][j][1]:关闭区间 [i,j] 的路灯后,人在右端点 j 时的最小功耗
关键公式(未关闭路灯的总功率):
当扩展到区间 [i,j] 时,未关闭的路灯功率 = 总功率(sum [n]) - 已关闭区间的功率
例如:计算f[i][j][0]时,已关闭区间是 [i,j],功率和为sum[j]-sum[i],因此未关闭功率为sum[n] - (sum[j]-sum[i])
状态转移逻辑:
扩展区间时,走路时间 = 两点间距离(位置差),因为速度是 1m/s
每一步的功耗增加 = 走路时间 × 未关闭路灯的总功率(这些灯在走路过程中持续耗电)
初始化与结果:
初始状态:只关闭初始位置 c 的路灯,功耗为 0(f[c][c][0] = f[c][c][1] = 0)
最终结果:关闭所有路灯(区间 [1,n])后,取人在左端点或右端点的最小功耗
输入输出样例验证
输入:
plaintext
5 3
2 10
3 20
5 20
6 30
8 10
输出:270,与题目样例一致,说明代码正确性。
效率说明
时间复杂度:O (n²),n≤50 时,循环次数仅 50×50=2500 次,效率极高
空间复杂度:O (n²),使用的数组大小为 60×60×2=7200,远低于内存限制
自定义 min 函数避免了标准库函数的额外开销,进一步提升运行速度