问题描述

2020 年 NOI 最后一题是一道结合图论、动态规划与状态压缩的综合性算法题,题目围绕 "疫情期间的物资配送" 展开,具体要求如下:

给定一个有向图 G (V, E),其中节点代表城市,边代表连接城市的道路。每个节点有两个属性:风险等级 l(0-5)和防疫检查时间 t。每条边有三个属性:行驶时间 d、基础成本 c 和最大每日通行量 k。

需要将一批应急物资从起点 S 运往终点 T,满足以下约束:

  1. 每日 0 点至 6 点为宵禁时间,禁止通行(边无法使用)
  2. 经过风险等级为 l 的节点需要额外隔离时间 l×2 小时
  3. 每条边每天的使用次数不能超过其最大通行量 k
  4. 总运输时间不得超过 D 天(按自然日计算)

请设计算法找到满足所有约束条件的最小成本运输方案,若存在多条路径则选择总运输时间最短的方案。

问题分析

本题的核心挑战在于时间与空间约束的复杂交互,传统路径算法需要进行针对性扩展:

  1. 时间周期性约束:宵禁制度引入了以天为周期的时间限制,影响边的可用性
  2. 节点风险成本:不同风险等级的节点会带来额外隔离时间,增加了状态维度
  3. 资源容量限制:边的每日通行量限制要求跟踪时间与使用次数的关系
  4. 多目标优化:首要目标是最小化成本,次要目标是缩短运输时间

问题可转化为带周期性时间约束和容量限制的最小成本路径问题,需要通过时间扩展网络来建模周期性约束。

算法设计

我们采用基于时间扩展网络的改进 Dijkstra 算法,结合动态规划处理周期性约束:

  1. 状态表示:定义 dp [u][d][t] 为第 d 天的 t 时刻到达节点 u 时的最小成本,其中:

    • u 为当前节点
    • d 为当前天数(从 0 开始)
    • t 为当天时刻(0-23 小时)
  2. 状态转移:对于每个状态 (u, d, t),考虑两种决策:

    • 在节点停留:停留 Δt 时间后的新状态为 (u, d', t'),其中 d' 和 t' 根据跨天情况计算
    • 前往相邻节点:使用边 (u, v),需检查是否在宵禁时间,计算到达 v 的天数和时刻,更新成本
  3. 约束处理:

    • 宵禁检查:边只能在 6-24 点使用(非宵禁时段)
    • 容量限制:每条边按天跟踪使用次数,不超过最大通行量 k
    • 风险隔离:到达节点 v 后需增加 l_v×2 小时的隔离时间
    • 时间上限:总天数 d 不得超过 D
  4. 优先级队列:按成本排序,成本相同则按总时间(天数 + 时刻)排序

实现细节
  1. 时间建模:将时间分解为 "天数 + 时刻" 的组合形式,方便处理跨天和周期性约束
  2. 状态剪枝:对于相同节点、天数和时刻,仅保留最小成本状态
  3. 容量跟踪:使用三维数组 count [u][v][d] 记录边 (u, v) 在第 d 天的使用次数
  4. 隔离处理:到达节点后立即计算并累加隔离时间,更新状态时间
  5. 路径重构:通过前驱指针记录每个状态的来源,包括使用的边和停留决策
复杂度分析
  • 时间复杂度:O (E × D × 24 × log (V × D × 24)),其中 D 为最大天数,V 为节点数,E 为边数
  • 空间复杂度:O (V × D × 24 + E × D),主要用于存储 DP 状态和边的每日使用计数

通过合理设置天数上限 D(题目通常限制在 10 天以内),算法能够在规定时间内高效运行。

代码实现

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

