题目源自 XTU http://202.197.224.59/OnlineJudge2/index.php/Problem/read/id/1170
题意: 有编号为1到10^9个盒子,每个盒子为空或装部分球,现在给K个区间,形如[ L, R , num ] 表示编号为 L到R的盒子中至少有num个球,求所有盒子装球的最小数量和。
其实不难想到解法,我们依据c的值从小到大覆盖指定区间,若出现当前区间被完整覆盖时,则意味着冲突,应该为 "Error",否则求和。重要的是如何实现,区间覆盖问题,是线段树(segment Tree)的强项。
我们且先,假定盒子数量在承受范围内,例如说 n = 10^6这种,然后进行N个区间用对应的 Ci覆盖,再统计,即可解决本题。但是考虑到盒子数量为10^9,仅仅N=10^5个区间,极端情况下也只会出现 2*N 个不同端点, 离散化这种基本的做法就不提了。
重要的是,离散化后的统计问题,举个简单例子, 比如一个区间是 [ 10, 100 ], C = 2, 离散化后为 [ 1, 2 ], 那么对于覆盖操作,我们应该覆盖 [1,2]值为 Ci, 假定还有一个是 [10,200],C = 1, 离散化后区间为 [1,3], 那么我们此时已经覆盖了 1,2,剩下3, 则显然是覆盖3, 然后我们实际已经得出 val[1] = 2, val[2] = 2, val[3] = 1. 那么我们统计的时候该怎么办呢?
可能有的人会认为, 对于 a[2] - a[1] = 100-10 , a[3] - a[2] = 200 - 100, 然后统计即可. 那么, 假定我们覆盖是 [200,300],而非 [10,200],此时离散化是 [3,4], 那我们如何知道区间[2,3]是否连续呢. 这个问题令人觉得麻烦. 当然有的人会说, 可以在 ai 与 aj 之间插入一个中点 ax, 用来处理 [ai,ax], [ax,aj] , 那假定实际区间为 [1,3],[2,4] ,那此时,也无法识别区间连续问题.
由上面分析, 区间覆盖以及统计的时, 端点的问题, 令人棘手. 既然考虑端点不好实现, 那么我们可以将其转换成 线段来处理, 例如对于一个点 [1,1] ,我们将其看作[1,2), 其长度为
2-1 = 1, 直观的看法是 将闭合区间转换成 左闭右开, 这样关注的就是线段, 而非点. 那这个时候我们再来考虑线段树如何实现以及统计:
看题目给的第三组例子,来作为示范:
3
3 4 1
5 6 10
4 5 100
则三个区间 [3,4], [5,6], [4,5] 转换成线段 [3,5), [5,7), [4,6) ,区间长度分别为 2, 2, 2 . 对应C为1,10,100. 则此时出现的端点为{ 3,4,5,6,7 },离散化后为{ 1,2,3,4,5 }, 然后根据C值从大到小覆盖即可.
到这里问题用线段树来搞,也差不多了. 然后再讨论讨论 此类线段树 的实现细节问题:
这里用"线段"树,而非点树, 另外, 写法也有待注意, "线段"树的写法不能使用 HH的那种, 在分支结构中 HH写法为:
void update( int rt,int l,int r,int a,int b ,int c){ // (a,b)为更新区间 ....
if( a <= l && r <= b ) ... int m = (l+r)>>1; if( a <= m ) update( rt, l, m, a, b, c ); if( m <= b ) update( rt, m, r, a, b, c ); }
此时考虑,其分解思路是,将 [1,n]中属于[a,b]的划分成满足 (a<=l && r<=b)的片段求和,(a,b)始终不改变, 其会出现 b > r的情形, 假定 l = 1, r = 3, 查询区间( a = 2, b = 3 ), 则 左边递归下去会成为 [1,2],而右边会递归成[2,3] ,显然右边会结束, 而左边已经是单位线段了,此时永远不会出现 a <= l && r <= b 这样的结束情况, 那么等待你的只能是死循环. 所以这样写显然不可行.
换种思维分解, 因为最初 1 <= a , b <= n, 则我们可以考虑分三种情况划分. m = (1+n)/2 :
m <= a, 则此时 区间[a,b]位于 [m,n]之间, 则我们只递归处理右区间 [m ,r]即可.
b <= m, 则此时 区间[a,b]位于 [1,m]之间, 则我们只递归处理左区间 [1,m]即可.
a < m < b , 则此时 区间 [a,b]应该分解成 [ 1, m ], [m, n] 的组合情况.
代码实现:
void update(int rt,int l,int r,int a,int b,int c){ if( a <= l && r <= b ) ... int m = (l+r)>>1; if(m <= a) update( rch, a, b, c ); else if(b <= m) update( lch, a, b, c ); else update(lch,a,m,c), update(rch,m,b,c); }
好了. 用线段树来实现离散化后的区间操作与涉及端点的问题已经解决完了. 完整代码实现:
#include<cstdio> #include<cstring> #include<cstdlib> #include<algorithm> #include<map> using namespace std; const int inf = 0x3f3f3f3f; const int N = (int)1e6+10; typedef long long LL; #define lch rt<<1,l,m #define rch rt<<1|1,m,r struct Query{ int l,r,c; void input(){ scanf("%d%d%d",&l,&r,&c); r++; } bool operator < (const Query &tmp) const{ return c > tmp.c; } }Q[N]; int Min[N<<2], tar[N<<2]; int n, Q_num, val[N]; map<int,int> mp; void push_up(int rt){ Min[rt] = min( Min[rt<<1], Min[rt<<1|1] ); } void push_down(int rt){ if( tar[rt] ){ int lc = rt<<1, rc = rt<<1|1; if( Min[lc] == 0 ) Min[lc] = tar[rt], tar[lc] = tar[rt]; if( Min[rc] == 0 ) Min[rc] = tar[rt], tar[rc] = tar[rt]; tar[rt] = 0; } } void mktree(int rt,int l,int r){ Min[rt] = tar[rt] = 0; if( r-l == 1 ) return; int m = (l+r)>>1; mktree( lch ), mktree( rch ); push_up(rt); } void update(int rt,int l,int r,int a,int b,int c){ if( r-l > 1 ) push_down(rt); //因为c从大到小覆盖,避免影响需要先更新下去 if( a <= l && r <= b ){ if( Min[rt] == 0 ) Min[rt] = c, tar[rt] = c; return; } int m = (l+r)>>1; if(m <= a) update( rch, a, b, c ); else if(b <= m) update( lch, a, b, c ); else update(lch,a,m,c), update(rch,m,b,c); push_up(rt); } int query_min(int rt,int l,int r,int a,int b){ if( r-l > 1 ) push_down(rt); if(a <= l && r <= b){ return Min[rt]; } int m = (l+r)>>1; int t = inf; if(m <= a) return query_min( rch, a, b ); else if(b <= m) return query_min( lch, a, b); else return min( query_min(lch,a,m), query_min(rch,m,b) ); } LL query_sum(int rt,int l,int r,int a,int b){ if( r-l > 1) push_down(rt); if( a <= l && r <= b ) return 1LL*Min[rt]*(val[r]-val[l]); int m = (l+r)>>1; if(b <= m) return query_sum(lch,a,b); else if(m <= a) return query_sum(rch,a,b); else return query_sum(lch,a,m) + query_sum(rch,m,b); } void print(){ for(int i = 1; i <= 7; i++) printf("rt: %d, Min = %d, tar = %d\n", i, Min[i], tar[i] ); } int main(){ freopen("1.in","r",stdin); int T; scanf("%d", &T); while( T-- ){ scanf("%d", &Q_num); mp.clear(); n = 0; for(int i = 0; i < Q_num; i++){ Q[i].input(); val[n++] = Q[i].l; val[n++] = Q[i].r; } sort( val, val+n ); sort( Q, Q+Q_num ); n = unique( val, val+n ) - val; for(int i = 0; i < n; i++) mp[ val[i] ] = i; mktree( 1, 0, n-1 ); bool flag = true; for(int i = 0; i < Q_num && flag; i++){ int a = mp[ Q[i].l ], b = mp[ Q[i].r ]; int m = query_min( 1, 0, n-1, a, b ); if( Q[i].c >= m ) update( 1, 0, n-1, a, b, Q[i].c ); else flag = false; } if( flag ){ LL res = 0; for(int i = 0; i < n-1; i++) res += query_sum( 1, 0, n-1, i, i+1 ); printf("%lld\n", res ); } else puts("Error"); } return 0; }
理论还是要依靠上面的将闭合区间转换成左闭右开的段落区间. 然后模拟覆盖, 比如我们覆盖了 [1,4)区间, 则应该将1,2,3顶点的父节点指向4, 以便以后若再次处理当前区间内端点时, 可以直接通过找其父节点到右边界 4去, 这样就可以不用重复的判定当前点是否已经被更新, 这样使用并查集合并, 并利用路径压缩保证在 平摊下O(logN)的时间复杂度.
然后判定冲突, 因为我们是根据C的值从大到小操作, 则每次对于覆盖的区间 [ a, b ] , 查询其是否发生过更改, 若没有, 则意味着这一段区间已经被覆盖了更大的C, 所以必定不合法.
具体代码实现:
#include<cstdio> #include<cstdlib> #include<cstring> #include<algorithm> using namespace std; const int N = (int)1e6+10; typedef long long LL; int mp[N]; int n, C[N]; void add(int x,int c){ for(;x<N;x+=x&(-x)) C[x]+=c; } int sum(int x){ int s = 0; for(;x>=1;x-=x&(-x)) s += C[x]; return s; } struct node{ int l, r, c; bool operator < (const node &tmp) const{ return c > tmp.c; } }Q[N]; int idx[N], sz[N], st[N], list[N], cnt; int find(int x){ return st[x]==x?x:(st[x]=find(st[x])); } int getod(int c){ return lower_bound(idx,idx+n,c)-idx+1; } int main(){ freopen("1.in","r",stdin); int T; scanf("%d", &T); while( T-- ){ int Q_num; LL ans = 0; scanf("%d", &Q_num); n = 0; cnt = 0; for(int i = 0; i < Q_num; i++){ scanf("%d%d%d", &Q[i].l, &Q[i].r, &Q[i].c); Q[i].r++; idx[n++] = Q[i].l; idx[n++] = Q[i].r; } sort( idx, idx+n ); n = unique( idx, idx+n ) - idx; for(int i = 1; i <= n; i++){ st[i] = i; sz[i] = idx[i]-idx[i-1]; } sort( Q, Q+Q_num ); memset( C, 0, sizeof(C)); bool flag = true; int j; for(int i = 0; (i<Q_num)&&flag; i = j ){ cnt = 0, j = i+1; while( (j<Q_num)&&(Q[j].c==Q[j-1].c) ) j++; for(int k = i; k < j; k++){ int a = getod( Q[k].l ); int b = getod( Q[k].r ); for(int t = find(a); t < b; t = find(t+1) ){ list[cnt++] = t; st[t] = find(t+1); //printf("sz = %d, c = %d\n", sz[t], Q[i].c ); ans += 1LL*Q[i].c*sz[t]; } } for(int k = 0; k < cnt; k++) add( list[k], 1 ); for(int k = i; k < j; k++){ int a = getod( Q[k].l ); int b = getod( Q[k].r ); if( sum(b)-sum(a-1) == 0 ) flag = false; } for(int k = 0; k < cnt; k++) add( list[k], -1 ); } if( flag ) printf("%lld\n", ans ); else puts("Error"); } return 0; }