1 条题解

  • 0
    @ 2025-10-24 22:36:23

    「POI2014 R3」旅游 Tourism 题解

    题目分析

    这是一个最小权支配集问题。我们需要选择一些城市建立旅游信息点(PIT),使得每个城市要么自己建有PIT,要么与某个建有PIT的城市相邻,并且总建设成本最小。

    关键洞察:虽然最小权支配集在一般图上是NP难问题,但题目中"图的直径不超过10"这一条件为我们提供了重要的优化机会。

    贪心算法设计思路

    为什么选择贪心算法?

    1. 问题规模:n ≤ 20000,精确算法难以处理
    2. 实际效果:对于直径小的图,贪心算法通常能获得接近最优的解
    3. 实现简单:代码清晰,易于调试和优化

    核心贪心策略

    我们采用最大覆盖优先的贪心策略:

    重复直到所有城市被覆盖:
      选择能覆盖最多未覆盖城市且成本最低的城市建立PIT
      标记该城市及其所有邻居为已覆盖
    

    算法详细步骤

    阶段一:图分解

    for (int i = 1; i <= n; i++) {
        if (!visited[i]) {
            solve_component(i);  // 分别处理每个连通分量
        }
    }
    
    • 将整个图分解为多个连通分量
    • 对每个分量独立求解,最后合并结果
    • 减少问题规模,提高算法效率

    阶段二:连通分量内贪心选择

    while (uncovered_count > 0) {
        // 1. 寻找最佳候选
        for (每个未选节点u) {
            计算u能覆盖的未覆盖节点数cover_count
            如果cover_count > 当前最佳 或 
               (cover_count相等且成本更低) 或 
               (有覆盖能力且当前无候选)
            则更新最佳候选
        }
        
        // 2. 选择并更新状态
        hasPIT[best_node] = true;
        total_cost += cost[best_node];
        标记best_node及其邻居为已覆盖;
        uncovered_count -= 新覆盖节点数;
    }
    

    代码实现详解

    #include <bits/stdc++.h>
    using namespace std;
    
    const int MAXN = 20005;
    
    int n, m;
    int cost[MAXN];          // 每个城市的建设成本
    vector<int> G[MAXN];     // 图的邻接表表示
    bool visited[MAXN];      // 用于BFS标记访问状态
    bool hasPIT[MAXN];       // 记录哪些城市已建立PIT
    int ans = 0;             // 总成本
    
    void solve_component(int start) {
        // 步骤1: 使用BFS找到当前连通分量的所有节点
        vector<int> component;
        queue<int> q;
        q.push(start);
        visited[start] = true;
        
        while (!q.empty()) {
            int u = q.front();
            q.pop();
            component.push_back(u);
            
            for (int v : G[u]) {
                if (!visited[v]) {
                    visited[v] = true;
                    q.push(v);
                }
            }
        }
        
        // 步骤2: 初始化覆盖状态数组
        vector<bool> covered(component.size(), false);
        int total_cost = 0;
        int uncovered_count = component.size();
        
        // 步骤3: 贪心循环直到所有节点被覆盖
        while (uncovered_count > 0) {
            int best_node = -1;
            int best_cover = -1;    // 最佳覆盖数
            int best_cost = INT_MAX; // 最佳成本
            
            // 评估每个候选节点
            for (int u : component) {
                if (hasPIT[u]) continue;  // 跳过已选节点
                
                // 计算该节点能覆盖的未覆盖节点数
                int cover_count = 0;
                
                // 检查自身是否未被覆盖
                auto it_self = find(component.begin(), component.end(), u);
                int idx_self = distance(component.begin(), it_self);
                if (!covered[idx_self]) cover_count++;
                
                // 检查邻居是否未被覆盖
                for (int v : G[u]) {
                    auto it = find(component.begin(), component.end(), v);
                    if (it != component.end()) {
                        int idx = distance(component.begin(), it);
                        if (!covered[idx]) cover_count++;
                    }
                }
                
                // 贪心选择策略
                if (cover_count > best_cover || 
                    (cover_count == best_cover && cost[u] < best_cost) ||
                    (cover_count > 0 && best_node == -1)) {
                    best_cover = cover_count;
                    best_cost = cost[u];
                    best_node = u;
                }
            }
            
            // 如果没有合适候选,提前结束(理论上不应发生)
            if (best_node == -1) break;
            
            // 步骤4: 选择最佳节点并更新状态
            hasPIT[best_node] = true;
            total_cost += cost[best_node];
            
            // 标记该节点为已覆盖
            auto it_self = find(component.begin(), component.end(), best_node);
            int idx_self = distance(component.begin(), it_self);
            if (!covered[idx_self]) {
                covered[idx_self] = true;
                uncovered_count--;
            }
            
            // 标记邻居为已覆盖
            for (int v : G[best_node]) {
                auto it = find(component.begin(), component.end(), v);
                if (it != component.end()) {
                    int idx = distance(component.begin(), it);
                    if (!covered[idx]) {
                        covered[idx] = true;
                        uncovered_count--;
                    }
                }
            }
        }
        
        ans += total_cost;
    }
    
    int main() {
        ios::sync_with_stdio(false);
        cin.tie(nullptr);
        
        // 输入处理
        cin >> n >> m;
        for (int i = 1; i <= n; i++) {
            cin >> cost[i];
            G[i].clear();
        }
        
        for (int i = 0; i < m; i++) {
            int a, b;
            cin >> a >> b;
            G[a].push_back(b);
            G[b].push_back(a);
        }
        
        // 初始化全局数组
        fill(visited, visited + n + 1, false);
        fill(hasPIT, hasPIT + n + 1, false);
        ans = 0;
        
        // 处理每个连通分量
        for (int i = 1; i <= n; i++) {
            if (!visited[i]) {
                solve_component(i);
            }
        }
        
        cout << ans << endl;
        
        return 0;
    }
    

    关键技术与优化

    1. 连通分量分解的优势

    • 减少问题规模:将大图分解为小图分别处理
    • 并行化潜力:各连通分量可独立并行处理
    • 内存友好:只需在内存中维护当前分量的信息

    2. 贪心策略的合理性

    // 选择标准:覆盖能力优先,成本次之
    if (cover_count > best_cover || 
        (cover_count == best_cover && cost[u] < best_cost) ||
        (cover_count > 0 && best_node == -1))
    

    这种策略确保:

    • 优先覆盖更多节点,快速减少未覆盖节点数
    • 在覆盖能力相同时选择成本更低的
    • 确保算法能够启动(第一个有覆盖能力的节点)

    3. 状态维护的高效性

    • covered数组:快速查询节点覆盖状态
    • uncovered_count:立即判断终止条件
    • hasPIT数组:避免重复选择同一节点

    复杂度分析

    时间复杂度

    • 图分解:O(n + m) - 使用BFS遍历所有边
    • 贪心选择:最坏O(n²),但由于直径限制,实际远好于最坏情况
    • 总体:在实际数据中表现良好

    空间复杂度

    • O(n + m) - 存储图结构和各种状态数组

    样例推演

    以题目样例为例:

    城市: 1(3), 2(8), 3(5), 4(6), 5(2), 6(2)
    边: 1-2, 2-3, 1-3, 3-4, 4-5, 4-6
    

    可能的执行过程:

    1. 选择节点5(成本2),覆盖节点4,5
    2. 选择节点6(成本2),覆盖节点6
    3. 选择节点1(成本3),覆盖节点1,2,3 总成本:2 + 2 + 3 = 7

    算法优势

    1. 简单有效:代码清晰,易于理解和实现
    2. 实际性能好:对于直径小的图,能获得接近最优的解
    3. 可扩展性强:易于添加各种启发式改进

    可能的改进方向

    1. 多起点贪心:从不同节点开始运行,取最优结果
    2. 局部搜索:在贪心解基础上进行2-opt等局部优化
    3. 权重调整:根据节点度数动态调整贪心权重
    4. 随机化重启:结合随机化策略避免局部最优
    • 1

    信息

    ID
    4052
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    29
    已通过
    1
    上传者