[APIO2009]会议中心(贪心+倍增)
题意
有一些形如[L, R]的区间,你要选出尽可能多的区间,并满足区间两两交集为空(注意[X, X]非空) 。
求最多能选出的区间数及字典序最小的方案。
扯淡
如果不要求方案,这就是一道水题。
但是要求方案这就是一道火题辣。
题解
首先我们引入一个定理
任何不影响贪心策略的线段都存在于某一最优方法中。(这里的贪心策略指的是求最多能选出的区间数的贪心策略)
如图,线段
DC,EF
D
C
,
E
F
都可以选择,因为它们既不与
AB
A
B
相交,又不影响下一步决策(选择
GH
G
H
)
根据这个定理,这道题的做法就是按字典序选出可行的线段。
如何运用这个定理判断此线段是否能选择?
既然它满足贪心策略,那么就用贪心来检验好了。
设
f(l,r)
f
(
l
,
r
)
为只选择
∈[l,r]
∈
[
l
,
r
]
线段的答案。那么线段
[x,y]
[
x
,
y
]
能选择当且仅当
(l为此区间左边的已选区间的最右区间的右端点,r为此区间右边的已选区间的最左区间的左端点)
如图
条件是 f(a+1,e−1)+f(g+1,h−1)+1=g(a+1,h−1) f ( a + 1 , e − 1 ) + f ( g + 1 , h − 1 ) + 1 = g ( a + 1 , h − 1 )
贪心因为不能每次 O(n) O ( n ) 的检验所以要用倍增优化(这就是这道倍增题的唯一倍增考点???)
算法流程:
- 离散化线段
- 构建倍增数组( fi,j f i , j 的定义:从 i i 开始选择条线段,最优方案中第 j j 条线段的右端点为)
- 统计答案(按字典序选择,能选则选)
#include <cstdio>
#include <cstdlib>
#include <algorithm>
using namespace std;
const int maxn = 200005;
const int Inf = 1 << 30;
int n, l[maxn], r[maxn], tot, cnt, f[maxn * 2][20];
int *q[maxn * 2];
int lmax[maxn * 2], rmax[maxn * 2], lsum[maxn * 2], rsum[maxn * 2];
int ans[maxn];
inline bool cmp(const int* a, const int* b)
{
return *a < *b;
}
int solve(int l, int r)
{
int ans = 0;
for(int j = 18; j >= 0; --j)
if(f[l][j] <= r) ans += (1 << j), l = f[l][j] + 1;
return ans;
}
#define lowbit(x) ((x) & (-x))
void add_sum(int *p, int x)
{
while(x <= cnt) {
++p[x];
x += lowbit(x);
}
}
int query_sum(int *p, int x)
{
int ret = 0;
while(x > 0) {
ret += p[x];
x -= lowbit(x);
}
return ret;
}
void add_max(int *p, int x)
{
int t = x;
while(x <= cnt) {
p[x] = max(p[x], t);
x += lowbit(x);
}
}
int query_max(int *p, int x)
{
int ret = 0;
while(x > 0) {
ret = max(ret, p[x]);
x -= lowbit(x);
}
return ret;
}
int main()
{
//读入
scanf("%d", &n);
for(int i = 1; i <= n; ++i)
scanf("%d%d", &l[i], &r[i]), q[++tot] = &l[i], q[++tot] = &r[i];
//离散化
sort(q + 1, q + tot + 1, cmp);
for(int tmp = -1, i = 1; i <= tot; ++i) {
if(*q[i] != tmp) {
tmp = *q[i]; *q[i] = ++cnt;
}else *q[i] = cnt;
}
//构建倍增数组
for(int i = 1; i <= cnt + 1; ++i)
for(int j = 0; j <= 20; ++j)
f[i][j] = Inf;
for(int i = 1; i <= n; ++i)
f[l[i]][0] = min(f[l[i]][0], r[i]);
for(int i = cnt; i >= 1; --i) {
f[i][0] = min(f[i][0], f[i + 1][0]);
for(int j = 1; f[i][j - 1] <= cnt && (1 << j) <= n; ++j)
f[i][j] = f[f[i][j - 1] + 1][j - 1];
}
//统计答案
tot = 0;
for(int i = 1; i <= n; ++i) {
int v = tot - query_sum(rsum, l[i] - 1) - query_sum(lsum, cnt - r[i]);
if(v > 0) continue; //此区间已被覆盖
int l_lst = query_max(rmax, l[i] - 1), r_nxt = cnt - query_max(lmax, cnt - r[i]);
if(solve(l_lst + 1, l[i] - 1) + solve(r[i] + 1, r_nxt) + 1 == solve(l_lst + 1, r_nxt)) {
ans[++tot] = i;
add_sum(rsum, r[i]); add_sum(lsum, cnt - l[i] + 1);
add_max(rmax, r[i]); add_max(lmax, cnt - l[i] + 1);
}
}
//输出
printf("%d\n", tot);
for(int i = 1; i <= tot; ++i)
printf("%d ", ans[i]);
return 0;
}