笔记
2025-10-25 17:28:24
发布于:浙江
题解:宝石争夺战
问题分析
贝丝与艾尔西公主通过轮流取宝石的游戏来决定宝石归属,游戏规则如下:
- 宝石总数为 S
- 贝丝先手,两人轮流取宝石
- 每次取的数量必须是对称数(回文数,且无前导零)
- 无法取宝石的人输
关键洞察
通过分析游戏规律,我们发现了一个简洁而优雅的解决方案:
核心观察:游戏结果只取决于宝石总数 S 的个位数字!
为什么?
- 对称数的特性:对称数(回文数)的个位数永远不会是0(因为无前导零)
- 关键策略:
- 如果当前宝石数的个位不是0,当前玩家总是可以取走个位数的宝石(1-9都是对称数),使剩余宝石数变为10的倍数
- 如果当前宝石数的个位是0,玩家无法一次性取完所有宝石,且无论取多少,都会让对手处于有利位置
解决方案
- 如果 S 的个位数字是0 → 艾尔西获胜(输出 "E")
- 如果 S 的个位数字不是0 → 贝丝获胜(输出 "B")
代码实现
#include <bits/stdc++.h>
using namespace std;
int main() {
int T;
cin >> T;
while (T--) {
string s; // 使用字符串处理大数
cin >> s;
int len = s.size();
if (s[len - 1] == '0')
cout << "E" << endl;
else
cout << "B" << endl;
}
return 0;
}
算法说明
- 时间复杂度:O(T),其中 T 是测试用例数量
- 空间复杂度:O(1)
- 优势:避免了复杂的博弈论计算,直接通过个位数字判断结果
示例验证
| 输入 | 输出 | 解释 |
|---|---|---|
| 8 | B | 个位8≠0,贝丝直接取完获胜 |
| 10 | E | 个位=0,贝丝无法直接取完,艾尔西获胜 |
| 12 | B | 个位2≠0,贝丝取2剩10,迫使艾尔西处于不利位置 |
总结
这个问题展示了博弈论中一个有趣的现象:有时候复杂的游戏规则背后隐藏着极其简单的胜负判定条件。通过深入分析游戏的核心机制,我们找到了这个优雅的解决方案,避免了繁琐的状态转移计算。
题解:石子雕像
问题分析
在一个古老的村庄中,村民需要将各种不同数量的石子调整为至少k种石子数量相同。他们只能进行一种操作:将石子的数量除以2并向下取整。目标是找到实现这一目标所需的最少操作次数。
解题思路
-
关键观察:每种石子可以通过不断除以2(向下取整)的操作,最终变成0。在这个过程中,石子会经过一系列中间值。
-
算法选择:
- 对于每种石子,我们记录它除以2过程中经过的所有值以及对应的操作次数
- 使用数组
bucket记录每个目标值能被多少种石子达到 - 使用数组
cnt记录达到每个目标值所需的总操作次数(只考虑前k个最小的操作次数)
-
具体实现:
- 首先对石子数量进行排序
- 对于每个石子,不断除以2直到0,记录中间值和操作次数
- 更新
bucket和cnt数组 - 最后遍历所有可能的目标值,找到满足条件的最小操作次数
代码实现
#include <bits/stdc++.h>
using namespace std;
const int max_n = 2e5 + 10, INF = 5e6 + 10;
int n, k;
int a[max_n], cnt[max_n], bucket[max_n];
void change(int x) {
int t = 0;
while (x) {
bucket[x] += 1; // 记录有多少数字可以变成x
if (bucket[x] <= k) {
cnt[x] += t; // 累加前k个变成x的操作次数
}
x /= 2;
++t;
}
}
int main() {
cin >> n >> k;
int ma = 0;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
ma = max(a[i], ma);
}
sort(a + 1, a + 1 + n);
for (int i = 1; i <= n; ++i) {
change(a[i]);
}
int ans = INF;
for (int target = 1; target <= ma; ++target) {
if (bucket[target] >= k) {
ans = min(ans, cnt[target]);
}
}
cout << ans << endl;
return 0;
}
算法复杂度
- 时间复杂度:O(n log M),其中M是石子数量的最大值
- 空间复杂度:O(M),其中M是石子数量的最大值
示例解释
示例1:
- 输入:5 3, [1, 2, 2, 4, 5]
- 输出:1
- 解释:将4除以2得到2,这样就有3个石子的数量为2(原来的2个2和新产生的2)
示例2:
- 输入:5 3, [1, 2, 3, 4, 5]
- 输出:2
- 解释:需要更多操作才能使得至少3种石子数量相同
这种方法通过模拟石子除以2的过程,高效地找到了满足条件的最小操作次数。
题解:滑雪场连通问题
题目分析
本题要求找到最小的安全高度 D,使得滑雪场中所有重要点(标记为1的格子)能够相互到达。两个格子可以相互到达的条件是:
- 它们是相邻的(有公共边)
- 它们的高度差绝对值不超过 D
解题思路
问题转换
我们可以将这个问题转换为:在给定的高度差限制 D 下,判断所有重要点是否在同一个连通分量中。我们需要找到满足这个条件的最小 D。
二分搜索 + BFS 解法
由于 D 的取值范围很大(0 到 10^9),直接枚举所有可能的 D 值效率太低。我们可以使用二分搜索来快速找到最小的满足条件的 D。
算法步骤:
- 读取输入数据,包括地图尺寸、高度信息和重要点位置
- 统计重要点总数,并记录任意一个重要点的位置作为搜索起点
- 使用二分搜索在可能的 D 值范围内(0 到 10^9)查找最小满足条件的 D
- 对于每个候选的 D 值,使用 BFS 检查从起点出发能否访问到所有重要点
算法实现
#include <bits/stdc++.h>
using namespace std;
int n, m;
int dir_x[] = {1, 0, -1, 0};
int dir_y[] = {0, 1, 0, -1};
int h[510][510]; // 存储高度
int imp[510][510]; // 存储是否为重要点
bool vis[510][510]; // 标记数组
struct point {
int x, y; // 横纵坐标
};
int sx, sy; // 起点坐标
// BFS 检查在给定高度差限制下是否能访问所有重要点
bool bfs(int num, int dif) {
memset(vis, 0, sizeof(vis)); // 标记数组清空
queue<point> q;
q.push({sx, sy}); // 起点入队
vis[sx][sy] = 1; // 起点标记
if (imp[sx][sy]) num--; // 如果起点是重要点,计数减一
while (!q.empty()) {
int x = q.front().x; // 取队头
int y = q.front().y;
q.pop();
for (int i = 0; i < 4; i++) {
int x1 = x + dir_x[i], y1 = y + dir_y[i]; // 相邻格子
// 检查边界和访问状态
if (x1 < 1 || x1 > n || y1 < 1 || y1 > m || vis[x1][y1])
continue;
// 检查高度差是否在允许范围内
if (abs(h[x1][y1] - h[x][y]) > dif)
continue;
// 如果是重要点,更新计数
if (imp[x1][y1])
num--;
vis[x1][y1] = 1; // 标记已访问
q.push({x1, y1});
}
}
return num == 0; // 检查是否访问了所有重要点
}
int main() {
freopen("skiing.in", "r", stdin);
freopen("skiing.out", "w", stdout);
cin >> n >> m;
// 读取高度信息
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> h[i][j];
}
}
// 读取重要点信息并统计数量
int num = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> imp[i][j];
if (imp[i][j] == 1) {
if (num == 0) { // 记录第一个重要点作为起点
sx = i;
sy = j;
}
num++;
}
}
}
// 二分搜索寻找最小满足条件的 D
int l = 0, r = 1000000000;
int ans = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (bfs(num, mid)) {
r = mid - 1;
ans = mid;
} else {
l = mid + 1;
}
}
cout << ans << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
算法分析
时间复杂度
- 二分搜索:O(log(max_D)),其中 max_D = 10^9,约为 30 次迭代
- 每次 BFS:O(M×N),其中 M 和 N 最大为 500,即 250,000 个格子
- 总复杂度:O(log(max_D) × M × N) ≈ 30 × 250,000 = 7,500,000 次操作
空间复杂度
- O(M×N) 用于存储高度信息、重要点标记和访问状态
关键点说明
- 二分搜索的应用:通过二分法在较大的 D 值范围内快速定位最小满足条件的值
- BFS 连通性检查:对于每个候选 D 值,使用 BFS 检查从任意一个重要点出发能否到达所有其他重要点
- 起点选择:选择任意一个重要点作为 BFS 起点,因为如果所有重要点连通,从任何一个出发都能到达所有其他重要点
- 高度差限制:在 BFS 扩展时,只考虑高度差不超过当前 D 值的相邻格子
问题分解与解决步骤
步骤 1:理解问题本质
- 将滑雪场建模为网格图
- 将连通性问题转化为图论问题
- 确定约束条件:相邻格子、高度差限制
步骤 2:确定算法框架
- 识别这是一个最优化问题(最小化 D)
- 选择二分搜索作为主要算法框架
- 确定验证函数(BFS)来检查给定 D 是否满足条件
步骤 3:实现验证函数
- 使用 BFS 进行连通性检查
- 正确处理边界条件和访问标记
- 高效统计重要点访问情况
步骤 4:优化与调试
- 选择合适的起点以减少不必要的计算
- 处理极端情况(如只有一个重要点)
- 确保算法在数据范围内高效运行
总结
本题通过将问题转换为连通性检查,并利用二分搜索优化查找过程,有效地解决了在大数据范围内的最小 D 值查找问题。这种"二分答案 + 验证"的思路在解决最优化问题时非常实用,特别是当直接求解困难但验证相对容易时。
关键思路分解:
- 问题建模:将物理问题转化为图论问题
- 算法选择:二分搜索处理最优化,BFS 处理连通性验证
- 实现细节:正确处理边界、标记和计数
- 性能优化:利用问题特性减少不必要的计算
这种分解思维有助于系统地解决复杂问题,将大问题拆解为可管理的小问题,并选择合适的算法和数据结构来解决每个子问题。
题解:护栏上色
问题分析
阿北需要为护栏上色,护栏由N个1米长的小段组成。他有26种颜色(A-Z表示,A最浅,Z最深)。初始时所有护栏未被上色,每次操作可以给任意连续若干小段涂上同一种颜色,深色可以覆盖浅色,但浅色不能覆盖深色。
现在阿北考虑Q个候选区间,对于每个区间[a,b],需要将区间外的护栏涂上目标颜色,而区间内保持未上色状态。要求计算每个候选情况下的最少操作次数。
关键洞察
通过分析涂色规则,我们发现了一个高效的解决方案:
- 涂色特性:深色可以覆盖浅色,但浅色不能覆盖深色
- 最优策略:从浅到深依次涂色,每次涂色时尽量覆盖更长的连续区间
- 问题分解:对于每个查询[l,r],问题可分解为[1,l-1]和[r+1,n]两个独立区间的涂色问题
解决方案
使用预处理前缀和后缀数组的方法:
pre[i]:表示从第1段到第i段的最小涂色次数suf[i]:表示从第i段到第n段的最小涂色次数
对于每个查询[l,r],答案就是 pre[l-1] + suf[r+1]
算法核心
预处理过程中维护一个颜色标记数组vis,记录当前可用的颜色状态:
-
处理前缀数组:
- 从左到右遍历
- 遇到比当前颜色深的颜色时,清除这些颜色的标记(因为浅色不能覆盖深色)
- 如果当前颜色未被标记,则增加操作次数并标记该颜色
-
处理后缀数组:
- 从右到左遍历
- 同样清除比当前颜色深的颜色标记
- 如果当前颜色未被标记,则增加操作次数并标记该颜色
代码实现
#include<bits/stdc++.h>
using namespace std;
int n, q;
char a[100010];
int pre[100010], suf[100010];
int vis[100010];
int main() {
freopen("rail.in", "r", stdin);
freopen("rail.out", "w", stdout);
cin >> n >> q;
for(int i = 1; i <= n; i++) {
cin >> a[i];
}
// 预处理前缀数组
for(int i = 1; i <= n; i++) {
pre[i] = pre[i-1];
// 清除比当前颜色深的颜色标记
for(int j = a[i] - 'A' + 1; j < 26; j++) {
vis[j] = 0;
}
// 如果当前颜色未出现过,需要新的一笔
if(vis[a[i] - 'A'] == 0) {
vis[a[i] - 'A'] = 1;
pre[i]++;
}
}
// 初始化标记数组
memset(vis, 0, sizeof(vis));
// 预处理后缀数组
for(int i = n; i >= 1; i--) {
suf[i] = suf[i+1];
// 清除比当前颜色深的颜色标记
for(int j = a[i] - 'A' + 1; j < 26; j++) {
vis[j] = 0;
}
// 如果当前颜色未出现过,需要新的一笔
if(vis[a[i] - 'A'] == 0) {
vis[a[i] - 'A'] = 1;
suf[i]++;
}
}
// 处理查询
for(int i = 1; i <= q; i++) {
int l, r;
cin >> l >> r;
cout << pre[l-1] + suf[r+1] << endl;
}
fclose(stdin);
fclose(stdout);
return 0;
}
算法复杂度
- 时间复杂度:O(n × 26 + q),其中26是颜色种类数
- 空间复杂度:O(n)
示例验证
输入:
8 2
ABBAABCB
3 6
1 4
处理过程:
- 预处理得到前缀数组和后缀数组
- 查询[3,6]:pre[2] + suf[7] = 2 + 2 = 4
- 查询[1,4]:pre[0] + suf[5] = 0 + 3 = 3
输出:
4
3
总结
这个问题通过巧妙的预处理技术,将每个查询的复杂度降低到O(1)。关键在于利用颜色覆盖的特性,在预处理时维护颜色状态,从而快速计算出任意区间的最小操作次数。这种方法既保证了正确性,又具有很高的效率,能够处理大规模数据。
题解:孤独的子串计数
题目分析
本题要求统计一个仅由字符 'G' 和 'H' 组成的字符串中,所有"孤独的子串"的数量。孤独的子串定义为:
- 子串中恰好只包含一个 'G' 或一个 'H'
- 子串长度不低于 3
解题思路
暴力解法(适用于小数据范围)
最直观的方法是枚举所有长度至少为3的子串,检查每个子串中 'G' 和 'H' 的数量。这种方法的时间复杂度为 O(N³),只能通过 N ≤ 50 的测试点。
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("lonely.in", "r", stdin);
freopen("lonely.out", "w", stdout);
long long n;
string s;
cin >> n >> s;
long long sum = 0;
// 枚举所有长度至少为3的子串
for (int l = 0; l < n; l++) {
for (int r = l + 2; r < n; r++) {
int g = 0, h = 0;
// 统计当前子串中G和H的数量
for (int i = l; i <= r; i++) {
if (s[i] == 'G') g++;
else h++;
}
// 检查是否满足孤独子串条件
if (g == 1 || h == 1) {
sum++;
}
}
}
cout << sum << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
优化解法(适用于中等数据范围)
通过双指针技巧,可以在枚举子串时避免重复计算字符数量,将时间复杂度优化到 O(N²)。这种方法可以处理 N ≤ 5000 的数据。
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("lonely.in", "r", stdin);
freopen("lonely.out", "w", stdout);
long long n;
string s;
cin >> n >> s;
long long sum = 0;
for (int l = 0; l < n; l++) {
int g = 0, h = 0;
// 初始化长度为2的子串
for (int i = l; i <= l + 1 && i < n; i++) {
if (s[i] == 'G') g++;
else h++;
}
// 扩展右端点
for (int r = l + 2; r < n; r++) {
// 更新当前字符计数
if (s[r] == 'G') g++;
else h++;
// 检查是否满足条件
if (g == 1 || h == 1) {
sum++;
}
// 剪枝:如果G和H都至少有2个,后续子串不可能满足条件
if (g >= 2 && h >= 2) {
break;
}
}
}
cout << sum << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
高效解法(适用于大数据范围)
对于 N ≤ 5×10⁵ 的数据范围,我们需要 O(N) 或 O(N log N) 的算法。
核心思路是:对于每个位置 i,计算以 s[i] 作为子串中唯一字符(即唯一的 'G' 或 'H')的所有满足条件的子串数量。
具体步骤:
- 对于每个位置 i,向左找到第一个与 s[i] 相同的字符位置 l
- 向右找到第一个与 s[i] 相同的字符位置 r
- 计算包含位置 i 且满足条件的子串数量:
- 如果 s[i] 在子串的最左边或最右边,计算相应的子串数量
- 否则,分别计算 s[i] 作为子串起点、终点和中间位置的情况
算法实现
#include <bits/stdc++.h>
using namespace std;
int main() {
freopen("lonely.in", "r", stdin);
freopen("lonely.out", "w", stdout);
int n;
string s;
cin >> n >> s;
long long sum = 0;
for (int i = 0; i < n; i++) {
int l = i - 1, r = i + 1;
// 向左寻找第一个与s[i]相同的位置
while (l >= 0 && s[l] != s[i]) {
l--;
}
l++; // [l, i-1] 区间内的字符都与s[i]不同
// 向右寻找第一个与s[i]相同的位置
while (r < n && s[r] != s[i]) {
r++;
}
r--; // [i+1, r] 区间内的字符都与s[i]不同
// 计算包含位置i的孤独子串数量
if (l == i || r == i) {
// s[i]在子串的最左边或最右边
if (l != i || r != i) {
sum += r - l - 1;
}
} else {
// s[i]作为子串终点
sum += i - l - 1;
// s[i]作为子串起点
sum += r - i - 1;
// s[i]在子串中间
sum += 1LL * (i - l) * (r - i);
}
}
cout << sum << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
算法分析
- 时间复杂度:O(N),每个字符最多被访问3次(向左扩展、向右扩展、作为唯一字符计算)
- 空间复杂度:O(1),只使用了常数级别的额外空间
关键点说明
- 唯一字符定位:对于每个位置 i,找到它作为唯一字符的最大区间 [l, r]
- 三种情况处理:
- 字符在子串最左/最右边
- 字符作为子串起点/终点
- 字符在子串中间位置
- 乘法原理应用:当字符在子串中间时,使用乘法原理计算所有可能的子串
总结
本题的关键在于转换思路,从枚举所有子串转为对每个字符计算它能作为唯一字符的子串数量。这种"以每个元素为中心"的思考方式在处理子串/子数组问题时非常有效,能够将问题复杂度从 O(N²) 或 O(N³) 降低到 O(N)。
题解:滑雪场连通问题
题目分析
本题要求找到最小的安全高度 D,使得滑雪场中所有重要点(标记为1的格子)能够相互到达。两个格子可以相互到达的条件是:
- 它们是相邻的(有公共边)
- 它们的高度差绝对值不超过 D
解题思路
问题转换
我们可以将这个问题转换为:在给定的高度差限制 D 下,判断所有重要点是否在同一个连通分量中。我们需要找到满足这个条件的最小 D。
二分搜索 + BFS 解法
由于 D 的取值范围很大(0 到 10^9),直接枚举所有可能的 D 值效率太低。我们可以使用二分搜索来快速找到最小的满足条件的 D。
算法步骤:
- 读取输入数据,包括地图尺寸、高度信息和重要点位置
- 统计重要点总数,并记录任意一个重要点的位置作为搜索起点
- 使用二分搜索在可能的 D 值范围内(0 到 10^9)查找最小满足条件的 D
- 对于每个候选的 D 值,使用 BFS 检查从起点出发能否访问到所有重要点
算法实现
#include <bits/stdc++.h>
using namespace std;
int n, m;
int dir_x[] = {1, 0, -1, 0};
int dir_y[] = {0, 1, 0, -1};
int h[510][510]; // 存储高度
int imp[510][510]; // 存储是否为重要点
bool vis[510][510]; // 标记数组
struct point {
int x, y; // 横纵坐标
};
int sx, sy; // 起点坐标
// BFS 检查在给定高度差限制下是否能访问所有重要点
bool bfs(int num, int dif) {
memset(vis, 0, sizeof(vis)); // 标记数组清空
queue<point> q;
q.push({sx, sy}); // 起点入队
vis[sx][sy] = 1; // 起点标记
if (imp[sx][sy]) num--; // 如果起点是重要点,计数减一
while (!q.empty()) {
int x = q.front().x; // 取队头
int y = q.front().y;
q.pop();
for (int i = 0; i < 4; i++) {
int x1 = x + dir_x[i], y1 = y + dir_y[i]; // 相邻格子
// 检查边界和访问状态
if (x1 < 1 || x1 > n || y1 < 1 || y1 > m || vis[x1][y1])
continue;
// 检查高度差是否在允许范围内
if (abs(h[x1][y1] - h[x][y]) > dif)
continue;
// 如果是重要点,更新计数
if (imp[x1][y1])
num--;
vis[x1][y1] = 1; // 标记已访问
q.push({x1, y1});
}
}
return num == 0; // 检查是否访问了所有重要点
}
int main() {
freopen("skiing.in", "r", stdin);
freopen("skiing.out", "w", stdout);
cin >> n >> m;
// 读取高度信息
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> h[i][j];
}
}
// 读取重要点信息并统计数量
int num = 0;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
cin >> imp[i][j];
if (imp[i][j] == 1) {
if (num == 0) { // 记录第一个重要点作为起点
sx = i;
sy = j;
}
num++;
}
}
}
// 二分搜索寻找最小满足条件的 D
int l = 0, r = 1000000000;
int ans = 0;
while (l <= r) {
int mid = (l + r) / 2;
if (bfs(num, mid)) {
r = mid - 1;
ans = mid;
} else {
l = mid + 1;
}
}
cout << ans << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
算法分析
时间复杂度
- 二分搜索:O(log(max_D)),其中 max_D = 10^9,约为 30 次迭代
- 每次 BFS:O(M×N),其中 M 和 N 最大为 500,即 250,000 个格子
- 总复杂度:O(log(max_D) × M × N) ≈ 30 × 250,000 = 7,500,000 次操作
空间复杂度
- O(M×N) 用于存储高度信息、重要点标记和访问状态
关键点说明
- 二分搜索的应用:通过二分法在较大的 D 值范围内快速定位最小满足条件的值
- BFS 连通性检查:对于每个候选 D 值,使用 BFS 检查从任意一个重要点出发能否到达所有其他重要点
- 起点选择:选择任意一个重要点作为 BFS 起点,因为如果所有重要点连通,从任何一个出发都能到达所有其他重要点
- 高度差限制:在 BFS 扩展时,只考虑高度差不超过当前 D 值的相邻格子
问题分解与解决步骤
步骤 1:理解问题本质
- 将滑雪场建模为网格图
- 将连通性问题转化为图论问题
- 确定约束条件:相邻格子、高度差限制
步骤 2:确定算法框架
- 识别这是一个最优化问题(最小化 D)
- 选择二分搜索作为主要算法框架
- 确定验证函数(BFS)来检查给定 D 是否满足条件
步骤 3:实现验证函数
- 使用 BFS 进行连通性检查
- 正确处理边界条件和访问标记
- 高效统计重要点访问情况
步骤 4:优化与调试
- 选择合适的起点以减少不必要的计算
- 处理极端情况(如只有一个重要点)
- 确保算法在数据范围内高效运行
总结
本题通过将问题转换为连通性检查,并利用二分搜索优化查找过程,有效地解决了在大数据范围内的最小 D 值查找问题。这种"二分答案 + 验证"的思路在解决最优化问题时非常实用,特别是当直接求解困难但验证相对容易时。
关键思路分解:
- 问题建模:将物理问题转化为图论问题
- 算法选择:二分搜索处理最优化,BFS 处理连通性验证
- 实现细节:正确处理边界、标记和计数
- 性能优化:利用问题特性减少不必要的计算
这种分解思维有助于系统地解决复杂问题,将大问题拆解为可管理的小问题,并选择合适的算法和数据结构来解决每个子问题。
题解:史迪仔的晚餐计划
题目分析
史迪仔需要在接下来的 T 天内尽可能多地吃到晚餐。他只能在有菜的情况下才能做饭,而菜只能从超市进货时购买。超市会在特定的天数进货,每次进货时史迪仔可以购买固定数量的菜。我们需要计算在 T 天内史迪仔最多能吃到多少顿晚餐。
解题思路
问题转换
这个问题可以看作是一个时间序列上的资源分配问题:
- 资源(菜)在特定时间点(进货日)增加
- 资源每天消耗1单位(做饭)
- 目标是最大化消耗的资源总量
关键观察
- 我们不需要模拟每一天,而是可以按进货时间段来处理
- 在每个进货时间段内,能吃的晚餐数受两个因素限制:
- 当前拥有的菜量
- 时间段的天数
- 剩余的菜可以累积到下一个时间段
算法思路
- 按进货时间顺序处理每个进货事件
- 对于每个进货事件:
- 将进货量加入当前菜量
- 计算从当前进货日到下一个进货日(或最后一天)之间的天数
- 在这个时间段内,能吃掉的菜量是 min(当前菜量, 时间段天数)
- 更新总晚餐数和剩余菜量
算法实现
#include <bits/stdc++.h>
using namespace std;
long long n, T;
typedef long long ll;
struct node {
ll v; // 能买的菜量
ll d; // 进货的天数
} a[100003];
int main() {
freopen("Dinner.in", "r", stdin);
freopen("Dinner.out", "w", stdout);
cin >> n >> T;
for (int i = 1; i <= n; i++) {
cin >> a[i].d >> a[i].v; // 输入进货天数和能买的菜量
}
ll ans = 0; // 吃上饱饭的日子
ll res = 0; // 家中的剩余菜量
// 设置虚拟的最后一次进货,方便统一处理
a[n + 1].d = T + 1;
a[n + 1].v = 0;
for (int i = 1; i <= n; i++) {
res += a[i].v; // 将当前天新买的菜量加入总和
// 计算从当前进货日到下一个进货日之间的天数
ll days = a[i + 1].d - a[i].d;
// 在这个时间段内能吃的晚餐数受菜量和天数的限制
ll canEat = min(res, days);
ans += canEat; // 更新总晚餐数
res -= canEat; // 更新剩余菜量
}
cout << ans << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
算法分析
时间复杂度
- 我们只需要遍历 n 次进货事件,时间复杂度为 O(n)
- 对于 n ≤ 10^5 的数据范围,这个复杂度是完全可行的
空间复杂度
- 我们使用了一个大小为 O(n) 的数组来存储进货信息
- 总空间复杂度为 O(n)
关键点说明
-
虚拟的最后一次进货:通过设置一个虚拟的进货点(T+1天),我们可以统一处理所有时间段,包括最后一次进货到第 T 天的时间段。
-
时间段处理:对于每个进货时间段 [d_i, d_{i+1}),我们能吃的晚餐数受两个因素限制:
- 当前拥有的菜量 res
- 时间段长度 (d_{i+1} - d_i)
-
菜量累积:当前时间段未用完的菜会累积到下一个时间段,这通过更新 res 变量实现。
问题分解与解决步骤
步骤 1:理解问题结构
- 识别出这是一个资源在时间上分配的问题
- 注意到资源(菜)只在特定时间点增加
- 资源每天稳定消耗(做饭)
步骤 2:确定处理单元
- 不需要按天处理,可以按进货时间段处理
- 每个时间段内,消耗的菜量受时间段长度和当前菜量的双重限制
步骤 3:设计算法框架
- 按进货顺序处理每个事件
- 对于每个进货事件,计算到下一个进货事件的时间段
- 确定该时间段内能消耗的菜量
步骤 4:处理边界情况
- 最后一次进货后的时间段需要特殊处理
- 通过添加虚拟进货点简化代码逻辑
总结
本题通过将问题分解为多个时间段,并在每个时间段内计算能消耗的最大菜量,有效地解决了史迪仔的晚餐计划问题。这种"分段处理"的思路在解决时间序列上的资源分配问题时非常有效。
关键思路分解:
- 问题建模:将连续的时间离散化为多个时间段
- 资源跟踪:跟踪每个时间段的起始菜量和结束菜量
- 限制处理:同时考虑时间限制和资源限制
- 边界处理:通过虚拟点简化边界条件处理
这种思路可以推广到类似的资源分配问题,特别是当资源在特定时间点增加,而消耗是连续的情况。
题解:越野比赛场地调整
题目分析
阿北需要将当前的地面高度数组 t 调整到希望的高度数组 p。每次操作可以将一段连续位置的高度统一增加或减少1个单位。我们需要找出最小的操作次数。
解题思路
问题转换
将问题转化为差分数组的处理:
- 计算每个位置的差值
d[i] = p[i] - t[i] - 构建差分数组
diff,其中:diff[1] = d[1]diff[i] = d[i] - d[i-1](对于i = 2 到 n)diff[n+1] = -d[n]
关键观察
每次操作对应差分数组中的两个变化:
- 对区间
[l, r]加1:diff[l] += 1,diff[r+1] -= 1 - 对区间
[l, r]减1:diff[l] -= 1,diff[r+1] += 1
算法思路
- 计算每个位置的高度差值
- 构建差分数组
- 计算差分数组中所有正数的和,即为最小操作次数
算法实现
#include <bits/stdc++.h>
using namespace std;
int n;
long long p[300010], t[300010];
long long d[300010]; // 差值数组
long long diff[300010]; // 差分数组
int main() {
freopen("arrange.in", "r", stdin);
freopen("arrange.out", "w", stdout);
cin >> n;
// 读入希望高度
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
// 读入当前高度
for (int i = 1; i <= n; i++) {
cin >> t[i];
}
// 计算差值
for (int i = 1; i <= n; i++) {
d[i] = p[i] - t[i];
}
// 构建差分数组
diff[1] = d[1];
for (int i = 2; i <= n; i++) {
diff[i] = d[i] - d[i-1];
}
diff[n+1] = -d[n]; // 边界处理
// 计算正数和
long long ans = 0;
for (int i = 1; i <= n+1; i++) {
if (diff[i] > 0) {
ans += diff[i];
}
}
cout << ans << endl;
fclose(stdin);
fclose(stdout);
return 0;
}
算法分析
时间复杂度
- 计算差值:O(n)
- 构建差分数组:O(n)
- 计算正数和:O(n)
- 总时间复杂度:O(n)
空间复杂度
- 需要存储差值数组和差分数组:O(n)
关键点说明
-
差分数组的构建:
- 通过差分将区间操作转化为点操作
- 简化了问题的复杂度
-
边界处理:
- 添加
diff[n+1] = -d[n]确保差分数组的和为0 - 这是算法正确性的关键
- 添加
-
正数和的计算:
- 差分数组中的正数和直接对应最小操作次数
- 因为每次操作可以抵消一个正数和一个负数
问题分解与解决步骤
步骤1:理解操作的本质
- 每次操作影响连续区间
- 寻找将当前高度调整到希望高度的最小操作次数
步骤2:数学建模
- 将问题转化为差值数组的处理
- 使用差分技术简化区间操作
步骤3:算法设计
- 发现差分数组中正数和与操作次数的关系
- 设计线性时间算法
步骤4:实现与优化
- 直接计算正数和,避免复杂的过程
- 处理边界条件确保正确性
总结
本题通过巧妙的差分转换,将复杂的区间操作问题简化为简单的数组处理。差分数组中的正数和直接给出了最小操作次数,使得我们能够在O(n)时间内解决问题。
核心思路:
- 将区间操作转化为差分数组的点操作
- 利用差分数组的性质简化计算
- 正数和直接对应最小操作次数
这种差分技巧在解决区间修改问题时非常有效,是算法竞赛中的常用技术。
这里空空如也






有帮助,赞一个