题目分析
问题描述
公司新建了一栋 NNN 层的摩天大楼,但发现每层只能建造一个游戏室(乒乓球室或台球室)。整栋大楼必须至少有一个乒乓球室和一个台球室。
已知每层有 TiT_iTi 个乒乓球玩家和 PiP_iPi 个台球玩家。目标是为每层分配合适的游戏室类型,使得所有员工到最近的对应类型游戏室的距离之和最小。距离定义为楼层差的绝对值。
输入格式
- 第一行:测试用例数量 TTT (1≤T≤1001 \leq T \leq 1001≤T≤100)
- 每个测试用例:
- 第一行:楼层数 NNN (2≤N≤40002 \leq N \leq 40002≤N≤4000)
- 接下来 NNN 行:每行两个整数 TiT_iTi 和 PiP_iPi (1≤Ti,Pi≤1091 \leq T_i, P_i \leq 10^91≤Ti,Pi≤109),表示第 iii 层的乒乓球和台球玩家数量
输出格式
对于每个测试用例,输出一行 Case #x: y,其中 xxx 是测试用例编号,yyy 是最小总距离。
样例分析
样例输入:
1
2
10 5
4 3
最优分配:
- 第一层:乒乓球室
- 第二层:台球室
距离计算:
- 第一层的 555 个台球玩家需要到第二层,距离为 111,总距离 5×1=55 \times 1 = 55×1=5
- 第二层的 444 个乒乓球玩家需要到第一层,距离为 111,总距离 4×1=44 \times 1 = 44×1=4
- 总距离:5+4=95 + 4 = 95+4=9
解题思路
关键观察
- 交替段结构:最优解中,同类型的游戏室会形成连续的段,不同类型的段交替出现。
- 距离计算 :对于类型为 ttt 的连续段 [L,R][L, R][L,R],段内类型为 1−t1-t1−t 的玩家需要去段外的游戏室:
- 去左边的 L−1L-1L−1 层(类型 1−t1-t1−t)
- 或去右边的 R+1R+1R+1 层(类型 1−t1-t1−t)
- 单调性 :在连续段 [L,R][L, R][L,R] 中,存在一个分割点 midmidmid,使得:
-
L,mid\]\[L, mid\]\[L,mid\] 的玩家去左边的游戏室更近
-
动态规划设计
定义状态:
- dp[i][t]dp[i][t]dp[i][t]:前 iii 层的最小总距离,且第 iii 层的游戏室类型为 ttt(000 表示乒乓球,111 表示台球)
状态转移:
- 枚举前一个段的结束位置 jjj(0≤j<i0 \leq j < i0≤j<i)
- 当前段为 [j+1,i][j+1, i][j+1,i],类型为 ttt
- 计算段内另一种类型玩家的最小距离和:
- 如果 j=0j = 0j=0(第一段):所有玩家只能去右边的 i+1i+1i+1 层
- 如果 i=Ni = Ni=N(最后一段):所有玩家只能去左边的 jjj 层
- 否则:使用双指针找到最优分割点 midmidmid,[j+1,mid][j+1, mid][j+1,mid] 去左边,[mid+1,i][mid+1, i][mid+1,i] 去右边
优化技巧
- 前缀和预处理:快速计算区间内玩家数量和加权距离
- 双指针优化 :利用决策单调性,将内层循环从 O(N)O(N)O(N) 优化到 O(1)O(1)O(1)
- 边界处理:确保至少有两种类型的游戏室
算法复杂度
- 时间复杂度 :O(N2)O(N^2)O(N2),通过双指针优化避免了内层循环
- 空间复杂度 :O(N)O(N)O(N),使用前缀和数组和 dpdpdp 数组
代码实现
cpp
// Game Rooms
// UVa ID: 12991
// Verdict: Accepted
// Submission Date: 2025-10-24
// UVa Run Time: 0.550s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <iostream>
#include <vector>
#include <algorithm>
#include <climits>
using namespace std;
typedef long long ll;
const ll INF = 1e18; // 定义无穷大
/**
* 计算区间 [L, R] 内所有玩家到目标楼层的距离和
* @param L 区间左端点
* @param R 区间右端点
* @param targetFloor 目标楼层
* @param countSum 玩家数量的前缀和数组
* @param weightedSum 玩家数量×楼层号的前缀和数组
* @return 总距离代价
*/
ll calculateCost(int L, int R, int targetFloor, const vector<ll>& countSum, const vector<ll>& weightedSum) {
if (L > R) return 0; // 空区间代价为 0
ll cnt = countSum[R] - countSum[L - 1]; // 区间内玩家总数
ll weighted = weightedSum[R] - weightedSum[L - 1]; // 区间内玩家楼层加权和
return abs(weighted - targetFloor * cnt); // 计算总距离
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(NULL);
int testCases;
cin >> testCases;
for (int caseNum = 1; caseNum <= testCases; caseNum++) {
int totalFloors;
cin >> totalFloors;
// 存储每层的乒乓球和台球玩家数量,下标从 1 开始
vector<ll> tableTennisPlayers(totalFloors + 2, 0);
vector<ll> poolPlayers(totalFloors + 2, 0);
for (int i = 1; i <= totalFloors; i++) {
cin >> tableTennisPlayers[i] >> poolPlayers[i];
}
// 前缀和数组初始化
vector<ll> sumTableTennis(totalFloors + 2, 0); // 乒乓球玩家前缀和
vector<ll> sumPool(totalFloors + 2, 0); // 台球玩家前缀和
vector<ll> sumTableTennisWeighted(totalFloors + 2, 0); // 乒乓球玩家×楼层号前缀和
vector<ll> sumPoolWeighted(totalFloors + 2, 0); // 台球玩家×楼层号前缀和
// 计算前缀和
for (int i = 1; i <= totalFloors; i++) {
sumTableTennis[i] = sumTableTennis[i - 1] + tableTennisPlayers[i];
sumPool[i] = sumPool[i - 1] + poolPlayers[i];
sumTableTennisWeighted[i] = sumTableTennisWeighted[i - 1] + tableTennisPlayers[i] * i;
sumPoolWeighted[i] = sumPoolWeighted[i - 1] + poolPlayers[i] * i;
}
// DP 数组:dp[i][type] 表示前 i 层,第 i 层类型为 type 的最小代价
vector<vector<ll>> dp(totalFloors + 1, vector<ll>(2, INF));
dp[0][0] = dp[0][1] = 0; // 初始化
// 主 DP 循环
for (int i = 1; i <= totalFloors; i++) {
for (int currentType = 0; currentType < 2; currentType++) {
// 根据当前类型选择对应的前缀和数组
const vector<ll>& otherCount = (currentType == 0) ? sumPool : sumTableTennis;
const vector<ll>& otherWeighted = (currentType == 0) ? sumPoolWeighted : sumTableTennisWeighted;
// 双指针优化:optimalMid 记录当前最优分割点
int optimalMid = 0;
for (int prevEnd = 0; prevEnd < i; prevEnd++) {
int L = prevEnd + 1; // 当前段起始位置
int R = i; // 当前段结束位置
// 检查是否整栋楼只有一种类型(无效情况)
if (prevEnd == 0 && i == totalFloors) {
continue;
}
ll cost = 0; // 当前段的代价
if (prevEnd == 0) {
// 第一段:所有玩家只能去右边的 R+1 层
cost = calculateCost(L, R, R + 1, otherCount, otherWeighted);
} else if (i == totalFloors) {
// 最后一段:所有玩家只能去左边的 prevEnd 层
cost = calculateCost(L, R, prevEnd, otherCount, otherWeighted);
} else {
// 中间段:使用双指针找到最优分割点
// 随着 R 增大,optimalMid 单调不减
while (optimalMid < R - 1) {
// 计算当前分割点的代价
ll currentCost = calculateCost(L, optimalMid, prevEnd, otherCount, otherWeighted) +
calculateCost(optimalMid + 1, R, R + 1, otherCount, otherWeighted);
// 计算下一个分割点的代价
ll nextCost = calculateCost(L, optimalMid + 1, prevEnd, otherCount, otherWeighted) +
calculateCost(optimalMid + 2, R, R + 1, otherCount, otherWeighted);
// 如果下一个分割点不更优,则停止移动
if (nextCost >= currentCost) break;
optimalMid++;
}
// 使用最优分割点计算总代价
cost = calculateCost(L, optimalMid, prevEnd, otherCount, otherWeighted) +
calculateCost(optimalMid + 1, R, R + 1, otherCount, otherWeighted);
}
// 更新 DP 状态
dp[i][currentType] = min(dp[i][currentType],
dp[prevEnd][1 - currentType] + cost);
}
}
}
// 输出结果:取两种类型的较小值
ll answer = min(dp[totalFloors][0], dp[totalFloors][1]);
cout << "Case #" << caseNum << ": " << answer << "\n";
}
return 0;
}
总结
本题通过动态规划结合双指针优化,高效地解决了游戏室分配问题。关键点在于识别连续段的交替结构,并利用前缀和快速计算距离代价。算法在 O(N2)O(N^2)O(N2) 时间内解决了问题,适用于 N≤4000N \leq 4000N≤4000 的数据规模。