HDU 4027 Can you answer these queries? (线段树成段更新 && 开根操作 && 规律)
题意 : 给你N个数以及M个操作,操作分两类,第一种输入 "0 l r" 表示将区间[l,r]里的每个数都开根号。第二种输入"1 l r",表示查询区间[l,r]里所有数的和。
分析 : 不难想到用线段树,但是这里的线段树开根操作的更新很明显不能跟加减操作那样子通过Lazy Tag来实现,那么最笨的方法就是一直更新到叶子节点,不过这也就失去了线段树的高效性,每一次操作都更新到叶子节点的话会超时,此时来想想有没有什么规律可以减少操作的复杂度,细想就会发现在有限次的开根之后所有的数都会变成1,这里能给出的最大的数是263这个数被开根七次之后就会变成1,那么如果需要更新的某一段的值已经被开根了7次或7次以上那么就无需再向下更新,也就是每个叶子节点最多更新7次。普通的线段树操作就不叙述了,这里说说判断是否已经开根七次的方法。
①多开辟一个标记数组来记录开根信息,比如给区间(l, r)开根,那么就把被(l, r)包裹住的子区间对应的标记+1,同样的,如果下一次碰到某一个子区间的标记已经>=7了那么就直接return无需向下更新。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h>
#define LL long long
#define lson l, m, rt<<1
#define rson m+1, r, rt<<1|1
using namespace std;
const int maxn = 100000 + 5;
LL sumv[maxn<<2];
int sq[maxn<<2];
int N, M;
inline void build(int l, int r, int rt)
{
if(l == r){
scanf("%I64d", &sumv[rt]);
return ;
}
int m = (l + r)>>1;
build(lson);
build(rson);
sumv[rt] = sumv[rt<<1] + sumv[rt<<1|1];
}
inline void update(int L, int R, int l, int r, int rt)
{
if(L <= l && r <= R && sq[rt]>=7){///先判断是否是被查询区间包裹的子区间,再判断是否需要继续向下更新
sumv[rt] = r - l + 1;
return ;
}
if(L <= l && r <= R) sq[rt]++;///注意什么时候需要+1
if(l == r){
///sq[rt]++ 之前因为逻辑疏忽,我在这里也+1操作了,WA了很多次静下来思考才发现
sumv[rt] = (LL)sqrt(sumv[rt]);
return ;
}
int m = (l + r)>>1;
if(L <= m) update(L, R, lson);
if(R > m) update(L, R, rson);
sumv[rt] = sumv[rt<<1] + sumv[rt<<1|1];
}
LL query(int L, int R, int l, int r, int rt)
{
if(L <= l && r <= R) return sumv[rt];
LL ret = 0;
int m = (l + r)>>1;
if(L <= m) ret += query(L, R, lson);
if(R > m) ret += query(L, R, rson);
return ret;
}
int main(void)
{
int Case = 1;
while(~scanf("%d", &N)){
memset(sumv, 0, sizeof(sumv));
memset(sq, 0, sizeof(sq));
build(1, N, 1);
scanf("%d", &M);
printf("Case #%d:\n", Case++);
while(M--){
int command, L, R;
scanf("%d%d%d", &command, &L, &R);
if(L > R) swap(L, R);
if(!command) update(L, R, 1, N, 1);
else printf("%I64d\n", query(L, R, 1, N, 1));
}
puts("");
}
return 0;
}
②实际上有个更简便的方法,就是最后被开7次根及以上的区间和都会变成区间长度,那么我们只要每一次都判断被(l, r)包裹住的子区间的和是否等于区间长度就能判断是否需要再继续更新下去了。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
#include<bits/stdc++.h>
#define LL long long
#define lson l, m, rt<<1
#define rson m+1, r, rt<<1|1
using namespace std;
const int maxn = 100000 + 5;
LL sumv[maxn<<2];
int N, M;
inline void build(int l, int r, int rt)
{
if(l == r){
scanf("%I64d", &sumv[rt]);
return ;
}
int m = (l + r)>>1;
build(lson);
build(rson);
sumv[rt] = sumv[rt<<1] + sumv[rt<<1|1];
}
inline void update(int L, int R, int l, int r, int rt)
{
if(L <= l && r <= R && sumv[rt]==r-l+1) return ;
if(l == r){
sumv[rt] = (LL)sqrt(sumv[rt]);
return ;
}
int m = (l + r)>>1;
if(L <= m) update(L, R, lson);
if(R > m) update(L, R, rson);
sumv[rt] = sumv[rt<<1] + sumv[rt<<1|1];
}
LL query(int L, int R, int l, int r, int rt)
{
if(L <= l && r <= R){
return sumv[rt];
}
LL ret = 0;
int m = (l + r)>>1;
if(L <= m) ret += query(L, R, lson);
if(R > m) ret += query(L, R, rson);
return ret;
}
int main(void)
{
int Case = 1;
while(~scanf("%d", &N)){
memset(sumv, 0, sizeof(sumv));
build(1, N, 1);
scanf("%d", &M);
printf("Case #%d:\n", Case++);
while(M--){
int command, L, R;
scanf("%d%d%d", &command, &L, &R);
if(L > R) swap(L, R);
if(!command) update(L, R, 1, N, 1);
else printf("%I64d\n", query(L, R, 1, N, 1));
}
puts("");
}
return 0;
}
瞎 :
①更新到叶子节点的操作实际无需在main里面使用一个for循环,只要将update里面赋值的语句将原来的if(L <= l && r <= R)改成if(l == r)即可
②以后遇到类似开根的削减操作,可以考虑被削减多次之后变成的固定值会不会是解题的一个突破口
③在实现想法的时候需要认真思考,逻辑千万不能乱,否则调试起来相当困难,比如第一种判断方法下自己就写错了很多次,这还不要紧,关键是会怀疑一些没有必要怀疑的地方,冷静分析最重要,如果真的已经很乱了,不妨将代码删除,再来一遍!