会议中心-贪心/dp
P3626 会议中心 - 贪心/dp
题意
给定 \(m\) 条线段的 \(l\) 和 \(r\),求最大不重叠线段数,和满足线段数最多情况下最小字典序。
思路
写题历程:
第一问很容易想到贪心,\(n \log n\) 可得,但我贪心在第二问一点思路没有(题解也看不懂),而最小字典序可以在 dp 的时候求出,所以考虑 dp。
回头再来思考一下第一问怎么用 dp 求解。像我之前碰到都是单调队列优化,那这里应该也是可行的。看眼 tj ……使用贪心。而且并没有人用单调队列优化,而是用线段树(树状数组)。思考了一下,发现拿单调队列写确实会有不少问题(怪不得我以前写不出来)。dp 的状态转移:
每做出来一个放到线段树中,要用的时候查询 \(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,大概也就花了一个下午吧。
来补充一下细节:
- 需要映射
- nex 存的是下标
- nex 的预处理要到 \(n+1\)
- 边界都要要加哨兵
- 判断是否重合要用
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;
}

浙公网安备 33010602011771号