UVa 12991 Game Rooms
题目分析
问题描述
公司新建了一栋 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] 的玩家去左边的游戏室更近
- [mid+1,R][mid+1, R][mid+1,R] 的玩家去右边的游戏室更近
动态规划设计
定义状态:
- 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 数组
代码实现
// 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; // 空区间代价为 0ll 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 的数据规模。
