荒岛野人(信息学奥赛一本通- P1637)(洛谷-P2421)

【题目描述】

原题来自:NOI 2002

克里特岛以野人群居而著称。岛上有排列成环行的 M 个山洞。这些山洞顺时针编号为 1,2,⋯,M。

岛上住着 N 个野人,一开始依次住在山洞 C1,C2,⋯,CN 中,以后每年,第 i 个野人会沿顺时针向前走 Pi个洞住下来。每个野人 i 有一个寿命值 Li ,即生存的年数。下面四幅图描述了一个有 6 个山洞,住有三个野人的岛上前四年的情况。三个野人初始的洞穴编号依次为 1,2,3;每年要走过的洞穴数依次为 3,7,2;寿命值依次为 4,3,1。

奇怪的是,虽然野人有很多,但没有任何两个野人在有生之年处在同一个山洞中,使得小岛一直保持和平与宁静,这让科学家们很是惊奇。他们想知道,至少有多少个山洞,才能维持岛上的和平呢?

【输入】

第一行为一个整数 N,即野人的数目;

第二行到第 N+1 每行为三个整数 Ci,Pi,Li ,表示每个野人所住的初始洞穴编号,每年走过的洞穴数及寿命值。

【输出】

仅包含一个数 M,即最少可能的山洞数。输入数据保证有解,且 M 不大于 10^6。

【输入样例】

复制代码
3
1 3 4
2 7 3
3 2 1

【输出样例】

复制代码
6

【提示】

样例说明

该样例对应于题目描述中的例子。

数据范围与提示:

对于全部数据,1≤N≤15,1≤Ci,Pi≤100,0≤Li≤10^6。

1. 题意转换

剥开"荒岛"、"野人"、"寿命"这层奇妙的包装,这道题本质上是在求:寻找一个最小的模数 M(M≥max(Ci​)),使得对于任意两个野人 i 和 j,线性同余方程 (Pi​−Pj​)*x≡Cj​−Ci ​(mod M) 的最小非负整数解 x 都满足 x>min(Li​,Lj​)(即在有生之年无解)。

这道题是"相遇问题"在模运算体系下的经典建模,核心模型为 从小到大枚举 + 扩展欧几里得算法。

2. 思考过程与解题思路

  • 第一直觉的暴力解法 面对"每年走 Pi​ 步"的设定,初学者最容易想到的就是开一个数组模拟岛上的山洞,用一个外层大循环遍历年份 x,逐年模拟每个野人的位置并检查是否冲突。 为什么会超时? 野人的最大寿命 Li​≤10^6,山洞数 M 最多也会到10^6。如果一年年去模拟,不仅每次都要判定 O(N^2) 对野人,还会因为无法确定 M 的上限陷入死循环。

  • 推导正解的破局之路 遇到环形相遇问题,我们要形成条件反射------用同余方程代替步步模拟。 假设野人 i 和野人 j 在第 x 年相遇,说明他们此时所处的洞穴编号在模 M 意义下是相等的:

    Ci​+x⋅Pi​≡Cj​+x⋅Pj ​(mod M)

    移项化简,把未知数 x 放在左边,已知常数放在右边:

    (Pi​−Pj​)*x≡Cj​−Ci​ (mod M)

    这瞬间就变成了一个标准的二元一次不定方程:

    (Pi​−Pj​)*x+M⋅y=Cj​−Ci​

    既然能够通过扩展欧几里得算法瞬间算出两人相遇的年份 x,那我们还需要枚举年份吗?完全不需要! 观察一下数据范围,野人数量 N≤15 是一个极其诡异且明显的暗示。这意味着野人两两配对的总数最多只有 15×14​/2=105 对! 于是,思路豁然开朗:我们只要从小到大枚举山洞总数 M ,对于每个 M,利用 exgcd 校验这 105 对野人会不会在有生之年相撞。一旦发现有相撞的,立刻毙掉当前 M 并 M++。第一个让所有野人都相安无事的 M,就是我们的答案。

