前阵子颓废了一下,学了个整体二分和cdq分治,感觉还是比较神奇的一个东西,写篇博客总结一下。
关于整体二分
http://www.cnblogs.com/zig-zag/archive/2013/04/18/3027707.html先膜拜下Zig_zag神犇。。。
整体二分是一种非常神奇的离线算法。
带修改的区间查询第k小,树套树、分块什么的其实可以轻松秒掉,但是代码量都非常巨大,而整体二分在允许离线的情况下,可以完美的在O(n log^2 n)的时间内解决这个问题,且代码量极短。
考虑对于单个询问,我们可以二分答案,通过判断比mid小的数有多少个来缩小范围。对于多个询问我们同样可以这样做,每次二分将操作序列分成两个序列,对于询问操作,如果比二分的mid小的数大于k-1个,那么就将它放到左序列,否则就将它放到右序列;对于修改操作,如果修改的数小于mid,即对于左序列的二分有影响,就将它放到左序列,否则就将它放到右序列。当
修改的数小于mid时,我们记录一下其对右序列询问的贡献,这样再对左序列和右序列分别进行二分就可以了。每次插入和询问操作,很容易就想到用树状数组来维护,于是就完美的解决了这个代码量巨大的问题。
注意,二分前必须保证操作序列按照时间顺序排序。
/**************************************************************
Problem: 1901
User: Oxer
Language: C++
Result: Accepted
Time:332 ms
Memory:3176 kb
****************************************************************/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<cerrno>
using namespace std;
struct yts
{
int x,y,cur,id,op,k;
}q[30010],t[30010];
int tmp[30010],a[30010],ans[30010];
int c[30010];
bool mark[30010];
int n,m,cnt,num;
int lowbit(int x)
{
return x&(-x);
}
void add(int i,int x)
{
while (i<=n)
{
c[i]+=x;
i+=lowbit(i);
}
}
inline void assert(int x){
if (x){
int z=12;
int x= z/(z-12);
}
}
int query(int i)
{
int tmp=0;
while (i>0)
{
tmp+=c[i];
i-=lowbit(i);
}
return tmp;
}
void solve(int l,int r,int L,int R)
{
if (l>r) return;
if (L==R)
{
for (int i=l;i<=r;i++)
if (q[i].op==3) ans[q[i].id]=L;
return;
}
int mid=(L+R)/2;
for (int i=l;i<=r;i++)
{
if (q[i].op==1 && q[i].y<=mid) add(q[i].x,1);
if (q[i].op==2 && q[i].y<=mid) add(q[i].x,-1);
if (q[i].op==3) tmp[i]=query(q[i].y)-query(q[i].x-1);
}
for (int i=l;i<=r;i++)
{
if (q[i].op==1 && q[i].y<=mid) add(q[i].x,-1);
if (q[i].op==2 && q[i].y<=mid) add(q[i].x,1);
}
int ct=0;
for (int i=l;i<=r;i++)
{
if (q[i].op==3)
{
if (q[i].cur+tmp[i]>q[i].k-1) ct++,mark[i]=1;
else q[i].cur+=tmp[i],mark[i]=0;
}
else
{
if (q[i].y<=mid) ct++,mark[i]=1;
else mark[i]=0;
}
}
int l1=l,l2=l+ct;
for (int i=l;i<=r;i++)
if (mark[i]) t[l1++]=q[i];
else t[l2++]=q[i];
for (int i=l;i<=r;i++) q[i]=t[i];
solve(l,l1-1,L,mid);
solve(l1,l2-1,mid+1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
q[++num].x=i;q[num].y=a[i];q[num].op=1;q[num].id=0;
}
for (int i=1;i<=m;i++)
{
char opt[5];
scanf("%s",opt);
if (opt[0]=='C')
{
int x,y;
scanf("%d%d",&x,&y);
q[++num].op=2;q[num].x=x;q[num].y=a[x];q[num].id=0;
q[++num].op=1;q[num].x=x;q[num].y=y;q[num].id=0;
a[x]=y;
}
else
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
q[++num].op=3;q[num].x=x;q[num].y=y;q[num].k=z;q[num].id=++cnt;
}
}
solve(1,num,0,1000000000);
for (int i=1;i<=cnt;i++) printf("%d\n",ans[i]);
return 0;
}
给树套树跪了(至今还不会树套树)。。。和例题差不多,加了些小操作,用区间修改区间查询搞一搞就好了,还现学了个树状数组区间修改区间查询,真心快呀。
跪一下写线段树的神犇。。。
/**************************************************************
Problem: 3110
User: Oxer
Language: C++
Result: Accepted
Time:1116 ms
Memory:7620 kb
****************************************************************/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
using namespace std;
struct yts
{
int x,y,op,cur,k,id;
}q[100010],t[100010];
int tmp[100010],ans[100010],c[2][100010];
bool mark[100010];
int n,m,num,cnt;
int lowbit(int x)
{
return x&(-x);
}
void modify(int o,int i,int x)
{
while (i<=n)
{
c[o][i]+=x;
i+=lowbit(i);
}
}
int query(int o,int i)
{
int tmp=0;
while (i>0)
{
tmp+=c[o][i];
i-=lowbit(i);
}
return tmp;
}
void solve(int l,int r,int L,int R)
{
if (l>r) return;
if (L==R)
{
for (int i=l;i<=r;i++)
if (q[i].op==2) ans[q[i].id]=L;
return;
}
int mid=(L+R)/2;
for (int i=l;i<=r;i++)
{
if (q[i].op==1)
{
if (q[i].k>mid)
{
modify(0,q[i].x,1);modify(1,q[i].x,q[i].x-1);
modify(0,q[i].y+1,-1);modify(1,q[i].y+1,-q[i].y);
}
}
else tmp[i]=(query(0,q[i].y)*q[i].y-query(1,q[i].y))-(query(0,q[i].x-1)*(q[i].x-1)-query(1,q[i].x-1));
}
for (int i=l;i<=r;i++)
if (q[i].op==1 && q[i].k>mid)
{
modify(0,q[i].x,-1);modify(1,q[i].x,1-q[i].x);
modify(0,q[i].y+1,1);modify(1,q[i].y+1,q[i].y);
}
int ct=0;
for (int i=l;i<=r;i++)
{
if (q[i].op==2)
{
if (q[i].cur+tmp[i]>q[i].k-1) mark[i]=1;
else ct++,q[i].cur+=tmp[i],mark[i]=0;
}
else
{
if (q[i].k>mid) mark[i]=1;
else ct++,mark[i]=0;
}
}
int l1=l,l2=l+ct;
for (int i=l;i<=r;i++)
if (!mark[i]) t[l1++]=q[i];
else t[l2++]=q[i];
for (int i=l;i<=r;i++) q[i]=t[i];
solve(l,l1-1,L,mid);
solve(l1,l2-1,mid+1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&q[i].op,&q[i].x,&q[i].y,&q[i].k);
if (q[i].op==2) q[i].id=++cnt;
}
solve(1,m,-n,n);
for (int i=1;i<=cnt;i++) printf("%d\n",ans[i]);
return 0;
}
bzoj2527 http://www.lydsy.com/JudgeOnline/problem.php?id=2527
这也是一道比较经典的整体二分题目,二分一下时刻,每次二分把它保证操作的时刻<=mid,然后查询一下就可以了。
小技巧:在操作序列最后加入一个极限操作,保证边界不会溢出。
hzwer学长的代码风格确实精简。。。
/**************************************************************
Problem: 2527
User: Oxer
Language: C++
Result: Accepted
Time:12952 ms
Memory:19324 kb
****************************************************************/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
#include<vector>
#define maxn 300010
#define ll long long
#define inf 1000000000
using namespace std;
ll c[maxn];
int l[maxn],r[maxn],val[maxn],p[maxn];
int tmp[maxn],id[maxn],ans[maxn];
bool mark[maxn];
int n,m,k,T;
vector<int> a[maxn];
int lowbit(int x)
{
return x&(-x);
}
void add(int i,int x)
{
while (i<=m)
{
c[i]+=x;
i+=lowbit(i);
}
}
ll query(int i)
{
ll q=0;
while (i>0)
{
q+=c[i];
i-=lowbit(i);
}
return q;
}
void opera(int i,int f)
{
if (l[i]<=r[i]) add(l[i],f*val[i]),add(r[i]+1,-f*val[i]);
else add(1,f*val[i]),add(l[i],f*val[i]),add(r[i]+1,-f*val[i]);
}
void solve(int l,int r,int L,int R)
{
if (l>r) return;
if (L==R)
{
for (int i=l;i<=r;i++)
ans[id[i]]=L;
return;
}
int mid=(L+R)/2;
while (T<=mid) {T++;opera(T,1);}
while (T>mid) {opera(T,-1);T--;}
ll tot=0;int now,cnt=0;
for (int i=l;i<=r;i++)
{
tot=0;now=id[i];
for (int j=0;j<a[now].size();j++)
{
tot+=query(a[now][j]);
if (tot>=p[now]) break;
}
if (tot>=p[now]) mark[i]=1,cnt++;
else mark[i]=0;
}
int l1=l,l2=l+cnt;
for (int i=l;i<=r;i++)
if (mark[i]) tmp[l1++]=id[i];
else tmp[l2++]=id[i];
for (int i=l;i<=r;i++) id[i]=tmp[i];
solve(l,l1-1,L,mid);
solve(l1,l2-1,mid+1,R);
}
int main()
{
scanf("%d%d",&n,&m);
for (int i=1;i<=m;i++)
{
int x;
scanf("%d",&x);
a[x].push_back(i);
}
for (int i=1;i<=n;i++) scanf("%d",&p[i]);
scanf("%d",&k);
for (int i=1;i<=k;i++)
scanf("%d%d%d",&l[i],&r[i],&val[i]);
for (int i=1;i<=n;i++) id[i]=i;
k++;
l[k]=1;r[k]=m;val[k]=inf;
solve(1,n,1,k);
for (int i=1;i<=n;i++)
if (ans[i]!=k) printf("%d\n",ans[i]);
else printf("NIE\n");
return 0;
}
bzoj2738 http://www.lydsy.com/JudgeOnline/problem.php?id=2738
第一眼看到数据范围吓傻了。。。什么?!二维可持久化线段树?!原谅我太弱了。。。仔细一看还是比较水的,插入的时候按照权值排序,二分一下答案,每次保证插入的权值<=mid,然后写个二维树状数组查询就好了。应该算是比较经典的例题了。
/**************************************************************
Problem: 2738
User: Oxer
Language: C++
Result: Accepted
Time:11524 ms
Memory:7276 kb
****************************************************************/
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<iostream>
using namespace std;
struct yts
{
int x,y,val;
}a[510*510];
struct yts1
{
int x1,y1,x2,y2,k;
}q[60010];
int c[510][510];
int tmp[60010],id[60010],ans[60010];
bool mark[60010];
int n,m,T,cnt;
bool cmp(yts x,yts y)
{
return x.val<y.val;
}
int lowbit(int x)
{
return x&(-x);
}
void add(int k,int x)
{
for (int i=a[k].x;i<=n;i+=lowbit(i))
for (int j=a[k].y;j<=n;j+=lowbit(j))
c[i][j]+=x;
}
int get(int x,int y)
{
int tmp=0;
for (int i=x;i>0;i-=lowbit(i))
for (int j=y;j>0;j-=lowbit(j))
tmp+=c[i][j];
return tmp;
}
int query(int i)
{
int x1=q[i].x1,y1=q[i].y1,x2=q[i].x2,y2=q[i].y2;
return get(x2,y2)-get(x2,y1-1)-get(x1-1,y2)+get(x1-1,y1-1);
}
void solve(int l,int r,int L,int R)
{
if (l>r) return;
if (L==R) return;
int mid=(L+R)/2;
while (T<cnt && a[T+1].val<=mid) T++,add(T,1);
while (a[T].val>mid) add(T,-1),T--;
int cnt=0;
for (int i=l;i<=r;i++)
{
int x=query(id[i]);
if (x>q[id[i]].k-1) mark[i]=1,ans[id[i]]=mid,cnt++;
else mark[i]=0;
}
int l1=l,l2=l+cnt;
for (int i=l;i<=r;i++)
if (mark[i]) tmp[l1++]=id[i];
else tmp[l2++]=id[i];
for (int i=l;i<=r;i++) id[i]=tmp[i];
solve(l,l1-1,L,mid);
solve(l1,l2-1,mid+1,R);
}
int main()
{
scanf("%d%d",&n,&m);
int mx=0;
for (int i=1;i<=n;i++)
for (int j=1;j<=n;j++)
{
a[++cnt].x=i;a[cnt].y=j;
scanf("%d",&a[cnt].val);
mx=max(a[cnt].val,mx);
}
sort(a+1,a+cnt+1,cmp);
for (int i=1;i<=m;i++) scanf("%d%d%d%d%d",&q[i].x1,&q[i].y1,&q[i].x2,&q[i].y2,&q[i].k);
for (int i=1;i<=m;i++) id[i]=i;
solve(1,m,0,mx+1);
for (int i=1;i<=m;i++) printf("%d\n",ans[i]);
return 0;
}
总结:对于询问和修改混合的题目,要记录一下前面的修改操作对后面的贡献。对比一下,整体二分其实是优于许多高端的数据结构的,但是出难题也是比较难想的。
附:树状数组的区间修改、区间查询
记i-n的共同增量为delta[i],前i项的和为sum[i],则query(l,r)=sum[r]-sum[l-1],现在的关键是怎样求sum[i]。
sum[i]=sigma(a[1……i])+i*delta[1]+(i-1)*delta[2]+……+1*delta[i]
=sigma(a[1……i])+sigma(delta[j]*(i+1-j))
=sigma(a[1……i])+(i+1)*sigma(delta[j])-sigma(j*delta[j])
这样只要维护一下sigma(delta[i])和sigma(i*delta[i]),问题就解决了。