2 条题解

  • 0
    @ 2026-3-26 11:06:16

    你好!我是金牌教练。在处理这种“完成任务所需的最少时间”问题时,理解从“模拟过程”到“验证性质”的思维转变是进阶 NOI 的必经之路。

    下面我为你演示这道题的代码演进过程。


    版本 1:暴力枚举时间 (Linear Search)

    思路: 最直观的想法:既然求最少时间,我们就从 T=1,2,3,T=1, 2, 3, \dots 开始往后试。对于每一个 TT,计算所有公交车完成的旅途总数,一旦总数 totalTrips\ge totalTrips,当前的 TT 就是答案。

    复杂度分析:

    • 时间复杂度O(AnsN)O(Ans \cdot N)。其中 AnsAns 是最终答案,最大可能达到 101410^{14}
    • 空间复杂度O(N)O(N)
    • 评价:只能通过 totalTripstotalTripstime[i]time[i] 极小的测试点(约 10% 的分数)。
    /*
     * 暴力枚举法:从 1 分钟开始尝试
     * 适用场景:n, totalTrips 极小的情况
     */
    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    typedef long long ll;
    
    int main() {
        int n;
        ll totalTrips;
        if (!(cin >> n >> totalTrips)) return 0;
        vector<int> t(n);
        for (int i = 0; i < n; i++) cin >> t[i];
    
        // 从 1 开始暴力枚举时间
        for (ll time_spent = 1; ; time_spent++) {
            ll current_trips = 0;
            for (int i = 0; i < n; i++) {
                current_trips += (time_spent / t[i]);
            }
            if (current_trips >= totalTrips) {
                cout << time_spent << endl;
                break;
            }
        }
        return 0;
    }
    

    版本 2:优先队列模拟 (Min-Heap Simulation)

    思路: 我们可以看作是“哪个公交车先跑完下一趟”。用一个小根堆维护 {下一次完成时间, 车的单次耗时}。每次从堆顶取出完成时间最早的车,旅途数 +1,然后将这辆车完成下一趟的时间再次放入堆中。

    复杂度分析:

    • 时间复杂度O(totalTripslogN)O(totalTrips \cdot \log N)
    • 空间复杂度O(N)O(N)
    • 评价:当 totalTrips=107totalTrips = 10^7 时,运算量约 1.71081.7 \cdot 10^8,在 1s 时限内非常危险,可能拿到 40-60% 的分数。
    /*
     * 优先队列模拟:每次找最早完成下一趟的车
     * 易错点:这里记录的是“绝对完成时刻”,需要用 long long
     */
    #include <iostream>
    #include <vector>
    #include <queue>
    
    using namespace std;
    
    typedef long long ll;
    
    struct Bus {
        ll next_finish; // 该公交车下一次跑完的绝对时刻
        int cost;       // 跑一趟需要的固定时间
        bool operator>(const Bus& other) const {
            return next_finish > other.next_finish;
        }
    };
    
    int main() {
        int n;
        ll total;
        cin >> n >> total;
        priority_queue<Bus, vector<Bus>, greater<Bus>> pq;
        for (int i = 0; i < n; i++) {
            int c; cin >> c;
            pq.push({(ll)c, c});
        }
    
        ll count = 0;
        ll last_time = 0;
        while (count < total) {
            Bus top = pq.top();
            pq.pop();
            count++;
            last_time = top.next_finish;
            // 这辆车跑完后,立刻开始下一趟
            pq.push({top.next_finish + top.cost, top.cost});
        }
        cout << last_time << endl;
        return 0;
    }
    

    版本 3:最优解 - 二分答案 (Binary Search on Answer)

    思路: 如果我们知道时间 TT,可以很容易算出一共能跑多少趟。且时间 TT 越多,趟数越多,具有单调性。因此我们直接二分查找这个 TT

    复杂度分析:

    • 时间复杂度O(Nlog(MinTimetotalTrips))O(N \cdot \log(MinTime \cdot totalTrips))
    • 空间复杂度O(N)O(N)
    • 评价:运算量约 105474.710610^5 \cdot 47 \approx 4.7 \cdot 10^6,NOI 满分解法。
    /*
     * 二分答案:通过验证时间 T 是否可行来逼近最优解
     * 关键点 1:确定右边界 High = 最小时间 * 总趟数 (最坏情况)
     * 关键点 2:Check 函数中累加 Sum 时,若 Sum >= target 立即停止,防止 ll 溢出
     */
    #include <iostream>
    #include <vector>
    #include <algorithm>
    
    using namespace std;
    
    typedef long long ll;
    
    int n;
    ll totalTrips;
    int bus_time[100005];
    
    bool check(ll mid) {
        ll sum = 0;
        for (int i = 0; i < n; i++) {
            sum += (mid / bus_time[i]);
            // 关键优化:已经达到目标就直接返回,防止累加过大导致 long long 溢出
            if (sum >= totalTrips) return true;
        }
        return sum >= totalTrips;
    }
    
    int main() {
        // 提速:大数据量建议开启
        ios::sync_with_stdio(false);
        cin.tie(0);
    
        if (!(cin >> n >> totalTrips)) return 0;
        
        int min_val = 1e9;
        for (int i = 0; i < n; i++) {
            cin >> bus_time[i];
            if (bus_time[i] < min_val) min_val = bus_time[i];
        }
    
        ll low = 1;
        // 右边界:用最快的车完成所有任务所需的时间,这是一个安全的上界
        ll high = (ll)min_val * totalTrips; 
        ll ans = high;
    
        while (low <= high) {
            ll mid = low + (high - low) / 2;
            if (check(mid)) {
                ans = mid;      // 时间足够,尝试更短的时间
                high = mid - 1;
            } else {
                low = mid + 1;  // 时间不够,必须延长
            }
        }
    
        cout << ans << endl;
        return 0;
    }
    

    时间复杂度优化建议

    1. 数据溢出 (NOI 重灾区)

      • 1e7 * 1e7 = 1e14。这已经超过了 int21092 \cdot 10^9
      • check 函数中,如果不加 if (sum >= totalTrips) return true;,当 n=105,mid=1014,time[i]=1n=10^5, mid=10^{14}, time[i]=1 时,sum 会达到 101910^{19},甚至超过 unsigned long long 的上限。提前剪枝是保证正确性的核心。
    2. 右边界的选择

      • 不要盲目设 1e18。虽然二分是对数级别的,但更紧确的边界(如 min_element * totalTrips)可以减少几次迭代,并降低溢出风险。
    3. I/O 优化

      • 对于 N=105N=10^5 的数组读入,cin 较慢。使用 ios::sync_with_stdio(false)scanf 可以显著节省时间。
    4. 总结思考

      • 什么时候用二分答案? 当题目求“最小的可能值”,且如果你给定一个答案,你能很容易地判定它是否合法(满足单调性)时。

    同学,理解了从“挨个试”到“对半砍”的这种跳跃了吗?这就是算法的魅力!加油。

    信息

    ID
    19500
    时间
    1000ms
    内存
    128MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者