#include <iostream>
#include <vector>
#include <queue>
#include <climits>
#include <algorithm>using namespace std;const int MAX_NODES = 505;
const int MAX_DAYS = 15;    // Maximum allowed days
const int HOURS_PER_DAY = 24;
const int CURFEW_START = 0;  // 0:00
const int CURFEW_END = 6;    // 6:00 (exclusive)
const int INF_COST = INT_MAX / 2;// Structure to represent an edge
struct Edge {int to;             // Target nodeint duration;       // Travel time in hoursint cost;           // Transportation costint capacity;       // Maximum daily capacityEdge(int t, int d, int c, int cap): to(t), duration(d), cost(c), capacity(cap) {}
};// Structure to represent a node
struct Node {int risk_level;     // Risk level (0-5)int check_time;     // Check time in hours
};// Structure to represent a state in priority queue
struct State {int node;           // Current nodeint day;            // Current dayint hour;           // Current hour (0-23)int cost;           // Accumulated costState(int n, int d, int h, int c): node(n), day(d), hour(h), cost(c) {}// For priority queue (min-heap based on cost, then day, then hour)bool operator>(const State& other) const {if (cost != other.cost) {return cost > other.cost;}if (day != other.day) {return day > other.day;}return hour > other.hour;}
};// Structure to store DP state information
struct DPState {int cost;           // Minimum cost to reach this stateint prev_node;      // Previous nodeint prev_day;       // Previous dayint prev_hour;      // Previous hourint via_edge;       // Index of edge used to reach here (-1 if stayed)DPState() : cost(INF_COST), prev_node(-1), prev_day(-1), prev_hour(-1), via_edge(-1) {}
};// Helper function to add time and handle day transitions
void add_time(int& day, int& hour, int add_hours) {hour += add_hours;while (hour >= HOURS_PER_DAY) {hour -= HOURS_PER_DAY;day++;}
}int main() {int n, m;               // Number of nodes and edgesint S, T, D_max;        // Start node, target node, maximum allowed days// Read inputcin >> n >> m;cin >> S >> T >> D_max;// Initialize nodesvector<Node> nodes(n + 1);  // 1-indexedfor (int i = 1; i <= n; ++i) {cin >> nodes[i].risk_level >> nodes[i].check_time;}// Read edgesvector<vector<Edge>> edges(n + 1);vector<vector<vector<int>>> edge_usage(n + 1, vector<vector<int>>(n + 1, vector<int>(MAX_DAYS, 0)));  // [u][v][day] = usage countfor (int i = 0; i < m; ++i) {int u, v, d, c, cap;cin >> u >> v >> d >> c >> cap;edges[u].emplace_back(v, d, c, cap);}// DP table: dp[node][day][hour] = best statevector<vector<vector<DPState>>> dp(n + 1, vector<vector<DPState>>(MAX_DAYS, vector<DPState>(HOURS_PER_DAY)));// Priority queue for modified Dijkstra's algorithmpriority_queue<State, vector<State>, greater<State>> pq;// Initialize starting node (day 0, assuming we start at 6:00 to avoid curfew)int start_hour = 6;  // Start at 6:00 to avoid curfewdp[S][0][start_hour].cost = 0;pq.emplace(S, 0, start_hour, 0);// Track best solutionint best_cost = INF_COST;int best_day = -1;int best_hour = -1;// Process stateswhile (!pq.empty()) {State current = pq.top();pq.pop();int u = current.node;int day = current.day;int hour = current.hour;int cost = current.cost;// Check if we've exceeded maximum daysif (day >= D_max) {continue;}// Skip if we've found a better stateif (cost > dp[u][day][hour].cost) {continue;}// Check if we've reached targetif (u == T) {if (cost < best_cost) {best_cost = cost;best_day = day;best_hour = hour;}continue;}// Option 1: Stay at current node for 1 hour (can be extended by staying multiple times)int new_day = day;int new_hour = hour + 1;if (new_hour >= HOURS_PER_DAY) {new_hour -= HOURS_PER_DAY;new_day++;}if (new_day < MAX_DAYS && cost < dp[u][new_day][new_hour].cost) {dp[u][new_day][new_hour].cost = cost;dp[u][new_day][new_hour].prev_node = u;dp[u][new_day][new_hour].prev_day = day;dp[u][new_day][new_hour].prev_hour = hour;dp[u][new_day][new_hour].via_edge = -1;  // Indicates stayingpq.emplace(u, new_day, new_hour, cost);}// Option 2: Travel to adjacent nodes via edgesfor (size_t i = 0; i < edges[u].size(); ++i) {const Edge& edge = edges[u][i];int v = edge.to;int travel_time = edge.duration;int edge_cost = edge.cost;// Check if current time is within curfew (cannot travel during curfew)if (hour >= CURFEW_START && hour < CURFEW_END) {continue;}// Check if we would arrive during curfew (allowed, but departure must be outside)// Calculate arrival timeint arrival_day = day;int arrival_hour = hour + travel_time;// Handle day transitionswhile (arrival_hour >= HOURS_PER_DAY) {arrival_hour -= HOURS_PER_DAY;arrival_day++;}// Check if we exceed maximum daysif (arrival_day >= D_max) {continue;}// Check edge capacity for current dayif (edge_usage[u][v][day] >= edge.capacity) {continue;}// Calculate total time including check and quarantine at destinationint total_additional_time = nodes[v].check_time + nodes[v].risk_level * 2;int final_day = arrival_day;int final_hour = arrival_hour;add_time(final_day, final_hour, total_additional_time);if (final_day >= D_max) {continue;}// Calculate new costint new_cost = cost + edge_cost;// Update edge usage (temporarily)edge_usage[u][v][day]++;// Update state if this path is betterif (new_cost < dp[v][final_day][final_hour].cost) {dp[v][final_day][final_hour].cost = new_cost;dp[v][final_day][final_hour].prev_node = u;dp[v][final_day][final_hour].prev_day = day;dp[v][final_day][final_hour].prev_hour = hour;dp[v][final_day][final_hour].via_edge = i;pq.emplace(v, final_day, final_hour, new_cost);} else {// If not better, rollback edge usageedge_usage[u][v][day]--;}}}// Check if solution existsif (best_cost == INF_COST) {cout << -1 << endl;return 0;}// Reconstruct pathvector<int> path;int curr_node = T;int curr_day = best_day;int curr_hour = best_hour;while (curr_node != -1) {path.push_back(curr_node);const DPState& state = dp[curr_node][curr_day][curr_hour];int next_node = state.prev_node;int next_day = state.prev_day;int next_hour = state.prev_hour;curr_node = next_node;curr_day = next_day;curr_hour = next_hour;}reverse(path.begin(), path.end());// Output resultscout << best_cost << " " << best_day << " " << best_hour << endl;for (size_t i = 0; i < path.size(); ++i) {if (i > 0) cout << " -> ";cout << path[i];}cout << endl;return 0;
}
代码解析

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

  1. 数据结构设计

    • Edge结构体存储边的行驶时间、成本和每日最大通行量
    • Node结构体记录节点的风险等级和防疫检查时间
    • State结构体表示优先队列中的状态,包含当前节点、天数、时刻和累计成本
    • DPState结构体存储动态规划状态信息,包括成本、前驱节点和到达方式
  2. 核心算法实现

    • 采用改进的 Dijkstra 算法,使用优先级队列按成本、天数和时刻排序处理状态
    • 三维 DP 数组dp[node][day][hour]跟踪到达节点的最优状态
    • 实现两种状态转移:在当前节点停留,或通过边前往相邻节点
  3. 约束处理机制

    • 宵禁检查:确保仅在 6-24 点使用边进行运输
    • 容量管理:通过edge_usage数组跟踪每条边每天的使用次数
    • 风险隔离:到达节点后自动计算并累加检查时间和隔离时间(风险等级 ×2)
    • 时间控制:严格限制总天数不超过 D_max
  4. 时间计算

    • add_time辅助函数处理时间累加和跨天计算
    • 将时间分解为 "天数 + 时刻" 的组合形式,方便处理周期性约束
  5. 路径重构与结果输出

    • 通过前驱指针重构完整路径
    • 输出最小成本、到达天数、时刻和完整路径
    • 若不存在满足约束的路径,输出 - 1

