会议中心-贪心/dp

P3626 会议中心 - 贪心/dp

题意

给定 \(m\) 条线段的 \(l\)\(r\),求最大不重叠线段数,和满足线段数最多情况下最小字典序。

思路

写题历程
第一问很容易想到贪心,\(n \log n\) 可得,但我贪心在第二问一点思路没有(题解也看不懂),而最小字典序可以在 dp 的时候求出,所以考虑 dp。

回头再来思考一下第一问怎么用 dp 求解。像我之前碰到都是单调队列优化,那这里应该也是可行的。看眼 tj ……使用贪心。而且并没有人用单调队列优化,而是用线段树(树状数组)。思考了一下,发现拿单调队列写确实会有不少问题(怪不得我以前写不出来)。dp 的状态转移:

\[dp_r = \max(dp_r, \max(dp_{rr} \mid rr < l)) \]

每做出来一个放到线段树中,要用的时候查询 \(0\)\(l\) 的最大值。这道题用 dp 还要线段树多维护一个会议编号作为第二关键字,具体的,题解里都在各显神通,更本看不懂。

回到贪心,我现在想对每条已知的线段对应可填充的范围找最小的线段,这样贪心的问题是:在后一个区间的 \(l\) 没有确定的情况下,在可变范围内选取排序最小的线段可能不优,因为正常贪心的 \(l\) 是不确定的,可能在下一个区间它的最小的线段的 \(l\) 更右边,可以给当前线段更多的选择。

这让我想到了之前 chy 作类似贪心的时候先对左端点排序,再对右端点排序,这样可不可以避免掉这种问题呢?好像不能。看眼 tj。好像理解了。

正文
其实挺 dp 的,为了满足字典序最小,我们每次尝试指定下一个字典序最优的线段,然后判断如果包含这条线段能否做到个数不变,如果可以,则加入,否则就跳过。显然如果可以实现,则得到的就是答案。(贪心字典序)

具体实现:考虑维护一段区间内最多的线段数,然后每次尝试加入一个线段,如果加入后,其左端点到上一个线段的右端点间的区间 \(+\) 其右边的空区间可填充的线段数不变,则加入,否则跳过。

状态定义:\(cnt_{l,r}\) 表示 \([l,r]\) 区间内的最多线段数。
如果暴力枚举,时间复杂度是 \(O(n^2)\),显然不可行。然后捏?tj。

由于贪心策略一定,所以每条线段的下一个最优一定,考虑倍增优化,倍增需要知道每位的下一个是谁,即要找到左端点大于当前线段右端点且右端点尽可能小的线段。

这里题解没说怎么写,我选择用双指针,即在对右端点排序的树组上从 \(i\) 开始,\(j\) 不断向后,直到 \(\text{lin}[j].l > \text{lin}[i].r\) 为止,此时满足条件,直到 \(i\) 往后移动至 \(j\) 不再成立。
写到这里我觉得我需要先打一下代码试试。

成功 ac,大概也就花了一个下午吧。

来补充一下细节:

  1. 需要映射
  2. nex 存的是下标
  3. nex 的预处理要到 \(n+1\)
  4. 边界都要要加哨兵
  5. 判断是否重合要用 lower_bound,因为刚好重合的时候 upper_bound 左右都会返回下一个位置

Code

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

typedef struct line
{
    int l,r,id;
}line;

typedef struct node
{
    int pos,id;

    node(int x,int y):pos(x), id(y){}

    bool operator<(const node &o)const
    {
        return pos<o.pos;
    }
}node;

line lin[maxn];
int nex[maxn][20],log2_n;
int disc[maxn<<1],idx;
int n;
set<node> used;

bool cmp1(line a,line b)
{
    return a.r<b.r;
}

bool cmp2(line a,line b)
{
    return a.id<b.id;
}

int get_cnt(int id,int r)// []
{
    int ret=0;
    for(int k=log2_n;k>=0;--k)
    {
        if(lin[nex[id][k]].r<=r)
        {
            ret+=(1<<k);
            id=nex[id][k];
        }
    }
    return ret;
}

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

    scanf("%lld",&n);
    while((1<<log2_n)<=n) ++log2_n;// 道n要几次方
    --log2_n;

    for(int i=1,l,r ;i<=n;++i)
    {
        scanf("%lld%lld",&l,&r);
        disc[++idx]=l;
        disc[++idx]=r;
        lin[i]={l,r,i};
    }

    sort(disc+1,disc+1+idx);// 去重
    idx=unique(disc+1,disc+1+idx)-disc-1;
    for(int i=1;i<=n;++i)
    {
        lin[i].l=lower_bound(disc+1,disc+idx+1,lin[i].l)-disc;
        lin[i].r=lower_bound(disc+1,disc+idx+1,lin[i].r)-disc;
    }

    sort(lin+1,lin+1+n,cmp1);// 还原数组(因为set用的是下标)所以我们求的要用下标,这里要还原

    lin[n+1]={INF,INF,n+1};
    lin[0]={-INF,-INF,0};

    for(int i=0,j=0 ;i<=n;++i)
    {
        while(j<=n && lin[j].l<=lin[i].r)
        {
            ++j;
        }
        nex[lin[i].id][0]=lin[j].id;// 两个都换成下标
    }
    nex[n+1][0]=n+1;

    for(int k=1;k<=log2_n;++k)
    {
        for(int i=0;i<=n+1;++i)
        {
            nex[i][k]=nex[nex[i][k-1]][k-1];
        }
    }

    sort(lin+1,lin+1+n,cmp2);

    int tal=get_cnt(0,INF-1);
    printf("%lld\n",tal);

    used.emplace(node(-INF,0));
    used.emplace(node(INF,n+1));
    for(int i=1;i<=n;++i)
    {
        auto it=used.upper_bound({lin[i].r,lin[i].id});
        if(used.lower_bound({lin[i].r,lin[i].id})!= used.lower_bound({lin[i].l,lin[i].id}))
        {
            continue;
        }
        int rid=(*it).id;
        int pos=(*it).pos;
        --it;
        int lid=(*it).id;
        if(get_cnt(lid,lin[i].l-1)+get_cnt(i,pos-1)+1!=get_cnt(lid,pos-1))
        {
            continue;
        }
        printf("%lld ",i);
        used.emplace(node(lin[i].l,lin[i].id));
        used.emplace(node(lin[i].r,lin[i].id));
    }

    return 0;
}
posted @ 2025-11-10 15:05  玖玮  阅读(0)  评论(0)    收藏  举报