守卫-贪心,线段处理

P3634 守卫-贪心


P3634 守卫
这个线段处理值的学习

题意

给定区间及权值 \(0/1\),还有总共 \(1\) 的个数 \(k\) ,表示区间内是否有 \(1\) ,问一定为 \(1\) 的点有那些。

思路

很容易发现:

  1. \(0\) 的区间一定没有一。
  2. 去掉 \(0\) 之后长度刚好为 \(k\) 则一定有 \(1\)
  3. 值为 1 的区间长度恰好为 \(1\) 的时候,一定满足条件。
  4. 如果存在区间包含关系,保留小的区间。--小的区间外都可以为 \(0\)

考虑剩下的区间,容易发现如果在区间相交的地方存在 \(1\) ,可以减少剩余可能存在 \(1\) 的区间数量。因为如果权值为 \(1\) 的区间内有 \(1\) ,那剩余区间就是可能有 \(1\) ,不符合要求。可以用来判断无解。

考虑 \(1\) 最少的情况,因为此时总和最小,如果不选某个点导致总和超过 \(k\) ,则必须选这个点,因为无论如何构造都不会有方式让总和小于 \(k\)

细节

  1. \(1\) 最少的情况用线段覆盖来求。
  2. 保留的线段权值都为 \(1\) ,且删去权值为 \(0\) 的线段。
  3. 判断是否无解要知道在这个点前面作最优解和这个点后面作最优解,所以对得到的最小情况要做一次前缀和和后缀和。

代码

#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int maxn = 1e5+10;
constexpr int INF = 0x3f3f3f3f3f3f3f3f;

typedef struct line
{
    int l,r;
    bool operator<(const line &o)const // l为第一关键字
    {
        return l!=o.l ? l<o.l : r<o.r;
    }
}line;

int n,k,m;// 长,个数,区间数
int d[maxn];// 差分数组, 标记0的线段覆盖
int idx,cnt;// 线段数量,可能为1的点
line lin[maxn];// 线段-1
int pre[maxn],suf[maxn];// i左边可能为1的点的编号(cnt)
int sti[maxn];// 临时存储剩余的线段下标
int pre_cnt[maxn],suf_cnt[maxn];// 到某个线段最少要选取的点数(前后缀和)
int cov[maxn]; // cnt转坐标

signed main()
{
    #ifndef ONLINE_JUDGE
    freopen("cjdl.in","r",stdin);
    freopen("cjdl.out","w",stdout);
    #endif // ONLINE_JUDGE

    scanf("%lld%lld%lld",&n,&k,&m);

    for(int i=1,a,b,c ;i<=m;++i)
    {
        scanf("%lld%lld%lld",&a,&b,&c);
        if(c==0)
        {
            ++d[a];// 差分标记
            --d[b+1];
        }
        else
        {
            ++idx;
            lin[idx]={a,b};
        }
    }

    for(int i=1;i<=n;++i)
    {
        // 还原差分,找剩下1的点
        d[i+1]+=d[i];
        if(!d[i])
        {
            pre[i]=suf[i]=++cnt;
            cov[cnt]=i;
        }
    }
    if (k == cnt)// 如果刚好满足,必须要判!
    {
        for (int i = 1; i <= cnt; i++)
        {
            printf("%lld\n", cov[i]);
        }
        return 0;
    }

    for(int i=1;i<=n;++i)// 映射,似乎可以不用
    {
        if(!pre[i])
        {
            pre[i]=pre[i-1];
        }
    }
    for(int i=n;i>=1;--i)
    {
        if(!suf[i])
        {
            suf[i]=suf[i+1];
        }
    }

    for(int i=1;i<=idx;++i)
    {
        lin[i].l=suf[lin[i].l];// 跳过0的点
        lin[i].r=pre[lin[i].r];
    }

    sort(lin+1,lin+1+idx);// 现在排序
    for(int i=1;i<=idx;++i)
    {
        if(lin[i].l>lin[i].r)
        {
            continue;
        }
        while(sti[0] && lin[sti[sti[0]]].r>=lin[i].r)// 删除包含小线段的线段
        {
            --sti[0];
        }
        sti[++sti[0]]=i;
    }
    idx=sti[0];
    for(int i=1;i<=idx;++i)
    {
        lin[i]=lin[sti[i]];// 挑完的线段
    }

    int rr=0;// 前缀最少要选取的点数
    for(int i=1;i<=idx;++i)
    {
        if(lin[i].l>rr)
        {
            pre_cnt[i]=pre_cnt[i-1]+1;
            rr=lin[i].r;
        }
        else
        {
            pre_cnt[i]=pre_cnt[i-1];
        }
    }

    int ll=INF;// 后缀
    for(int i=idx;i>=1;--i)
    {
        if(lin[i].r<ll)
        {
            suf_cnt[i]=suf_cnt[i+1]+1;
            ll=lin[i].l;
        }
        else
        {
            suf_cnt[i]=suf_cnt[i+1];
        }
    }

    bool flag=0;
    for(int i=1;i<=idx;++i)
    {
        if(lin[i].l==lin[i].r)// 一个点必须要
        {
            flag=1;
            printf("%lld\n",cov[lin[i].l]);
            continue;
        }
        if(pre_cnt[i] != pre_cnt[i-1]+1) continue;// 不是有影响的线段右端点
        int pos=idx+1;
        int l=i+1,r=idx;// 二分找下一个线段(左端点刚好大) 可以用双指针更快
        while(l<=r)
        {
            int mid=l+((r-l)>>1);
            if(lin[mid].l>lin[i].r-1)
            {
                pos=mid;
                r=mid-1;
            }
            else
            {
                l=mid+1;
            }
        }
        if(pre_cnt[i]+suf_cnt[pos]>k)// 不选这个点无解
        {
            flag=1;
            printf("%lld\n",cov[lin[i].r]);
        }
    }

    if(!flag)// 没有选一个点
    {
        printf("-1\n");
    }

    return 0;
}
posted @ 2025-11-12 08:49  玖玮  阅读(8)  评论(0)    收藏  举报