该算法通过时间扩展网络模型成功处理了周期性宵禁约束,结合动态规划跟踪节点风险带来的额外时间成本,在满足所有防疫和通行约束的前提下找到了最小成本运输方案。

扩展思考

本题可以从以下几个方向进行扩展:

  1. 引入动态风险等级,节点风险随时间变化,增加状态动态性
  2. 考虑物资保质期,要求在特定时间前送达,增加时间约束复杂度
  3. 扩展为多批次运输,考虑批次间的资源竞争和协调
  4. 加入随机事件(如道路临时封闭、风险等级突变),设计鲁棒性方案

这些扩展更贴近疫情期间复杂多变的实际运输场景,对算法的适应性和前瞻性提出了更高要求。

通过本题的求解可以看出,NOI 题目注重考察选手将实际问题抽象为算法模型的能力,尤其是处理多重约束和周期性条件的能力,这要求选手不仅掌握基础算法,还要具备灵活的问题建模能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/news/917229.shtml
繁体地址,请注明出处:http://hk.pswp.cn/news/917229.shtml
英文地址,请注明出处:http://en.pswp.cn/news/917229.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

加密与安全

目录 一、URL编码&#xff1a; 二、Base64编码&#xff1a; 三、哈希算法&#xff1a; 四、Hmac算法&#xff1a; 五、对称加密算法&#xff1a; 一、URL编码&#xff1a; URL编码是浏览器发送数据给服务器时使用的编码&#xff0c;它通常附加在URL的参数部分。之所以需要…