3. 算法设计与样例推演

  • 算法执行流程与核心校验逻辑:

    1. 确定起点:山洞总数 M 绝对不能小于岛上的野人数 N,更不能小于任何一个野人初始居住的洞穴编号(物理限制)。所以枚举起点 num=max(N,max(Ci​))。

    2. 双重循环枚举野人对 (i,j),令 a=Pi​−Pj​,b=num,c=Cj​−Ci​。

    3. 为了防止负数干扰 exgcd 的计算,若 a<0,只需等式两边同乘 −1,即 a=−a, c=−c。

    4. 呼叫 exgcd(a,b,x,y) 得到最大公约数 d。如果c能被d整除,说明方程有解,两人有可能相遇。

    5. 将基础解通过缩放和黄金转正句型转化为最小非负相遇年份 x0​。

    6. 终极审判:如果 x0​≤min(Li​,Lj​),说明两人在活着的时候撞车了,当前山洞数 num 失败,跳出循环,继续检验 num+1。

  • 极简数据手玩推演(带入题目样例): 假设校验到了样例中的山洞数 num=6。 取出野人 2 和野人 3:

    • 野人 2:C2​=2,P2​=7,L2​=3

    • 野人 3:C3​=3,P3​=2,L3​=1

    1. 构建方程:(P2​−P3​)*x+6y=C3​−C2​⟹(7−2)x+6y=3−2⟹5x+6y=1。

    2. 调用 exgcd:解得基础解 x0​=−1,d=gcd(5,6)=1。

    3. 计算放大倍数:times=1/1=1。

    4. 计算模数(周期):mod=6/1=6。

    5. 转正最小年份:x0​=( (−1×1) (mod 6)+6) (mod 6)=5。 两人如果要相遇,最早是在第 5 年。

    6. 寿命校验:野人 2 活 3 年,野人 3 活 1 年,min(3,1)=1。相遇年份 5>1,因此两人在有生之年完美错开,不会冲突!

4. 时空复杂度分析

  • 时间复杂度 :O(Mans​⋅N^2⋅log(maxP))。 最坏情况下我们要枚举约 10^6 次 M。每次校验需要 O(N^2) 次,单次 exgcd 的复杂度是 O(logP)。表面上看 10^6×105×log2​(100)≈7×10^8 可能会卡常。但实际上这是一个极度宽松的上限 。因为绝大多数不合法的 M 在前几次野人配对校验中就会立刻 break,常数极小,实际运行时间都在 10ms 级别,对于 1s 的时限极其安逸。

  • 空间复杂度:O(N)。只需几个长度为 20 的数组来存储 15 个野人的信息,空间占用几乎为 0,不可能MLE。

5. 坑点与易错总结

  1. 物理界限的盲区:很多同学知道 M 至少从 N 开始枚举,却忘了如果 1 号野人一开始就住在 10 号洞,山洞总数绝不可能少于10。因此 M 的枚举下界必须是 max(Ci​)。

  2. 符号颠倒的负数陷阱 :在推导 (Pi​−Pj​)*x 时,Pi​−Pj​ 完全可能是负数。直接送进同余系统中,C++ 的 % 运算对负数处理极易出错。巧妙的解法是如果 Pi​−Pj​<0,让等号左右两端同时取反,强行转正。

  3. 周期必须除以最大公约数 :在求最终年份时,扩欧求出的局部周期必须是 num/gcd,千万不能直接 %num。

  4. 乘法防爆(习惯培养) :虽然本题数据范围小(Pi​,Ci​≤100),int 乘法 x0 * times 不会溢出,但在正规比赛中,凡是扩欧的放大步数计算,建议肌肉记忆般地使用 long long 或 __int128,防止"死在数据类型上"。

6. 完整代码

