## [$P3740$ $[HAOI2014]$ 贴海报](https://www.luogu.com.cn/problem/P3740) ### 一、题目描述 $Bytetown$城市要进行市长竞选,所有的选民可以畅所欲言地对竞选市长的候选人发表言论。为了统一管理,城市委员会为选民准备了一个张贴海报的$electoral$墙。 张贴规则如下: 1. $electoral$墙是一个长度为$N$个单位的长方形,每个单位记为一个格子; 2. 所有张贴的海报的高度必须与$electoral$墙的高度一致的; 3. 每张海报以“$A$ $B$”表示,即从第$A$个格子到第$B$个格子张贴海报; 4. 后贴的海报可以覆盖前面已贴的海报或部分海报。 现在请你判断,张贴完所有海报后,在$electoral$墙上还可以看见多少张海报。 #### 输入格式 第一行: $N$ $M$ 分别表示$electoral$墙的长度和海报个数 接下来$M$行: $A_i B_i$ 表示每张海报张贴的位置 #### 输出格式 输出贴完所有海报后,在$electoral$墙上还可以看见的海报数。 #### 样例输入 #1 ``` 100 5 1 4 2 6 8 10 3 4 7 10 ``` #### 样例输出 #1 ``` 4 ``` #### 提示 ![](https://cdn.luogu.com.cn/upload/pic/5209.png) 【约束条件】 $0<= N <= 10000000, 1<=M<=1000 , 1<= A_i <= B_i <=10000000$ 所有的数据都是整数。数据之间有一个空格 ### 二、解题思路 这是一道 **区间染色** 类的题目。 **可以用线段树来维护一个$bool$标记,来记录该点所辖区间是否完整被覆盖。** 我们可以 **从右向左遍历** 全部的海报所辖区间(因为贴海报是后面的覆盖前面的),每次将这个区间放入线段树,即将线段树中的$[l,r]$区间内所有为$false$的地方改为$true$,但是如果该区间内的点已经全部被标记过了,那么说明这张海报不会露出来。 最后统计所有露出来的海报即为最终答案。 而由于区间范围比较大$<=1e7$,要 **离散化** 区间。 需要注意的是,由于是 **区间覆盖** 问题,所以普通离散化会出问题,比如: $[1,6],[1,3],[5,6]$离散化后会变为$[1,4],[1,2],[3,4]$,这样一来,原来第一个区间就会被完全覆盖,其实人家的$[4]$是可以被看到的,没有海报可以盖住$4$,你离散化后就不对了,**离散化失败**。 这是因为,离散化操作让不相邻的点变得相邻了,这在普通问题中没有什么影响,但在 **区间覆盖** 问题上就变得很关键了。 所以我们要在离散化数组中, **插入$r[i]+1$,防止后续点不相邻的点在离散化后和它相邻** 。 ### $Code$ ```cpp {.line-numbers} #include using namespace std; const int N = 1e3 + 10; int n, m; int x[N], y[N]; bool flag; // 当前枚举到的海报是不是能被看到 // 推荐版本:静态离散化+区间覆盖 #define ls (u << 1) #define rs (u << 1 | 1) #define mid ((l + r) >> 1) struct Node { int l, r; bool color; // 是不是所辖区间被完整覆盖 } tr[N << 2]; void pushup(int u) { tr[u].color = tr[ls].color && tr[rs].color; // 只有左儿子被完整覆盖,并且,右儿子被完整覆盖,自己才是被完整覆盖 } void build(int u, int l, int r) { tr[u].l = l, tr[u].r = r; if (l == r) return; build(ls, l, mid); build(rs, mid + 1, r); } void modify(int u, int L, int R) { int l = tr[u].l, r = tr[u].r; if (l > R || r < L) return; if (tr[u].color) return; // 如果本区间原来就已经被覆盖过了,那么返回,也就不会在这段区间使得flag=true,也就是这个区域是看不到的 if (l >= L && r <= R) { // 如果以前没有被覆盖过,并且,本次本区域被完整覆盖,也就是第一次覆盖 flag = true; // 当前枚举到的海报可以被看到 tr[u].color = true; // 记录本区域被完整覆盖 return; } modify(ls, L, R), modify(rs, L, R); pushup(u); } int b[N << 1], bl; // 离散化数组 int main() { #ifndef ONLINE_JUDGE freopen("P3740.in", "r", stdin); #endif // 加快读入 ios::sync_with_stdio(false), cin.tie(0); cin >> n >> m; for (int i = 1; i <= m; i++) { cin >> x[i] >> y[i]; b[++bl] = x[i]; b[++bl] = y[i]; b[++bl] = y[i] + 1; // 区间覆盖问题的解决之道 } // 排序+去重 (要注意b数组的范围,bl代表了它的上界!) sort(b + 1, b + 1 + bl); int tot = unique(b + 1, b + 1 + bl) - b - 1; // 构建线段树 build(1, 1, tot); int res = 0; for (int i = m; i; i--) { // 倒序 flag = false; // 当前枚举到的海报是不是能被看到,默认是不能被看到 int L = lower_bound(b + 1, b + 1 + tot, x[i]) - b; int R = lower_bound(b + 1, b + 1 + tot, y[i]) - b; modify(1, L, R); // 覆盖掉【L,R】 if (flag) res++; // 如果当前枚举到的海报可以被看到,那么多了一张 } // 输出答案 printf("%d\n", res); return 0; } ``` ### 三、珂朵莉树解法 不懂珂朵莉树的同学可以看看$oi$ $wiki$的[介绍](https://oi-wiki.org/ds/odt/) 简单来说珂朵莉树的原理就是维护拥有相同元素的区间 ```cpp {.line-numbers} struct Node { int l, r; mutable int v; const bool operator<(const Node &b) const{ return l < b.l; } }; ``` 具体到本题就是将一整张海报的区间当作一个节点,每个不同的海报的区间染上不同的颜色,加入新的海报就相当于珂朵莉树的 $assign$ 操作 最后暴力的扫一遍$[1, 0]$ (整个墙)这个区间 记录访问到的颜色的种类(初始时整个墙围一整个区间 颜色$v$为$0$) ```cpp {.line-numbers} #include using namespace std; const int N = 1010; // 用于记录颜色的集合 set colors; // 柯朵莉树模板 struct Node { int l, r; mutable int v; bool operator<(const Node &b) const { return l < b.l; } }; set s; // 分裂:[l,x-1],[x,r] set::iterator split(int x) { auto it = s.lower_bound({x}); if (it != s.end() && it->l == x) return it; // 如果x就是一个区间的开头,就不用切割,直接返回这个区间 it--; if (it->r < x) return s.end(); // x太大,超过了最后一个区间的右端点,直接返回s.end() int l = it->l, r = it->r, v = it->v; s.erase(it); // 删掉旧区间 s.insert({l, x - 1, v}); // 拆分成两个区间 return s.insert({x, r, v}).first; // 返回新插入区间位置的迭代器 } void add(int l, int r, int v) { auto R = split(r + 1), L = split(l); for (auto it = L; it != R; it++) it->v += v; } void assign(int l, int r, int v) { auto R = split(r + 1), L = split(l); s.erase(L, R); s.insert({l, r, v}); } // 计算有多少种可见颜色 int countColor(int l, int r) { auto R = split(r + 1), L = split(l); for (auto it = L; it != R; it++) if (it->v) colors.insert(it->v); return colors.size(); } int main() { // 加快读入 ios::sync_with_stdio(false), cin.tie(0); int n, m; cin >> n >> m; // 初始时整个区间颜色id为0 s.insert({1, n, 0}); // m个区间统一赋值为i for (int i = 1; i <= m; i++) { int l, r; cin >> l >> r; assign(l, r, i); // 给海报区间染色 每个区间颜色不同 } // 输出有几种可见的颜色 cout << countColor(1, n) << endl; return 0; } ```