UVa 1707 Surveillance

题目描述

国际保护与控制公司(ICPC\texttt{ICPC}ICPC )希望监控他们的总部大楼。大楼呈凸多边形,有 nnn 面墙(编号 111nnn ),周围有 kkk 个可安装摄像头的位置。每个摄像头可以覆盖一段连续的墙壁。具体来说,对于第 iii 个摄像头,给定参数 aia_iaibib_ibi

  • ai≤bia_i \le b_iaibi ,则覆盖区间 [ai,bi][a_i, b_i][ai,bi]
  • ai>bia_i > b_iai>bi ,则覆盖区间 [ai,n]∪[1,bi][a_i, n] \cup [1, b_i][ai,n][1,bi] (即跨越 nnn111 的环形区间)。

两个摄像头的覆盖范围可以重叠。目标是选择最少的摄像头,使得所有墙壁都被覆盖。若无法覆盖所有墙壁,输出 impossible

输入格式 : 多组数据。每组第一行两个整数 nnnkkk3≤n≤1063 \le n \le 10^63n1061≤k≤1061 \le k \le 10^61k106 ),随后 kkk 行每行两个整数 ai,bia_i, b_iai,bi

输出格式 : 每组数据输出最少摄像头数,或 impossible

题目分析与解题思路

1. 问题转化:环形转链

本题的核心是环形区间覆盖问题。墙壁编号 111nnn 形成一个环,每个摄像头覆盖环上的一段连续墙壁。处理环形问题的常用技巧是破环成链:将长度为 nnn 的环复制一份,得到长度为 2n2n2n 的链。这样,环上任意一个跨越 nnn111 的区间,在链上都可以表示为一个普通区间。

具体来说,对于每个摄像头给出的 (a,b)(a, b)(a,b)

  • a≤ba \le bab :在链上对应区间 [a,b][a, b][a,b][a+n,b+n][a+n, b+n][a+n,b+n] (复制部分)。
  • a>ba > ba>b :在环上覆盖 [a,n]∪[1,b][a, n] \cup [1, b][a,n][1,b] ,等价于链上区间 [a,b+n][a, b+n][a,b+n]

注意,由于环上的一段连续墙壁长度不超过 nnn ,因此在链上我们只关心长度不超过 nnn 的区间。

2. 问题转化:最小区间覆盖

2n2n2n 的链上,我们需要覆盖任意一段长度为 nnn 的连续区间(例如 [1,n][1, n][1,n] ),这等价于覆盖整个环。问题转化为:在链上选择最少的区间,覆盖某个长度为 nnn 的连续段。

这是一个经典的最小区间覆盖问题,可以用贪心算法解决:

  1. 将区间按左端点排序。
  2. 设当前覆盖到位置 curcurcur ,初始为起点 startstartstart
  3. 在左端点 ≤cur\le curcur 的所有区间中,选择右端点最大的一个,将 curcurcur 更新为该右端点,同时计数加一。
  4. 重复直到 cur≥start+ncur \ge start + ncurstart+n ,表示已覆盖整个长度为 nnn 的段。

如果某一步无法找到左端点 ≤cur\le curcur 的区间,则覆盖失败。

3. 算法优化:预处理与倍增

直接对每个起点 startstartstart1≤start≤n1 \le start \le n1startn )进行贪心,时间复杂度为 O(n2)O(n^2)O(n2) ,无法通过 n≤106n \le 10^6n106 的数据。我们需要更高效的方法。

关键观察 :对于链上每个位置 iii ,我们可以预处理出从 iii 开始(选择左端点 ≤i\le ii 的区间)能够到达的最远位置,记为 next[i]next[i]next[i] 。这可以通过扫描所有区间并取前缀最大值得到。

有了 next[i]next[i]next[i] ,贪心过程可以描述为:从 startstartstart 出发,不断跳到 next[cur]next[cur]next[cur] ,直到越过 start+nstart+nstart+n 。这相当于在一条链上跳跃,每次跳到下一个最远可达位置。

为了快速计算从每个起点出发所需的跳跃次数,我们可以使用倍增法Binary Lifting\texttt{Binary Lifting}Binary Lifting ):

  • dp[i][j]dp[i][j]dp[i][j] 表示从位置 iii 出发,经过 2j2^j2j 次跳跃后到达的位置。
  • 初始 dp[i][0]=next[i]dp[i][0] = next[i]dp[i][0]=next[i]
  • 转移: dp[i][j]=dp[dp[i][j−1]][j−1]dp[i][j] = dp[dp[i][j-1]][j-1]dp[i][j]=dp[dp[i][j1]][j1]

这样,对于每个起点 startstartstart ,我们从高位到低位尝试跳跃:如果从当前位置 curcurcur2j2^j2j 步后仍未到达 start+nstart+nstart+n ,则执行这次跳跃,并将步数加上 2j2^j2j 。最后再跳一步即可。

