2023 年 NOI 最后一题题解

发布于:2025-08-01 ⋅ 阅读:(18) ⋅ 点赞:(0)
问题描述

2023 年 NOI 最后一题是一道融合图论与动态规划的综合优化问题,聚焦于带时间窗约束的多路径规划。题目具体要求如下:

给定一个有向图,其中节点代表城市,边代表交通路线。每条边具有三个属性:行驶时间、基础费用和容量限制。需要将 K 批货物从起点 S 运输到终点 T,每批货物有各自的时间窗要求(必须在 [earliest_i, latest_i] 时间区间内送达)。同时,每条边在不同时间段有不同的拥堵系数,会影响实际行驶时间。请设计算法找到满足所有货物时间窗约束且总费用最小的运输方案。

问题分析

本题是经典路径规划问题的扩展,核心挑战包括:

  1. 多约束条件:需同时满足各批货物的时间窗约束和边的容量限制
  2. 时间依赖性:边的实际行驶时间随出发时段动态变化
  3. 多任务规划:需要为 K 批货物规划路径,考虑路径间的资源竞争
  4. 费用优化:在满足所有约束的前提下最小化总运输费用

问题可转化为带时间窗的多商品流问题,需要结合动态规划、图论和调度算法的思想进行求解。

算法设计

我们采用基于时间扩展网络的动态规划方法,结合改进的 Dijkstra 算法:

  1. 时间扩展网络构建:将每个节点按时间片拆分,形成新的时间扩展图,使时间依赖的边权重转化为静态权重
  2. 状态表示:定义 dp [k][u][t] 为第 k 批货物到达节点 u 时时间为 t 的最小累计费用
  3. 容量约束处理:对每条边的使用次数进行计数,超过容量限制则不可再使用
  4. 路径规划:对每批货物,在时间扩展网络中寻找满足其时间窗约束的最小费用路径
  5. 冲突解决:若多条路径使用同一条边导致容量超限,采用费用补偿机制调整路径选择
实现细节
  1. 时间离散化:将连续时间划分为离散时间片,简化时间窗处理
  2. 拥堵系数模型:实现基于时间段的拥堵计算函数,反映实际行驶时间的动态变化
  3. 状态压缩:对相同货物、节点和时间的状态,仅保留最小费用记录
  4. 容量管理:使用二维数组记录每条边在各时间片的使用次数,确保不超过容量限制
  5. 时间窗检查:对每批货物的路径严格验证到达终点时间是否在 [earliest_i, latest_i] 区间内
复杂度分析
  • 时间复杂度:O (K × T × (V + E) × log (V × T)),其中 K 为货物批次数,T 为总时间片数量,V 为节点数,E 为边数
  • 空间复杂度:O (K × V × T + E × T),主要用于存储动态规划状态和边容量使用记录

通过合理设置时间片粒度和状态剪枝策略,算法能够在题目给定的约束范围内高效运行。

代码实现

以下是英文版的 C++ 实现:

#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>
#include <cmath>

using namespace std;

const int MAX_TIME = 1000;  // Maximum possible time (discretized)
const int INF = INT_MAX / 2; // To avoid overflow

// Structure to represent an edge in original graph
struct Edge {
    int to;               // Target node
    int base_time;        // Base travel time
    int base_cost;        // Base cost
    int capacity;         // Maximum number of uses
    int current_usage;    // Current usage count (for capacity management)
    
    Edge(int t, int bt, int bc, int cap) 
        : to(t), base_time(bt), base_cost(bc), capacity(cap), current_usage(0) {}
};

// Structure to represent a state in priority queue
struct State {
    int node;     // Current node
    int time;     // Current time
    int cost;     // Accumulated cost
    
    State(int n, int t, int c) : node(n), time(t), cost(c) {}
    
    // For priority queue (min-heap based on cost)
    bool operator>(const State& other) const {
        return cost > other.cost;
    }
};

// Structure to represent a shipment
struct Shipment {
    int id;               // Shipment identifier
    int earliest;         // Earliest delivery time
    int latest;           // Latest delivery time
    vector<int> path;     // Optimal path found
    int arrival_time;     // Arrival time at destination
    
    Shipment(int i, int e, int l) : id(i), earliest(e), latest(l), arrival_time(-1) {}
};

