您的位置:首页 > 其它

[BZOJ2811][Apio2012]Guard(线段树+二分+贪心)

2016-08-20 10:05 302 查看

=== ===

这里放传送门

=== ===

题解

首先我们要根据输入数据中的区间信息来得到哪些点上是一定没有忍者的,那么快速做到信息为‘0’的区间的合并就要用到线段树。把所有C=0 的区间都扔到线段树里面,然后枚举所有点就可以查出每个点的信息了。线段树的另一个用处就是做一个必要的特判,即当可能有忍者的点仅有k个时这些点就一定有忍者。

线段树的工作到此就结束了。这个时候序列里剩下的值为1的点一定大于k个,我们需要从里面找出哪些点对于满足条件是必须的。显然如果一个点是必须的,那么去掉这个点以后就无法构造出一组有k个点的合法解。

为了知道哪些点是必须的,我们要找到一组使用点数最少的合法解。如果使用的点数少于k个那么说明剩下的点是可以随便放的。只有这样选出来的点才有可能成为必须点。而显然如果我们有一些按端点从左到右排好序的区间,那么在区间的右端点放一个忍者比不是右端点的位置更优,因为它可以满足尽量多的区间。并且,如果有一个大区间里面包含着一个小区间,那么放在小区间的右端点显然更优,因为它可以同时满足两个区间,所以较大的区间对于构造使用点数尽量少的合法解是没有用的,可以舍弃。

具体来说,为了接下来二分的需要,要对这些点进行离散化,把“0”的点缩掉只留下“1”的点。于是用hash数组存储离散化后的编号,anti数组来找回离散化之前的编号用于输出。而离散化之后,有些值为1的区间可能左右端点在0上,所以要把区间“筛”一下,从中去掉不合法的区间。于是首先扫两遍序列得到每个点前后第一个1是谁,也就是pre 和nxt 。接下来枚举所有区间,如果这个区间的左端点是0的话就把它后移,然后用hash数组转换成离散化以后的值。注意如果这个左端点的后面没有1了,nxt数组会把它移动到n+1的位置,这个时候直接把这个区间打上抛弃标记就可以了。把右端点也这样移动以后如果右端点移动到了左端点前面,那么这个区间也被抛弃了。排除掉所有被抛弃的区间以后左端点为第一关键字升序,右端点为第二关键字降序排序。这样处理完以后就会有左右端点全部单增的性质,就可以用贪心的方法找到那些可能成为必须点的右端点了。

最后枚举前面选出来的所有点判断是否为必须点。那么就枚举每一个右端点判断一下去掉这个点以后还能不能构造出合法解。次优的点当然是右端点前面的那个点,于是如果能知道这个次优点最多能满足的区间范围,然后范围左边和右边最少需要的点数,再+1就得到了一个答案,然后再判断这个答案是否小于等于k就可以了。于是用f数组和g数组分别存储区间i的左边和右边所有区间分别最少需要多少点来满足,相当于前缀和后缀。找到次优点即右端点-1,然后二分得到这个点最多能满足的区间范围,最后用f和g数组计算出答案来更新即可。