EasyExcel 公式计算大全

EasyExcel 是基于 Apache POI 的封装&#xff0c;主要专注于简化 Excel 的读写操作&#xff0c;对于公式计算的支持相对有限。以下是 EasyExcel 中处理公式计算的全面指南&#xff1a;1. 基本公式写入1.1 写入简单公式Data public class FormulaData {ExcelProperty("数值…

2025年AI+数模竞赛培训意见征集-最后一轮

在过去几天的“AI时代下2025年数模竞赛培训课程需求调研紧急征集”我们收到了大量老师、学生的反馈。我们通过大家的实际需求&#xff0c;编写了下述2025年AI时代下最新的数学建模竞赛教学课程课程表&#xff0c;具体授课内容以及相关课件、支撑材料都将会免费发布&#xff0c;…

Qwen2 RotaryEmbedding 位置编码仅仅是第一层有吗

Qwen2 RotaryEmbedding 位置编码仅仅是第一层有吗,还是全部层都有 Qwen2 模型中的 Rotary Embedding(旋转位置编码)是应用于所有 Transformer 层 的,而非仅第一层。 1. Transformer 架构的核心逻辑 Qwen2 基于 Decoder-only Transformer 架构,而位置编码(如 Rotary Emb…

CNN卷积神经网络之LeNet和AlexNet经典网络模型(三)

CNN卷积神经网络之LeNet和AlexNet经典网络模型&#xff08;三&#xff09; 文章目录CNN卷积神经网络之LeNet和AlexNet经典网络模型&#xff08;三&#xff09;深度学习两大经典 CNN 模型速览1. LeNet-5&#xff1a;CNN 的开山之作&#xff08;1998&#xff09;2. AlexNet&#…

江协科技STM32 12-2 BKP备份寄存器RTC实时时钟

这一节我们要讲的主要内容是RTC实时时钟&#xff0c;实时时钟本质上是一个定时器&#xff0c;但是这个定时器是专门用来产生年月日时分秒&#xff0c;这种日期和时间信息的。所以学会了STM32的RTC就可以在STM32内部拥有一个独立运行的钟表。想要记录或读取日期和时间&#xff0…

【10】大恒相机SDK C++开发 ——对相机采集的原图像数据IFrameData裁剪ROI 实时显示在pictureBox中,3种方法实现(效率不同)

文章目录1 在回调函数中实现2 独立封装调用2.1 获取图像宽、高、pBuffer、channel2.2 内存图像数据截取ROI并显示2.3 回调函数调用3 for循环嵌套 方法24 for循环嵌套 方法35 按行复制数据提高效率&#xff0c;但很耗内存6 unsafe代码 解释及注意事项 看我另一篇文章7 ConvertTo…

ubuntu22.04系统入门 linux入门(二) 简单命令 多实践以及相关文件管理命令

以下有免费的4090云主机提供ubuntu22.04系统的其他入门实践操作 地址&#xff1a;星宇科技 | GPU服务器 高性能云主机 云服务器-登录 相关兑换码星宇社区---4090算力卡免费体验、共享开发社区-CSDN博客 之所以推荐给大家使用&#xff0c;是因为上面的云主机目前是免费使用的…

分布式ID方案(标记)

一、参考文章-标记 分布式ID方案有哪些&#xff1f;雪花算法如何搞定时钟回拨和动态机器ID&#xff1f; 二、应用 1.百度 uid-generator github项目地址 原理参考 2.百度 uid-generator 扩展应用 灯官网 灯 项目代码 lamp-util 单元模块 lamp-util 单元模块子模块 lamp-…

std::map 加锁

在并发环境下使用std::map&#xff0c;必须采取同步措施。 在并发环境下对 std::map 进行不加锁的读写操作会导致严重的线程安全问题&#xff0c;主要会产生以下几种问题&#xff1a; ⚠️ 主要风险与后果数据竞争&#xff08;Data Race&#xff09; 当多个线程同时修改同一个键…

学习笔记090——Ubuntu 中 UFW 防火墙的使用

文章目录1、允许特定的端口访问2、允许特定 IP 访问某个端口3、允许某个范围的端口4、查看 UFW 状态5、重新加载 UFW6、启用 UFW7、关闭 UFW1、允许特定的端口访问 # 允许 TCP 端口&#xff08;例如 80&#xff09;&#xff1a; sudo ufw allow 80/tcp# 允许 UDP 端口&#xf…

移动端 WebView 内存泄漏与性能退化问题如何排查 实战调试方法汇总

在混合 App 应用中&#xff0c;WebView 页面常承载复杂业务逻辑与交互。随着用户使用时间增长&#xff0c;特别在切换多个页面或反复打开界面后&#xff0c;常常会出现性能下降、页面卡顿、甚至白屏崩溃等现象。这通常是因为页面存在内存泄漏、事件监听未解绑或垃圾回收阻塞导致…

JSON 对象在浏览器中顺序与后端接口返回不一致的问题

一、问题描述 后端接口返回一个字典表的JSON对象&#xff0c;页面展示排序与预期排序不一致。 在浏览器调试面板Response中看到接口原始响应字符串&#xff0c;是期望顺序&#xff1a;在Preview中看到&#xff0c; key “22” 被提到最前&#xff0c;顺序发生变化&#xff1a;页…

Spring MVC数据传递全攻略

Spring MVC数据传递一、前端到后端的数据传递1. 使用 RequestParam 传递简单参数2. 使用 PathVariable传递路径参数3. 使用RequestBody传递 JSON 数据二、后端到前端的数据传递1. 使用Model或 ModelAndView传递数据到前端2. 使用HttpServletResponse直接写回数据3.使用Response…

仓库管理系统-12-前端之头部区域Header基于嵌套路由访问个人中心

文章目录 1 个人中心 1.1 DateUtils.vue(子组件) 1.2 Home.vue(父组件) 1.3 router/index.js(嵌套路由) 1.4 index.vue(路由占位符) 2 Header.vue 2.1 页面布局 2.2 toUser方法 2.3 初始加载 2.4 Header.vue 头部区域Header中有一个个人中心下拉菜单,点击个人中心选项,通过嵌…

【智能协同云图库】第七期:基于AI调用阿里云百炼大模型,实现AI图片编辑功能

摘要&#xff1a;AI 高速发展赋能传统业务&#xff0c;图库网站亦有诸多 AI 应用空间。以 AI 扩图功؜能为例&#xff0c;让我们来学习如何在项目⁠中快速接入 AI 绘图大模型。‏用户可以选择一张已上传的图片&#xff0c;‌通过 AI 扩图得到新的图片&#xff0c;希望可以帮到大…

Notepad++插件安装

方式一&#xff1a;自动安装&#xff08;有些notepad并不好用&#xff0c;推荐方式二&#xff09;工具栏-》插件-》插件管理如下点击安装后会提示&#xff0c;后端安装&#xff0c;安装成功后自动启动&#xff0c;本人使用的v8.6.4的版本&#xff0c;插件基本都无法自动安装&am…

git pull和git fetch的区别

git pull和git fetch是git版本控制系统中的两个基本命令&#xff0c;它们都用于从远程仓库更新本地仓库的信息&#xff0c;但执行的具体操作不同。git fetch:git fetch下载远程仓库最新的内容到你的本地仓库&#xff0c;但它并不自动合并或修改你当前的工作。它取回了远程仓库的…

Item35:考虑virtual函数以外的其他选择

在C++中,虚函数是实现多态的传统方式,但并非唯一选择。过度依赖虚函数可能导致派生类与基类的强耦合,或难以在运行时灵活切换行为。《Effective C++》Item35指出:应根据场景选择更合适的替代方案,包括NVI模式、函数指针、策略模式等。本文解析这些方案的原理、适用场景及实…

Vue3 状态管理新选择:Pinia 从入门到实战

一、什么是pinia? 在 Vue3 生态中&#xff0c;状态管理一直是开发者关注的核心话题。随着 Vuex 的逐步淡出&#xff0c;Pinia 作为官方推荐的状态管理库&#xff0c;凭借其简洁的 API、强大的功能和对 Vue3 特性的完美适配&#xff0c;成为了新时代的不二之选。今天我们就来深…