#P3586. 第2题-昙花一现
          
                        
                                    
                      
        
              - 
          
          
                      1000ms
            
          
                      Tried: 48
            Accepted: 10
            Difficulty: 7
            
          
          
          
                       所属公司 : 
                              京东
                                
            
                        
              时间 :2025年9月6日
                              
                      
          
 
- 
                        算法标签>扫描线算法          
 
第2题-昙花一现
解题思路
1. 将问题转化
把每株花对应为一个等长区间,定义覆盖计数 c(x) 为时刻 x 被多少区间覆盖。 我们关心的量为
S=x∑[c(x)=1],即被恰好 1 个区间覆盖的总长度。
若把某一株 i 的区间移走(临时删除),对 S 的影响:
- 把原来 i 独占 的部分(记为 unii)从 1 变为 0,Δ=−unii;
 - 把原来 恰好两株覆盖且其中一株是 i 的部分(记为 twoi)从 2 变为 1,Δ=+twoi;
 - 对覆盖数 ≥3 的部分无影响。
 
所以“删除 i”后得到 S′=S−unii+twoi。
再次把 i 放回去:我们可以把它移动到任意位置。若把它放到与其它区间完全不相交的地方,则新增贡献 = 区间长度 m。 (题面允许改成任意正整数,因此永远能把区间放到一个“远离所有区间”的位置。)
于是若选择第 i 株去改,最终
$$S_{\text{final}} = S - uni_i + two_i + m = S + m + (two_i - uni_i). $$显然应选使 twoi−unii 最大的 i。另外,我们也可以“不真正移动”,使答案至少为当前的 S。因此
$$\boxed{\text{Ans}=\max\Big(S,\ S+m+\max_i(two_i-uni_i)\Big)}. $$问题归约为:用一次扫描计算
- 总的 S;
 - 每个区间 i 的 unii(它独占的长度)与 twoi(它处在“恰好两株覆盖”时的长度)。
 
2. 扫描线 + 事件(坐标压缩)
时刻范围可能很大,但区间端点只有 O(n) 个。对所有端点做坐标压缩并用“进入/离开事件”扫描:
- 
对区间 [l,r] 建两个事件:在 l 处加入 i;在 r+1 处移除 i(把闭区间转成半开 [l,r+1) 便于处理)。
 - 
将所有事件时刻升序,维护当前活跃集合
act(存活区间的编号)。 - 
若相邻压缩点 [x,next) 之间长度为
len = next - x,则:|act|==1:记唯一编号为id,令uni[id]+=len且S+=len;|act|==2:设为a,b,令two[a]+=len,two[b]+=len;- 其它情况忽略。
 
 - 
时间复杂度 O(nlogn)(集合增删取元素),空间 O(n)。
 
这就是标准的扫描线算法。
复杂度
- 坐标压缩与事件排序:O(nlogn);
 - 扫描线:每事件一次插入/删除,O(nlogn);
 - 总复杂度:O(nlogn),空间 O(n)。
 
代码
Python
import sys
def solve_one(n, m, ts):
    # 构造闭区间 [l, r],并准备 r+1 作为离开事件
    L, R = [], []
    pts = []
    for i, t in enumerate(ts):
        l = t
        r = t + m - 1
        L.append(l)
        R.append(r)
        pts.append(l)
        pts.append(r + 1)
    # 坐标压缩
    xs = sorted(set(pts))
    idx = {x:i for i, x in enumerate(xs)}
    k = len(xs)
    # 事件:在压缩坐标 pos 处的进入/离开列表
    add = [[] for _ in range(k)]
    rem = [[] for _ in range(k)]
    for i in range(n):
        add[idx[L[i]]].append(i)
        rem[idx[R[i] + 1]].append(i)
    # 扫描线
    import bisect
    act = set()           # 当前活跃区间的编号集合
    uni = [0] * n         # 每个区间独占长度
    two = [0] * n         # 每个区间处在“恰好两株”时的长度
    S = 0                 # 全局恰好一株的长度
    for p in range(k - 1):
        # 先处理在 xs[p] 处的进入事件
        for i in add[p]:
            act.add(i)
        length = xs[p + 1] - xs[p]
        if length > 0:
            if len(act) == 1:
                i = next(iter(act))
                uni[i] += length
                S += length
            elif len(act) == 2:
                it = iter(act)
                a = next(it); b = next(it)
                two[a] += length
                two[b] += length
        # 区间在 r+1 处离开
        for i in rem[p + 1]:
            if i in act:
                act.remove(i)
    # 计算答案:max(S, S + m + max(two[i]-uni[i]))
    best = max((two[i] - uni[i] for i in range(n)), default=-(10**18))
    return max(S, S + m + best)