代码

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int n,k,m,ext[500010],pre[100010],nxt[100010],hash[100010],anti[100010],tot;
int cnt,ans[100010],now,wer,f[100010],g[100010], ,seq[100010];
bool dlt[500010];
struct interval{
int l,r,v;
bool usd;
}q[100010];
int comp(interval x,interval y){
return (x.usd<y.usd||(x.usd==y.usd&&x.l<y.l)||(x.usd==y.usd&&x.l==y.l&&x.r>y.r));
}
void update(int i){
ext[i]=ext[i<<1]+ext[(i<<1)+1];
}
void change(int i,int l,int r,int left,int right){
if (dlt[i]==true) return;
if (left<=l&&right>=r){
ext[i]=r-l+1;
dlt[i]=true;
return;
}
int mid=(l+r)>>1;
if (left<=mid) change(i<<1,l,mid,left,right);
if (right>mid) change((i<<1)+1,mid+1,r,left,right);
update(i);
}
bool ask(int i,int l,int r,int x){
if (ext[i]==r-l+1||dlt[i]==true) return 1;
if (l==r) return ext[i];
int mid=(l+r)>>1;
if (x<=mid) return ask(i<<1,l,mid,x);
else return ask((i<<1)+1,mid+1,r,x);
}
void get_pre_nxt(){
for (int i=1;i<=n;i++)
if (seq[i]==1) pre[i]=i;
else pre[i]=pre[i-1];
nxt[n+1]=n+1;
for (int i=n;i>=1;i--)
if (seq[i]==1) nxt[i]=i;
else nxt[i]=nxt[i+1];
}
void rebuild(){
for (int i=1;i<=m;i++){
interval *w=&q[i];
if (w->v==0){w->usd=true;continue;}
if (seq[w->l]==1) w->l=hash[w->l];
else{
w->l=nxt[w->l];
if (w->l==n+1) w->usd=true;
else w->l=hash[w->l];
}
if (seq[w->r]==1) w->r=hash[w->r];
else w->r=hash[pre[w->r]];
if (w->r<w->l) w->usd=true;
}
sort(q+1,q+m+1,comp);
for (int i=1;i<=m;i++)
if (q[i].usd==true){tot=i-1;break;}
now=tot;
for (int i=tot-1;i>=1;i--){
if (q[i].r>=q[now].r) q[i].usd=true;
else now=i;
}//去掉包含区间中较大的那个
sort(q+1,q+tot+1,comp);
for (int i=1;i<=m;i++)//找一下目前有多少个区间剩下
if (q[i].usd==true){tot=i-1;break;}
}
void get_f_g(){
int mx=0,mn=0x7fffffff;
for(int i=1;i<=tot;i++)//注意更新方式,不能用i-1更新i
if(q[i].l>mx)f[i]=f[i-1]+1,mx=q[i].r;
else f[i]=f[i-1];
for(int i=tot;i;i--)
if(q[i].r<mn)g[i]=g[i+1]+1,mn=q[i].l;
else g[i]=g[i+1];
}
int Lsearch(int l,int r,int x){
int mid;
while (l!=r){
int mid=(l+r)>>1;
if (q[mid].l>x) r=mid;
else l=mid+1;
}
return l;
}
int Rsearch(int l,int r,int x){
int mid,fds=0;
while (l!=r){
int mid=(l+r)>>1;
if (q[mid].r<x){
fds=max(fds,mid);l=mid+1;
}
else r=mid;
}
return fds;
}
int main()
{
scanf("%d%d%d",&n,&k,&m);
for (int i=1;i<=m;i++){
interval *w=&q[i];
scanf("%d%d%d",&w->l,&w->r,&w->v);
if (w->v==0) change(1,1,n,w->l,w->r);
}
for (int i=1;i<=n;i++){
seq[i]=ask(1,1,n,i)^1;//如果序列的值为1那么进行离散化
if (seq[i]==1){hash[i]=++cnt;anti[cnt]=i;}
}
if (ext[1]==n-k){//这个特判是必须的
for (int i=1;i<=n;i++)
if (seq[i]==1) printf("%d\n",i);
return 0;
}
get_pre_nxt();
rebuild();
get_f_g();
for (int i=1;i<=tot;i++){
if (f[i]!=f[i-1]+1) continue;//这一句必须加不然会出错
if (q[i].l==q[i].r){
ans[++ans[0]]=anti[q[i].l];continue;
}//对于长度为1的区间的特判
int I,J,x=q[i].r-1;
I=Lsearch(1,tot,x);
J=Rsearch(1,tot,x);
if (f[J]+g[I]+1>k) ans[++ans[0]]=anti[q[i].r];
}
if (ans[0]==0) {printf("-1\n");return 0;}
for (int i=1;i<=ans[0];i++) printf("%d\n",ans[i]);
return 0;
}


偏偏在最后出现的补充说明

一开始递推f和g的时候犯了一个错误就是直接用i+1或i-1来更新g[i]和f[i]了,但i+1和i-1的那个端点可能并没有在前面被选做放忍者的点,就造成了错误的更新,应该用一个变量记录最后一个放在了哪个位置。还有一个错误就是在筛选区间的时候把仅剩一个点的区间也直接特判掉计入答案了,这样一是会出现很多的重复答案最后需要去重,二是如果把这些区间提前筛掉后面的判断包含区间的步骤里就会少去掉很多应该去掉的区间,就会导致错误。这个特判可以在枚举区间的时候进行。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息