线段树上的舞蹈 —— Codeforces 1440 E

线段树上的舞蹈 —— Codeforces 1440 E

题目链接:

Codeforces 1440 E

题意描述://今晚先不写
分析:

从最主要的操作入手:即从左向右贪心选择的操作。

首先很容易想到一种类似的操作:即从左向右选择一段连续的区间,使得和不超过sum。(实际上我一开始也把题意理解成这样了)

这个的做法其实很简单,就是线段树上二分,具体来说:

如果我们能够O(1)获取前缀和的话,那么这种简化版的操作就是一个二分查找,实际上,即便没有前缀和,只要我们有线段树,就可以在线段树上进行二分,找到最末尾的位置。

因为线段树只能给我们带来区间内的信息,所以这种二分本质上还是从左向右遍历线段树,在这个过程中我们会维护一个cur来记录在此之前的累加和。

所以我们的矛盾点就发生在 cur<=sum 而 cur + t[k] > sum的情况上,对这样的区间不能直接跳过,而应该继续深入地二分出那个临界点。

对于通常的区间查询,将区间 [L, R] 挂在线段树上,至多会得到\(O(lgn)\)个树上区间,而在这里我们只会对一个树上区间进行二分查找,故时间复杂度显然是\(O(lgn)\)的。

// sum: 区间和上限
ll query(int L, int R, int k, int l, int r, ll cur) {
	if(cur > sum) return 0;
 if(L <= l && r <= R) {
     if(t[k] + cur <= sum) { // 说明此段区间可以直接跳过
         cnt += r - l + 1; return t[k];
     } else if(l == r) {	// **
         pos = l; return t[k]; 
     }
 }
 int m = l + ((r - l) >> 1); ll ans = 0;
 Pushdown(k, m - l + 1, r - m);
 if(L <= m) ans += query(L, R, k << 1, l, m, cur);
 if(R > m) ans += query(L, R, k << 1 | 1, m + 1, r, cur + ans);
 return ans;
}

**:此时这个叶子节点满足cur <= sum,sum+t[k] > cur, 显然它正好是我们目标区间的下一个结点。

线段树上的二分就是这样写,不过离我们真正要解决的问题还有一段距离。

前文已经提到过,这种关于累加和的线段树上二分其实就是在对线段树从左到右遍历,只不过我们通过整块整块地跳过很多区间来将遍历的时间复杂度优化成\(O(lgn)\)

而我们想要解决的贪心选择其实也是一个从左到右遍历的过程(废话),因此我们想要在线段树上模拟这种遍历。

观察一下线段树二分的代码,发现其实只要稍加改动就可以模拟了,实际上,我们只需要使维护的cur永远不超过sum,即当某个区间加到cur上也不会超过sum(t[k] + cur <= sum)时,我们才让它加到cur上,否则我们深入下去二分查找临界点,只不过这次不再加上临界点的值,这样会使cur永远不超过sum,而累加的区间又正好符合贪心选择。

if(cur > sum) return 0;
if(L <= l && r <= R) {
 if(t[k] + cur <= sum) { // 1:这样的区间可以直接跳过
     cnt += r - l + 1; return t[k];
 } else if(l == r) {	// 3:二分触底,但不再计算它
     return 0; //(唯一改动的代码)
 } // 2:对于累加后超限的区间,二分之
}

很好,这样我们似乎就做对了,然后兴高采烈地交题

……

T了??我二分咋还T了??

仔细想想,我们唯一的改动是没有把临界点累加,但正因为忽略了临界点,cur会一直不超过sum,这就导致会有大量的区间满足情况2,对每一个这样的区间,我们都会一直二分一直二分到底,最后的结果其实变成了我们在一个一个地往后尝试叶子结点,看它是否能被累加,这时间复杂度又退化了。

也就是说,对于可行的情况 (cur + t[k] <= sum) 我们是跳过的,但对于不可行的情况,我们依然在一个一个试错。

所以有没有一下子跳过很多不可行情况的方法呢?

很容易想到,一个区间没有任何可行点等价于这个区间的最小值+cur > sum,所以我们让线段树另行维护一个minv,对于minv[k] + cur > sum的情况直接跳过进行剪枝。结合整个序列的单调性,其实这就相当于在累加完一个连续的区间之后,我们二分查找下一个可行点,从新的可行点开始继续累加一个连续的区间……

查询部分的完整代码:

ll sum; int cnt;
ll query(int L, int R, int k, int l, int r, ll cur) {
 if(cur > sum) return 0;
 if(L <= l && r <= R) {
     if(minv[k] + cur > sum) return 0;
     if(t[k] + cur <= sum) {
         cnt += r - l + 1; return t[k];
     } else if(l == r) return 0;
 }
 int m = l + ((r - l) >> 1);
 Pushdown(k, m - l + 1, r - m);
 ll ans = 0;
 if(L <= m) ans += query(L, R, k << 1, l, m, cur);
 if(R > m) ans += query(L, R, k << 1 | 1, m + 1, r, cur + ans); 
 return ans;
}

为什么时间复杂度是对的?

根据贪心选择的性质,每一段连续的可行区间都会砍掉超过一半余额,这样我们只会在\(O(lgSum)\)个区间中跳转,在每个区间内部是二分,在区间间跳转也是二分,这样总的时间复杂度就完全可接受了。

总结

实际上对于这道题应该是能很快地发现上述性质,并沿着这种思路做下去的。

但如果你真的把两个二分分开来写的话,可能会有点麻烦。

实际上这一切都可以转化成在线段树上的遍历+剪枝过程,只通过一个线段树遍历查询就能实现两个互相交迭发生的过程,我愿称之为在线段树上跳舞)

posted @ 2020-11-21 04:28  古明地绿  阅读(166)  评论(0编辑  收藏  举报