def main():
    data = sys.stdin.read().strip().split()
    it = iter(data)
    t = int(next(it))
    out = []
    for _ in range(t):
        n = int(next(it)); m = int(next(it))
        ts = [int(next(it)) for _ in range(n)]
        out.append(str(solve_one(n, m, ts)))
    print("\n".join(out))
if __name__ == "__main__":
    main()
Java
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.*;
public class Main {
    static long solveOne(int n, int m, int[] ts) {
        int[] L = new int[n], R = new int[n];
        ArrayList<Integer> pts = new ArrayList<>(2 * n);
        for (int i = 0; i < n; i++) {
            L[i] = ts[i];
            R[i] = ts[i] + m - 1;
            pts.add(L[i]);
            pts.add(R[i] + 1);     // 半开区间的右端
        }
        // 坐标压缩
        Collections.sort(pts);
        ArrayList<Integer> xs = new ArrayList<>();
        for (int x : pts) if (xs.isEmpty() || xs.get(xs.size()-1) != x) xs.add(x);
        int k = xs.size();
        HashMap<Integer, Integer> idx = new HashMap<>(k * 2);
        for (int i = 0; i < k; i++) idx.put(xs.get(i), i);
        ArrayList<Integer>[] add = new ArrayList[k];
        ArrayList<Integer>[] rem = new ArrayList[k];
        for (int i = 0; i < k; i++) { add[i] = new ArrayList<>(); rem[i] = new ArrayList<>(); }
        for (int i = 0; i < n; i++) {
            add[idx.get(L[i])].add(i);
            rem[idx.get(R[i] + 1)].add(i);
        }
        long[] uni = new long[n];
        long[] two = new long[n];
        long S = 0;
        // 使用 LinkedHashSet 保证能取到首个元素(任意一个即可)
        HashSet<Integer> act = new HashSet<>();
        for (int p = 0; p < k - 1; p++) {
            // 进入
            for (int id : add[p]) act.add(id);
            long len = (long) xs.get(p + 1) - xs.get(p);
            if (len > 0) {
                if (act.size() == 1) {
                    int id = act.iterator().next();
                    uni[id] += len;
                    S += len;
                } else if (act.size() == 2) {
                    Iterator<Integer> it = act.iterator();
                    int a = it.next(), b = it.next();
                    two[a] += len;
                    two[b] += len;
                }
            }
            // 离开
            for (int id : rem[p + 1]) act.remove(id);
        }
        long best = Long.MIN_VALUE / 4;
        for (int i = 0; i < n; i++) best = Math.max(best, two[i] - uni[i]);
        return Math.max(S, S + m + best);
    }
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder out = new StringBuilder();
        int T = Integer.parseInt(br.readLine().trim());
        for (int tc = 0; tc < T; tc++) {
            String[] nm = br.readLine().trim().split("\\s+");
            int n = Integer.parseInt(nm[0]);
            int m = Integer.parseInt(nm[1]);
            String[] tsStr = br.readLine().trim().split("\\s+");
            int[] ts = new int[n];
            for (int i = 0; i < n; i++) ts[i] = Integer.parseInt(tsStr[i]);
            long ans = solveOne(n, m, ts);
            out.append(ans).append('\n');
        }
        System.out.print(out.toString());
    }
}
C++
#include <bits/stdc++.h>
using namespace std;
    
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int T; 
    if (!(cin >> T)) return 0;
    while (T--) {
        int n, m; 
        cin >> n >> m;
        vector<long long> t(n);
        for (int i = 0; i < n; ++i) cin >> t[i];
        vector<long long> L(n), R(n);
        vector<long long> pts; pts.reserve(2*n);
        for (int i = 0; i < n; ++i) {
            L[i] = t[i];
            R[i] = t[i] + m - 1;
            pts.push_back(L[i]);
            pts.push_back(R[i] + 1); // 半开右端
        }
        sort(pts.begin(), pts.end());
        pts.erase(unique(pts.begin(), pts.end()), pts.end());
        int k = (int)pts.size();
        vector<vector<int>> add(k), rem(k);
        auto get_idx = [&](long long x)->int {
            return (int)(lower_bound(pts.begin(), pts.end(), x) - pts.begin());
        };
        for (int i = 0; i < n; ++i) {
            add[get_idx(L[i])].push_back(i);
            rem[get_idx(R[i] + 1)].push_back(i);
        }
        vector<long long> uni(n, 0), two(n, 0);
        long long S = 0;
        unordered_set<int> act; act.reserve(n*2+10);
        for (int p = 0; p + 1 < k; ++p) {
            for (int id : add[p]) act.insert(id); // 进入
            long long len = pts[p+1] - pts[p];
            if (len > 0) {
                if (act.size() == 1) {
                    int id = *act.begin();
                    uni[id] += len; S += len;
                } else if (act.size() == 2) {
                    auto it = act.begin();
                    int a = *it; ++it; int b = *it;
                    two[a] += len; two[b] += len;
                }
            }
            for (int id : rem[p+1]) act.erase(id); // 离开
        }
        long long best = LLONG_MIN/4;
        for (int i = 0; i < n; ++i) best = max(best, two[i] - uni[i]);
        long long ans = max(S, S + (long long)m + best);
        cout << ans << "\n";
    }
    return 0;
}
        题目内容