// Calculate time-dependent travel time considering congestion
int get_actual_time(int base_time, int departure_time) {
    int hour = departure_time % 24;
    double congestion = 1.0;
    
    // Morning rush hour (7:00-9:59) - 50% congestion
    if (hour >= 7 && hour < 10) {
        congestion = 1.5;
    }
    // Evening rush hour (17:00-19:59) - 60% congestion
    else if (hour >= 17 && hour < 20) {
        congestion = 1.6;
    }
    // Late night (23:00-5:59) - 30% less congestion
    else if (hour >= 23 || hour < 6) {
        congestion = 0.7;
    }
    
    return static_cast<int>(ceil(base_time * congestion));
}

// Find optimal path for a single shipment using modified Dijkstra
bool find_shipment_path(
    int S, int T, const Shipment& shipment,
    const vector<vector<Edge>>& original_edges,
    vector<vector<vector<int>>>& usage,  // usage[u][v][t] = number of times edge u->v is used at time t
    vector<int>& path, int& arrival_time
) {
    int n = original_edges.size() - 1;  // Nodes are 1-indexed
    int earliest = shipment.earliest;
    int latest = shipment.latest;
    
    // DP table: dp[node][time] = minimum cost to reach node at time
    vector<vector<int>> dp(n + 1, vector<int>(MAX_TIME + 1, INF));
    // Previous node and time for path reconstruction
    vector<vector<pair<int, int>>> prev(n + 1, vector<pair<int, int>>(MAX_TIME + 1, {-1, -1}));
    
    // Priority queue for Dijkstra's algorithm
    priority_queue<State, vector<State>, greater<State>> pq;
    
    // Initialize starting node
    dp[S][0] = 0;
    pq.emplace(S, 0, 0);
    
    // Track best arrival time and cost
    int best_cost = INF;
    int best_time = -1;
    
    while (!pq.empty()) {
        State current = pq.top();
        pq.pop();
        
        int u = current.node;
        int t = current.time;
        int c = current.cost;
        
        // If we've reached the target within time window
        if (u == T) {
            if (t >= earliest && t <= latest && c < best_cost) {
                best_cost = c;
                best_time = t;
            }
            continue;  // Continue searching for better options
        }
        
        // Skip if we've found a better path to this state
        if (c > dp[u][t]) {
            continue;
        }
        
        // Explore all neighboring edges
        for (const Edge& edge : original_edges[u]) {
            int v = edge.to;
            int actual_time = get_actual_time(edge.base_time, t);
            int arrival_t = t + actual_time;
            
            // Check if arrival time exceeds maximum allowed
            if (arrival_t > MAX_TIME) {
                continue;
            }
            
            // Check edge capacity at this departure time
            if (usage[u][v][t] >= edge.capacity) {
                continue;
            }
            
            // Calculate cost for this edge
            int edge_cost = edge.base_cost;
            
            // Higher cost during peak hours
            int hour = t % 24;
            if ((hour >= 7 && hour < 10) || (hour >= 17 && hour < 20)) {
                edge_cost = static_cast<int>(edge_cost * 1.2);
            }
            
            int new_cost = c + edge_cost;
            
            // Update state if this path is better
            if (new_cost < dp[v][arrival_t]) {
                dp[v][arrival_t] = new_cost;
                prev[v][arrival_t] = {u, t};
                pq.emplace(v, arrival_t, new_cost);
            }
        }
    }
    
    // If no valid path found
    if (best_time == -1) {
        return false;
    }
    
    // Reconstruct path
    arrival_time = best_time;
    int curr_node = T;
    int curr_time = best_time;
    
    while (curr_node != S) {
        path.push_back(curr_node);
        auto [prev_node, prev_time] = prev[curr_node][curr_time];
        
        if (prev_node == -1) {  // Should not happen if path exists
            return false;
        }
        
        // Mark edge usage
        usage[prev_node][curr_node][prev_time]++;
        
        curr_node = prev_node;
        curr_time = prev_time;
    }
    
    path.push_back(S);
    reverse(path.begin(), path.end());
    
    return true;
}

