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

解题思路

关键观察

  1. 交替段结构:最优解中,同类型的游戏室会形成连续的段,不同类型的段交替出现。
  2. 距离计算 :对于类型为 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)
  3. 单调性 :在连续段 [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] 去右边

优化技巧

  1. 前缀和预处理:快速计算区间内玩家数量和加权距离
  2. 双指针优化 :利用决策单调性,将内层循环从 O(N)O(N)O(N) 优化到 O(1)O(1)O(1)
  3. 边界处理:确保至少有两种类型的游戏室

算法复杂度

  • 时间复杂度 :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 的数据规模。

相关推荐
。TAT。3 小时前
C++ - 多态
开发语言·c++·学习·1024程序员节
DreamLife☼3 小时前
Node-RED革命性实践:从智能家居网关到二次开发,全面重新定义可视化编程
mqtt·网关·低代码·智能家居·iot·1024程序员节·node-red
AI视觉网奇3 小时前
json 可视化 2025 coco json
python·1024程序员节
【非典型Coder】3 小时前
CompletableFuture线程池使用
1024程序员节
uesowys3 小时前
Apache Spark算法开发指导-特征转换Normalizer
1024程序员节·spark算法开发指导·特征转换normalizer
mit6.8243 小时前
[cpprestsdk] JSON类--数据处理 (`json::value`, `json::object`, `json::array`)
c++·1024程序员节
lpfasd1233 小时前
第9部分-性能优化、调试与并发设计模式
1024程序员节
潜心编码3 小时前
2026计算机毕业设计课题推荐
1024程序员节
2501_930707783 小时前
使用C#代码在Excel中创建数据透视表
1024程序员节