线段树(经典题)
离散化+成段更新+区间统计
题意:先输入case数,每个case输入n,表示下面有n个海报,每行就是海报的左右坐标,第i个海报的颜色为i。一面墙长度固定为10000000,问这些海报贴上去后能看到多少种颜色
这个问题的难处其实是怎么离散化(也就是映射,映射后就是简单的线段树整段更新,最后区间询问)。今天第一次学离散化,说说个人的解法
方法是和别人的大概一样的,可能代码实现有些不同,我的代码可能空间开销更大些(后来查看代码,很多空间能省略,但是下面的代码是我最原始的代码,并没有后来的优化,就以此来讲解)。
int s[MAXN][2]; 保存原始的数据,[0]是起点坐标,[1]是终点坐标,那么一共产生2*n个端点
struct point
{
int a,n,f; //端点坐标,属于哪条线段,起点或者终点
}p[2*MAXN];
所以把2*n个端点逐一放入p数组中,并且要记录这个端点是来自哪条线段(n这个域),在这条线段中是起点还是终点(f这个域)
然后对p数组排序,以端点坐标大小排序(a这个域)
接下来是映射,例如排序后的结果为
10,21,38,40,40,59
映射为
1,2,3,4,4,5
也就是说按数字大小映射,而且也可以发现,最后的5其实也代表了有多少个不同的数字
这个映射的结果一定包含[1,m]的所有整数,等下我们要建线段树的时候,总区间的长度就是[1,m]
我RE了很多次,就是搞错了m的大小范围,m最大可以去到80000(60000也行),80000怎么来的,是2*10000*4
因为海报的个数最多10000,有20000个端点,极端情况下,这20000个点都不一样,映射过去的话,就是[1,20000],也就是m最大可以是m
回想一般情况下,线段树的总区间为[1,m]的话,我们开辟线段树的数组一般为4*m(其实开到3*m就足够了),所以这就是为什么80000的原因
说回头,我们扫过p数组的时候,没得到一个点,就先映射它的坐标(和它前面那个端点坐标比较是否不同,不同的话,就是一个新的点,具体看代码),然后看它是来自哪条线段(映射后同样是这条线段),再看它在原线段中是起点还是终点,然后相对应地记录现在线段的起点和终点
最后映射后的线段信息全部在下面的数组中
struct interval //离散化后的线段
{
int l,r,col;
}in[MAXN];
得到了它,就可以建树,整段更新,查询了
/* 处理的主要问题就是如何映射,映射结束后,就是整段区间更新,最后询问总区间有多少种不同的颜色 */ #include <cstdio> #include <cstring> #include <algorithm> #define MAXN 10010 using namespace std; bool used[MAXN]; //询问时记录哪些颜色已经被计数 int s[MAXN][2] , N; //s数组记录原始数据,不过可以省略这个数组 struct point { int a,n,f; //端点坐标,属于哪条线段,起点或者终点 }p[2*MAXN]; struct interval //离散化后的线段 { int l,r,col; }in[MAXN]; struct segment //线段树结点 { int l,r,col,val; }t[8*MAXN]; int query(int a ,int b ,int rt) { int col=t[rt].col; if(t[rt].val) //单色 { if(!used[col]) //此颜色没被用过 { used[col]=1; return 1; } else //已经被用过 return 0; } int mid=(t[rt].l+t[rt].r)>>1; /* if(a>mid) //右孩子 return query(a,b,rt<<1|1); else if(b<=mid) //左孩子 return query(a,b,rt<<1); else //左右孩子 */ return query(a,mid,rt<<1)+query(mid+1,b,rt<<1|1); } void updata(int a ,int b ,int col ,int rt) { if(t[rt].val && t[rt].col==col) return ; //小剪枝,当前区间单色且与要更改的区间颜色相同则返回不用深入 if(t[rt].l==a && t[rt].r==b) //目标区间 { t[rt].col=col; t[rt].val=1; //单色 return ; } if(t[rt].val) //单色,传递给左右孩子 { t[rt<<1].val=t[rt<<1|1].val=t[rt].val; t[rt<<1].col=t[rt<<1|1].col=t[rt].col; t[rt].val=0; //修改为不是单色 } int mid=(t[rt].l+t[rt].r)>>1; if(a>mid) //访问右孩子 updata(a,b,col,rt<<1|1); else if(b<=mid) //访问左孩子 updata(a,b,col,rt<<1); else //左右均访问 { updata(a,mid,col,rt<<1); updata(mid+1,b,col,rt<<1|1); } } void build(int a ,int b ,int rt) { t[rt].l=a; t[rt].r=b; t[rt].val=0; t[rt].col=0; //不是单色的 if(a==b) return ; int mid=(a+b)>>1; build(a,mid,rt<<1); build(mid+1,b,rt<<1|1); } int cmp(struct point x ,struct point y) { return x.a<y.a?1:0; } int main() { int Case; scanf("%d",&Case); while(Case--) { scanf("%d",&N); for(int i=1; i<=N; i++) { scanf("%d%d",&s[i][0],&s[i][1]); p[2*i-1].a=s[i][0]; p[2*i-1].n=i; p[2*i-1].f=0; p[2*i].a=s[i][1]; p[2*i].n=i; p[2*i].f=1; } sort(p+1,p+2*N+1,cmp); p[0].a=p[1].a; //为了下面的循环要设一个特殊值 int m=1 , n ,f; //表示有多少个不同的数字,也就是映射使用的 for(int i=1; i<=2*N; i++) { n=p[i].n , f=p[i].f; if(p[i].a!=p[i-1].a) m++; if(!f) //起点 { in[n].l=m; in[n].col=n; } else //终点 { in[n].r=m; in[n].col=n; } } //for(int i=1; i<=N; i++) printf("[%d , %d] %d\n",in[i].l,in[i].r,in[i].col); /* 至此,离散化已经结束 接下来就是建树 然后利用已经保存好的区间去整段更新 最后加上一个查询即可 */ build(1,m,1); //整个线段树的区间长度[1,m] for(int i=1; i<=N; i++) updata(in[i].l, in[i].r, in[i].col, 1); memset(used,0,sizeof(used)); printf("%d\n",query(1,m,1)); } return 0; }