题解:洛谷 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

【核心思想】

  1. 问题分析:Bessie 在一条长为 \(N\) 的数轴上弹跳,起始位置 \(S\),初始能量为 \(1\),方向向右。每个位置要么是跳板\(q_i=0\),数值 \(v_i\)),要么是炮击目标\(q_i=1\),阈值 \(v_i\))。Bessie 每次以当前能量 \(k\) 向前跳 \(k\) 格,若落在跳板上,则能量增加 \(v_i\)反转方向;若落在炮击目标上,若当前能量 \(\ge v_i\) 则击破它(击破后仍可停留,但不再改变状态)。求她能击破的炮击目标总数。由于能量和方向会不断变化,可能进入无限循环或最终离开数轴。核心是模拟弹跳过程并检测循环

  2. 算法选择

    • 模拟 + 循环检测:按规则逐步模拟 Bessie 的跳跃。关键优化是检测跳板状态的重复——因为只有跳板会改变能量和方向,而炮击目标不会改变状态。若 Bessie 以相同的能量和方向再次到达同一个跳板,则后续轨迹将完全重复,进入无限循环,此时应停止模拟(不会再击破新的目标)。代码中用 vislastpower 记录每个跳板是否被访问过以及当时的力量值,若重复则跳出。
    • 炮击目标处理:着陆在炮击目标上时,若能量足够则击破(计数并标记为已破,将阈值设为大数),但无论是否击破,Bessie 的能量和方向均不变,继续下一次跳跃。
  3. 关键步骤

    • 读入:读取 \(N, S\),以及每个位置的类型 type 和数值 val
    • 初始化:Bessie 状态 pos = Spower = 1dir = 1(向右)。cnt = 0(击破数),tot 为炮击目标总数(可提前统计,用于提前终止)。
    • 模拟循环:当 pos\([1, N]\) 内且 cnt < tot 时:
      • 若当前位置是炮击目标:
        • power >= val 且尚未击破(val 未被设为大数),则 cnt++,并将 val 设为 INF(标记已破)。
        • 位置、能量、方向不变。
      • 若当前位置是跳板:
        • 循环检测:若该跳板已被访问过且记录的 lastpower == power,则说明进入重复状态,跳出循环。
        • 否则,记录 vis = 1lastpower = power
        • 方向反转:dir = -dir
        • 能量增加:power += val
      • 计算下一步位置:pos += dir * power
    • 输出cnt
  4. 时间/空间复杂度

    • 时间复杂度:\(O(N + \text{跳跃次数})\)。由于每个跳板状态(方向+能量)最多被有效访问一次(重复即终止),跳跃次数受限于状态数,实际可行。
    • 空间复杂度:\(O(N)\),存储每个位置的类型、数值和跳板的访问记录。
  5. 模拟 + 循环检测的核心思想

    • 状态关键点: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
posted @ 2026-08-28 15:53  团爸讲算法  阅读(1)  评论(0)    收藏  举报