关于“猜数游戏”的反思 与分析理解
[USACO08JAN]Haybale Guessing G
题面翻译
给一个长度为 \(n\) 的数组 \(q\) 个条件,数组中的数字互不相同,每个条件格式形如 \(l_i,r_i,x_i\) 表示这个数组的区间 \([l_i,r_i]\) 内的最小值为 \(x_i\),输出最早与前面的条件有矛盾的条件的编号,如果所有条件都不发生矛盾,输出 \(0\)。
输入格式
第一行两个整数,分别是 \(n\) 和 \(q\)。
第二行至第 \(q+1\) 行,每行三个整 \(l_i,r_i,x_i\) 描述一个条件。
输出格式
仅一个整数,表示最早发生矛盾的条件的编号。如果所有条件都没有发生矛盾,输出 \(0\)。
样例 #1
样例输入 #1
20 4
1 10 7
5 19 7
3 12 8
11 15 12
样例输出 #1
3
常见二分,二分第一个编号。注意这里当初也走了弯路,不需判断\(mid\)是否为第一个,因为后面右端点还会左移,只要判断是否有矛盾就好了。
然后就是重点——并查集的使用
观察这一段二分内的代码:
bool check(int st)
{
sort(a+1,a+st+1,cmp);
for(int i=1;i<=n+1;i++)
f[i]=i;
int p=a[1].mi,li,ri,la,ra;
li=la=a[1].l;
ri=ra=a[1].r;
for(int i=2;i<=st;i++)
if(a[i].mi==p)
{
li=min(li,a[i].l);
la=max(la,a[i].l);
ri=min(ri,a[i].r);
ra=max(ra,a[i].r);
if(ri<la) return 0;
}
else
{
if(fi(la)>ri) return 0;
for(int j=fi(li);;j=fi(j+1))
if(j>ra) break;
else f[j]=f[j+1];
p=a[i].mi;
li=la=a[i].l;
ri=ra=a[i].r;
}
if(fi(la)>ri) return 0;
else return 1;
}
看起来很不知所谓,甚至像是错误的,其实是一种非常简练高效巧妙的写法。
通过分析可以得知,题目中语句之间的矛盾只有两种:
- 同一段区间所包含的最小值不同
- 同一最小值所处的区间不同
第二种情况很好判断,只要记录\(mi\)值相同时,最大左端点和最小右端点是否有交叉区间就行了。
而对于第一种情况,考虑我们将二分范围内的语句\(mi\)值大小按照从大到小排序,当我们知道前面较大的\(mi\)值覆盖范围时,只要判断目前的最小范围能否被前面覆盖,若能被覆盖,则矛盾。
因为前面所枚举的\(mi\)值都大于当前值,所以不管是哪个只要能覆盖住就行;又因为只需判断是否矛盾而不需判断个数,所以只要判断最可能的那一种情况就得了。
下面说并查集。
这里的并查集\(f[x]\)保存的是\(x\)点向右第一个没有被覆盖的点。
原理参照这里:
有如下问题:
长为\(n\)的序列,开始均无颜色,
有\(m\)个操作,每次将\(l\)到\(r\)的数全染成\(c\)色,
求最终的序列。
我们考虑将染色反着来,则一个数若被染色一次,
则这就是它的最终颜色,不会改变。
所以下次若在遇到这个数则需要跳过,
也就是对已经染色的区间\([l,r]\),我们令其中的所有数都有一个指针,
以指向右边第一个还没有操作的数。
于是我们令\(father[i]\)表示\(i\)右边(包括\(i\)本身)第一个没有染色的数,
将\(i\)染色后,令\(father[i] = find(i + 1)\),
之后就是正常的并查集操作了。
然后这题是个特例,染色虽然是正着来的,但染的颜色全是一种,
只存在\(0 - >1\),染一次则以后不变色,所以也可用这种方法做。
这里差不多也就是这样的情况了。
注:下面为对上面并查集使用代码的详解,请仔细思考后阅读。
那么对于这一段:
if(fi(la)>ri) return 0;
其原理何在?为什么要用\(la\)右边第一个未被覆盖的点与\(ri\)比较呢?
\(la\)是所有\(mi\)值等于\(p\)的区间的最大左端点,\(ri\)则是最小右端点。而上面我们已经判断过这些都是连续的区间(第二条判断)
if(ri<la) return 0;
所以,\([la,ri]\)就是目前所有区间的交集,也是\(mi\)值可能存在的最小区间。
所以,假如之前的区间覆盖住了这一交集,那么便矛盾:而假如连交集都没有覆盖住,那么更不可能覆盖住别的区间了。