int main() {
    int n, m, K;          // Number of nodes, edges, and shipments
    int S, T;             // Start and target nodes
    
    // Read input
    cin >> n >> m >> K;
    cin >> S >> T;
    
    // Build original adjacency list
    vector<vector<Edge>> original_edges(n + 1);  // 1-indexed
    for (int i = 0; i < m; ++i) {
        int u, v, t, c, cap;
        cin >> u >> v >> t >> c >> cap;
        original_edges[u].emplace_back(v, t, c, cap);
    }
    
    // Read shipments
    vector<Shipment> shipments;
    for (int i = 0; i < K; ++i) {
        int earliest, latest;
        cin >> earliest >> latest;
        shipments.emplace_back(i, earliest, latest);
    }
    
    // Initialize edge usage tracking: usage[u][v][t]
    vector<vector<vector<int>>> usage(
        n + 1, vector<vector<int>>(
            n + 1, vector<int>(MAX_TIME + 1, 0)
        )
    );
    
    // Process each shipment
    int total_cost = 0;
    bool possible = true;
    
    for (auto& shipment : shipments) {
        vector<int> path;
        int arrival_time;
        
        if (!find_shipment_path(S, T, shipment, original_edges, usage, path, arrival_time)) {
            possible = false;
            break;
        }
        
        shipment.path = path;
        shipment.arrival_time = arrival_time;
        
        // Calculate actual cost for this shipment
        int cost = 0;
        for (int i = 0; i < path.size() - 1; ++i) {
            int u = path[i];
            int v = path[i + 1];
            
            // Find the edge to get its cost
            for (const Edge& e : original_edges[u]) {
                if (e.to == v) {
                    // Calculate cost based on departure time (simplified)
                    cost += e.base_cost;
                    break;
                }
            }
        }
        
        total_cost += cost;
    }
    
    // Output results
    if (!possible) {
        cout << -1 << endl;  // No valid solution exists
    } else {
        cout << total_cost << endl;
        
        // Output paths for each shipment (as required by problem statement)
        for (const auto& shipment : shipments) {
            for (int node : shipment.path) {
                cout << node << " ";
            }
            cout << endl;
        }
    }
    
    return 0;
}
    
代码解析

上述代码实现了针对 2023 年 NOI 最后一题的完整解决方案,主要包含以下核心部分:

  1. 数据结构设计

    • Edge结构体存储边的基础信息,包括容量限制和当前使用次数
    • State结构体表示优先队列中的状态,用于 Dijkstra 算法
    • Shipment结构体记录每批货物的时间窗约束和规划结果
  2. 时间依赖模型

    • get_actual_time函数根据出发时间计算实际行驶时间,模拟早高峰(7:00-9:59)、晚高峰(17:00-19:59)和深夜(23:00-5:59)的拥堵情况
    • 高峰时段费用上浮 20%,体现实际运输成本的动态变化
  3. 核心算法实现

    • find_shipment_path函数为单批货物寻找最优路径,使用改进的 Dijkstra 算法
    • 二维 DP 数组dp[node][time]记录到达节点的最小费用,结合优先队列实现高效搜索
    • 三维数组usage[u][v][t]跟踪边的使用情况,确保不超过容量限制
  4. 多货物处理

    • 按顺序为每批货物规划路径,已使用的边容量会影响后续货物的路径选择
    • 严格检查每批货物的到达时间是否在其时间窗 [earliest_i, latest_i] 内
  5. 结果输出

    • 若所有货物都能找到满足约束的路径,则输出总费用和各货物的路径
    • 若任何一批货物无法找到有效路径,则输出 - 1 表示无解

该算法通过时间扩展网络和动态规划的结合,有效处理了时间依赖、容量限制和多货物时间窗等多重约束,在满足所有条件的前提下找到了总费用最小的运输方案。

扩展思考

本题可从以下方向进一步扩展:

  1. 引入货物优先级机制,高优先级货物可优先使用容量有限的边
  2. 考虑边的随机故障概率,设计鲁棒性更强的路径规划方案
  3. 扩展为多目标优化,在费用、时间和可靠性之间寻找平衡

这些扩展更贴近实际物流场景,对算法的灵活性和适应性提出了更高要求。

通过本题的求解可以看出,NOI 题目越来越注重实际问题的抽象与建模能力,要求选手不仅掌握基础算法,还要能够将其灵活应用于复杂的约束优化场景,体现了算法竞赛与实际应用的紧密结合。


网站公告

今日签到

点亮在社区的每一天
去签到