4. 算法步骤

  1. 读入与标准化 : 将每个区间 (a,b)(a, b)(a,b) 标准化为链上区间。注意,对于 a≤ba \le bab 的情况,需要同时处理原区间和复制区间。
  2. 预处理 nextnextnext 数组
    • 初始化 next[i]=inext[i] = inext[i]=i
    • 对于每个标准化后的区间 [l,r][l, r][l,r] ,更新 next[l]=max⁡(next[l],r)next[l] = \max(next[l], r)next[l]=max(next[l],r)
    • 计算前缀最大值: next[i]=max⁡(next[i],next[i−1])next[i] = \max(next[i], next[i-1])next[i]=max(next[i],next[i1])
  3. 构建倍增表
    • dp[i][0]=next[i]+1dp[i][0] = next[i] + 1dp[i][0]=next[i]+1 (加 111 是为了跳到下一个未覆盖的位置)。
    • 对于 j≥1j \ge 1j1dp[i][j]=dp[dp[i][j−1]][j−1]dp[i][j] = dp[dp[i][j-1]][j-1]dp[i][j]=dp[dp[i][j1]][j1]
    • 注意处理边界,超出 2n2n2n 的位置设为 2n+12n+12n+1
  4. 枚举起点并计算
    • 对每个起点 start=1,2,…,nstart = 1, 2, \dots, nstart=1,2,,n
      • 用倍增法计算从 startstartstart 出发,需要多少步(即多少个区间)才能覆盖 [start,start+n−1][start, start+n-1][start,start+n1]
      • 记录最小步数。
  5. 输出答案 : 若最小步数不超过 kkk ,输出该步数;否则输出 impossible

5. 时间复杂度与空间复杂度

  • 预处理 nextnextnext 数组: O(n+k)O(n + k)O(n+k)
  • 构建倍增表: O(nlog⁡n)O(n \log n)O(nlogn)
  • 枚举起点并查询: O(nlog⁡n)O(n \log n)O(nlogn)
  • 总时间复杂度: O((n+k)log⁡n)O((n+k) \log n)O((n+k)logn) ,满足 n,k≤106n, k \le 10^6n,k106 的限制。
  • 空间复杂度: O(nlog⁡n)O(n \log n)O(nlogn) ,主要来自倍增表。

代码实现

// Surveillance
// UVa ID: 1707
// Verdict: Accepted
// Submission Date: 2025-12-05
// UVa Run Time: 2.800s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net

#include <bits/stdc++.h>
using namespace std;

const int MAXN = 2e6 + 10; // 注意:2 * n 可能达到 2e6
const int LOG = 22;        // 2^21 > 2e6

int n, k;
int maxReach[2 * MAXN]; // maxReach[i] = 从 ≤ i 的位置开始能覆盖到的最远位置
int dp[2 * MAXN][LOG];  // 倍增表

int main() {
    while (scanf("%d %d", &n, &k) == 2) {
        // 初始化
        for (int i = 1; i <= 2 * n; ++i)
            maxReach[i] = i;
        
        // 处理每个摄像头
        for (int i = 0; i < k; ++i) {
            int a, b;
            scanf("%d %d", &a, &b);
            if (a <= b) {
                // 区间 [a, b]
                maxReach[a] = max(maxReach[a], b);
                // 在链的复制部分也要更新
                if (a + n <= 2 * n)
                    maxReach[a + n] = max(maxReach[a + n], b + n);
            } else {
                // 区间 [a, b+n](在环上覆盖了a到n和1到b)
                maxReach[a] = max(maxReach[a], b + n);
            }
        }
        
        // 计算前缀最大值
        for (int i = 2; i <= 2 * n; ++i)
            maxReach[i] = max(maxReach[i], maxReach[i - 1]);
        
        // 构建倍增表
        for (int i = 1; i <= 2 * n; ++i)
            dp[i][0] = maxReach[i] + 1; // 注意:+1是为了方便,表示选择这个区间后能到达的下一个位置
        
        // 如果dp[i][0]超过边界,设为2*n+1
        for (int i = 1; i <= 2 * n; ++i)
            if (dp[i][0] > 2 * n) dp[i][0] = 2 * n + 1;
        
        // 构建倍增表
        for (int j = 1; j < LOG; ++j)
            for (int i = 1; i <= 2 * n; ++i)
                if (dp[i][j - 1] <= 2 * n)
                    dp[i][j] = dp[dp[i][j - 1]][j - 1];
                else
                    dp[i][j] = 2 * n + 1;
        
        int ans = k + 1;
        
        // 检查每个起点
        for (int start = 1; start <= n; ++start) {
            int current = start;
            int steps = 0;
            
            // 使用倍增找到覆盖[start, start+n-1]所需的最小步数
            for (int j = LOG - 1; j >= 0; --j) {
                if (dp[current][j] < start + n) {
                    current = dp[current][j];
                    steps += (1 << j);
                }
            }
            
            // 再走一步
            current = dp[current][0];
            steps++;
            
            if (current <= start + n) {
                ans = min(ans, steps);
            }
        }
        
        if (ans > k)
            printf("impossible\n");
        else
            printf("%d\n", ans);
    }
    
    return 0;
}

总结

本题是环形区间覆盖问题的经典例题,通过破环成链技巧将环形问题转化为线性问题,再使用贪心+倍增的方法高效求解。关键在于预处理每个位置能到达的最远位置,并利用倍增表快速模拟贪心过程。算法的时间复杂度为 O((n+k)log⁡n)O((n+k) \log n)O((n+k)logn) ,能够处理 n,kn, kn,k 高达 10610^6106 的大规模数据。

这类问题在竞赛中常见,掌握破环成链和倍增技巧对于解决类似问题非常有帮助。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值