昙花是一种很美丽的花;可惜的是,昙花开花的时间非常短暂,所以才有“昙花一现”之说,人们常说,目睹昙花一现,意味着好的事情即将发生。
小明是一位大魔法师,他种植了 n 株昙花,并预知了这些昙花开花的时间 t1,t2,…,tn,每一株昙花只会开花 m 秒,m 是一个固定的数值。也就是说,第 i 株昙花将会在 [ti,ti+m−1] 开花。
小明想让自己欣赏昙花开花的时间尽可能长。但是,小明不满足于看两株及以上的昙花开花,他认为那样太过艳丽,有失风采。也就是说,小明想让恰有一株昙花开花的时刻尽可能多。
作为大魔法师,小明可以施展至多一次魔法:他可以选定任意一株昙花,并将这株昙花的开花时间 ti 修改成任意正整数,请问,小明最多能让恰有一株昙花开花的时间变为多久?
输入描述
本题中,单个测试用例包含多组数据。
第一行一个正整数 T ,表示数据组数。
对于每一组数据:
第一行两个正整数 n 和 m
第二行 n 个正整数,表示 t1,t2,...,tn 。
1≤∑n≤2∗105,1≤m,ti≤5∗n,1≤T≤1000
注意,小明施展魔法时可以将 ti 修改成任意正整数,不受 ti≤5∗n 的约束。
输出描述
输出 T 行,每行一个非负整数,表示恰有一株昙花开花的最大时刻数。
样例1
输入
2
4 10
1 3 12 20
5 5
2 1 10 3 11
输出
36
12
说明
第一组样例的解释如下:
施展魔法前,四株昙花的开花时间段分别为 [1,10],[3,12],[12,21] 和 [20,29] 。
恰有一株昙花开花的时间段有 [1,2],[11,11],[13,19] 和 [22,29] ,时刻数为 2+1+7+8=18 。
小明可以施展魔法,将 t2 修改为 30 。
施展魔法后,四株昙花的开花时间段分别为 [1,10],[30,39],[12,21] 和 [20,29] 。
恰有一颗昙花开花的时间段有 [1,10],[12,19],[22,29] 和 [30,39] ,时刻数为 10+8+10=36 。
可以证明没有更优的方案。