cpp 复制代码
//数据量n小于等于15 非常小 所以可以直接暴力求解
//直接从最小可能山洞数开始枚举(最小山洞数肯定不能小于野人数)
//然后每次枚举的山洞数num 
//如果两个野人要相遇 代表
//c[i]+x0*p[i]≡c[j]+x[0]*p[j] (mod num)
//移项 变成x0*(p[i]-p[j])≡c[j]-c[i] (mod num)
//再变成 x0*(p[i]-p[j])+num*y0=c[j]-c[i]
//扩欧求解x0
//x0代表年数,如果算出来的x0比l[i]和l[j](野人寿命)都小 
//则代表第i个野人和第j个野人有生之年会住进同一个山洞
//就直接continue 然后增加山洞数
//直到所有野人有生之年都不会相遇 这个山洞数就OK
#include <iostream>
using namespace std;
int n;
int c[20];//c[i]代表第i个野人所住的初始洞穴编号
int p[20];//p[i]代表第i个野人每年走过的洞穴数
int l[20];//l[i]代表第i个野人的寿命
int num;//山洞数量 从最小山洞数n开始(最小山洞数肯定不能小于野人数)

//扩欧
int exgcd(int a,int b,int &x,int &y){
    if(b==0){
        x=1;
        y=0;
        return a;
    }
    int d=exgcd(b,a%b,x,y);
    int tmp=x;
    x=y;
    y=tmp-(a/b)*y;
    return d;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin>>n;
    num=n;//最小山洞数肯定不能小于野人数
    //存储每个野人的信息
    for(int i=1;i<=n;i++){
        cin>>c[i]>>p[i]>>l[i];
        num=max(n,c[i]);//山洞数目不能小于最开始野人居住的山洞位置
    }
    //遍历洞穴数量 从最小山洞数n开始(最小山洞数肯定不能小于野人数)
    while(1){
        bool flag=1;//标记 当前山洞数量能否满足要求
        //枚举所有野人两两比对
        for(int i=1;i<n;i++){
            for(int j=i+1;j<=n;j++){
                int x0;
                int y0;
                int a=p[i]-p[j];
                int weishu=c[j]-c[i];
                //确保a不能为负数
                //如果a<0 等式左右两边同时乘以-1 保持平衡
                if(a<0){
                    a=0-a;
                    weishu=0-weishu;
                }
                int d=exgcd(a,num,x0,y0);
                //根据裴蜀定理 如果有解
                if((weishu%d)==0){
                    //算出放大倍数 实际常数是基础常数gcd的多少倍
                    int times=weishu/d;
                    //算出新的模数(周期) 方程同除以d后 模数也要随之缩小
                    int mod=num/d;
                    //算出非负最小寿命 即两人相遇的最小非负整数年
                    x0=((x0*times)%mod+mod)%mod;
                    //如果第i个野人和第j个野人寿命都比x0大
                    //代表有生之年会住进一个山洞
                    //代表肯定不行 把标记改为0
                    if(l[i]>=x0&&l[j]>=x0){
                        flag=0;
                        break;
                    }
                }
            }
            //如果标记已经为0 直接num++ 退出本轮循环
            if(flag==0){
                num++;
                break;
            }
        }
        //如果遍历完所有野人 flag为1代表当前num满足条件
        if(flag==1){
            cout<<num;
            return 0;
        }
    }
    return 0;
}
相关推荐
朝朝辞暮i1 小时前
C++ 第 40 章:ROS2 Publisher + Timer + Subscriber + Callback + Executor 完整闭环
开发语言·c++·算法·ros2
400分1 小时前
具身智能系列之从零到「机械臂会放试管」:π0.5 VLA 真机部署完全指南【第一节】
算法
小宋10211 小时前
一套服务托管多个 LoRA:适配器加载、租户隔离与热切换实战
大数据·人工智能·算法
Rosanci1 小时前
Codex 下载与本地部署实战:从安装到运行全流程指南
开发语言·前端·算法·chatgpt·codex
我爱工作&工作love我2 小时前
P14358 [CSP-J 2025] 座位
算法
旖旎夜光2 小时前
LeetCode 1576: 替换所有的问号(模拟) —— 题解
c++·学习·算法·leetcode·力控
朝朝辞暮i2 小时前
C++ 第 38 课:线程、SingleThreadedExecutor、MultiThreadedExecutor、Callback Group
开发语言·c++·算法·ros2
(Charon)2 小时前
【C++面试】互斥锁与读写锁:使用场景、实现原理与写饥饿问题
c++·算法
longlongzihan2 小时前
回溯法详解:LeetCode 46. 全排列
算法·leetcode·回溯