题解:洛谷 P10132 Cannonball
【题目来源】
洛谷:P10132 [USACO24JAN] Cannonball B - 洛谷
【题目描述】
Bessie 已经精通了变成炮弹并沿着长度为 \(N\)(\(1\le N\le 10^5\))的数轴弹跳的艺术,数轴上的位置从左到右编号为 \(1,2,\cdots,N\)。她从某个整数位置 \(S\)(\(1\le S\le N\))开始,以 \(1\) 的起始能量向右弹跳。如果 Bessie 的能量为 \(k\),则她将弹跳至距当前位置向前距离 \(k\) 处进行下一次弹跳。
从 \(1\) 到 \(N\) 的每个整数位置上均有炮击目标或跳板。每个炮击目标和跳板都有一个在 \(0\) 到 \(N\) 范围内的整数值。一个数值为 \(v\) 的跳板会使 Bessie 的能量增加 \(v\) 并反转她的方向。一个数值为 \(v\) 的炮击目标会当着陆时能量不小于 \(v\) 时被击破。着陆在炮击目标上不会改变 Bessie 的能量和方向。被击破的炮击目标将保持击破状态,Bessie 依然可以该炮击目标上弹跳,同样不会改变能量和方向。
如果 Bessie 弹跳无限长的时间或直到她离开数轴,她会击破多少个炮击目标?
如果 Bessie 开始时位于一个她可以击破的炮击目标,她会立刻这样做。类似地,如果 Bessie 开始时位于一个跳板,跳板的效果将在她第一次跳跃之前生效。
【输入】
输入的第一行包含 \(N\) 和 \(S\),其中 \(N\) 为数轴的长度,\(S\) 为 Bessie 的起始位置。
以下 \(N\) 行描述了每一个位置。其中第 \(i\) 行包含整数 \(q_i\) 和 \(v_i\),如果位置 \(i\) 上有一个跳板则 \(q_i=0\),位置 \(i\) 上有一个炮击目标则 \(q_i=1\),\(v_i\) 是位置 \(i\) 上的跳板或炮击目标的数值。
【输出】
输出一个整数,为将被击破的炮击目标数量。
【输入样例】
5 2
0 1
1 1
1 2
0 1
1 1
【输出样例】
1
【核心思想】
-
问题分析:Bessie 在一条长为 \(N\) 的数轴上弹跳,起始位置 \(S\),初始能量为 \(1\),方向向右。每个位置要么是跳板(\(q_i=0\),数值 \(v_i\)),要么是炮击目标(\(q_i=1\),阈值 \(v_i\))。Bessie 每次以当前能量 \(k\) 向前跳 \(k\) 格,若落在跳板上,则能量增加 \(v_i\) 并反转方向;若落在炮击目标上,若当前能量 \(\ge v_i\) 则击破它(击破后仍可停留,但不再改变状态)。求她能击破的炮击目标总数。由于能量和方向会不断变化,可能进入无限循环或最终离开数轴。核心是模拟弹跳过程并检测循环。
-
算法选择:
- 模拟 + 循环检测:按规则逐步模拟 Bessie 的跳跃。关键优化是检测跳板状态的重复——因为只有跳板会改变能量和方向,而炮击目标不会改变状态。若 Bessie 以相同的能量和方向再次到达同一个跳板,则后续轨迹将完全重复,进入无限循环,此时应停止模拟(不会再击破新的目标)。代码中用
vis和lastpower记录每个跳板是否被访问过以及当时的力量值,若重复则跳出。 - 炮击目标处理:着陆在炮击目标上时,若能量足够则击破(计数并标记为已破,将阈值设为大数),但无论是否击破,Bessie 的能量和方向均不变,继续下一次跳跃。
- 模拟 + 循环检测:按规则逐步模拟 Bessie 的跳跃。关键优化是检测跳板状态的重复——因为只有跳板会改变能量和方向,而炮击目标不会改变状态。若 Bessie 以相同的能量和方向再次到达同一个跳板,则后续轨迹将完全重复,进入无限循环,此时应停止模拟(不会再击破新的目标)。代码中用
-
关键步骤:
- 读入:读取 \(N, S\),以及每个位置的类型
type和数值val。 - 初始化:Bessie 状态
pos = S,power = 1,dir = 1(向右)。cnt = 0(击破数),tot为炮击目标总数(可提前统计,用于提前终止)。 - 模拟循环:当
pos在 \([1, N]\) 内且cnt < tot时:- 若当前位置是炮击目标:
- 若
power >= val且尚未击破(val未被设为大数),则cnt++,并将val设为INF(标记已破)。 - 位置、能量、方向不变。
- 若
- 若当前位置是跳板:
- 循环检测:若该跳板已被访问过且记录的
lastpower == power,则说明进入重复状态,跳出循环。 - 否则,记录
vis = 1,lastpower = power。 - 方向反转:
dir = -dir。 - 能量增加:
power += val。
- 循环检测:若该跳板已被访问过且记录的
- 计算下一步位置:
pos += dir * power。
- 若当前位置是炮击目标:
- 输出:
cnt。
- 读入:读取 \(N, S\),以及每个位置的类型
-
时间/空间复杂度:
- 时间复杂度:\(O(N + \text{跳跃次数})\)。由于每个跳板状态(方向+能量)最多被有效访问一次(重复即终止),跳跃次数受限于状态数,实际可行。
- 空间复杂度:\(O(N)\),存储每个位置的类型、数值和跳板的访问记录。
-
模拟 + 循环检测的核心思想:
- 状态关键点:Bessie 的状态变化只发生在跳板上,炮击目标不会改变状态。因此,跳板是决定轨迹是否重复的关键节点。
- 重复判定:如果 Bessie 以完全相同的能量和方向再次到达同一个跳板,那么之后的所有跳跃都将与之前相同,进入无限循环,且不会再击破新的炮击目标。及时检测并退出可避免无限循环。
- 炮击目标只影响计数:炮击目标被击破后,只是标记为“已破”,不影响 Bessie 的物理状态,所以无需特殊处理(除了防止重复计数)。
- 适用场景:适用于“状态转移由特定节点(跳板)改变,其他节点不影响状态”的模拟问题。核心是通过记忆关键节点的状态组合来检测环。
【算法标签】
普及- #模拟
【代码详解】
#include <bits/stdc++.h>
using namespace std;
int n; // 房间数量
int s; // 起始房间编号
int cnt; // 已击败的敌人数量
int tot; // 敌人总数
int step; // 移动步数(用于调试或限制循环次数)
// 房间结构体
struct node
{
int type; // 房间类型:0=弹簧,1=敌人
int weight; // 房间数值:弹簧的弹力值/敌人的生命值
int vis; // 标记弹簧是否被访问过
int lastpower; // 上次访问弹簧时的力量值
} p[100005];
// 人物结构体
struct Person
{
int dir; // 移动方向:1=向右,-1=向左
int power; // 当前力量值
int pos; // 当前位置(房间编号)
};
int main()
{
// 输入房间数量和起始位置
cin >> n >> s;
// 输入每个房间的信息
for (int i = 1; i <= n; i++)
{
cin >> p[i].type >> p[i].weight;
if (p[i].type == 1)
{
tot++; // 统计敌人总数
}
}
// 初始化贝西的状态
Person bessie = {1, 1, s}; // 初始方向向右,力量为1,位置为s
int flag = 0; // 标志变量(未使用)
// 主循环:贝西在房间之间移动
while (bessie.pos >= 1 && bessie.pos <= n)
{
// 调试用的步数限制(注释掉的代码)
// if (step > n - 1) break;
// 如果已击败所有敌人,退出循环
if (tot == cnt)
{
// flag = 1;
break;
}
// 调试输出(注释掉的代码)
// cout << "pos " << bessie.pos << endl;
// cout << "power " << bessie.power << endl;
// 处理敌人房间(type=1)
if (p[bessie.pos].type == 1)
{
// 调试输出(注释掉的代码)
// cout << "1 power " << bessie.power << endl;
// 如果力量足够击败敌人
if (bessie.power >= p[bessie.pos].weight)
{
cnt++; // 击败敌人计数加1
p[bessie.pos].weight = 1e9; // 将敌人生命值设为极大值(标记为已击败)
}
}
// 处理弹簧房间(type=0)
if (p[bessie.pos].type == 0)
{
// 调试输出(注释掉的代码)
// cout << "0 power " << bessie.power << endl;
// 检测循环:如果弹簧已被访问过且力量值相同,说明进入死循环
if (p[bessie.pos].vis == 1 && bessie.power == p[bessie.pos].lastpower)
{
break; // 退出循环避免无限循环
}
// 改变移动方向
bessie.dir = bessie.dir * -1;
// 标记弹簧已被访问,记录当前力量值
p[bessie.pos].vis = 1;
p[bessie.pos].lastpower = bessie.power;
// 增加力量值(弹簧效果)
bessie.power = bessie.power + p[bessie.pos].weight;
}
// 移动到下一个位置
bessie.pos = bessie.pos + (bessie.dir * bessie.power);
step++; // 步数计数(用于调试或循环控制)
}
// 输出击败的敌人数量
cout << cnt << endl;
return 0;
}
【运行结果】
5 2
0 1
1 1
1 2
0 1
1 1
浙公网安备 33010602011771号