【学习笔记】势能线段树——线段树的小套路,让暴力显得更加优美

一些同类型的套路

线段树维护的区间和要具有可加性,并且对于区间的修改需要打lazy标记或者标记永久化。

然而,一些操作,比如对 lr 区间内的每个数开方、取模等不具备可整体修改性质的操作就无从下手了,并且(至少是我知道的)别的数据结构们也维护不了这些操作。

但是,这些仍然可以用线段树来维护,只是需要一点点的小变动。

P4145 上帝造题的七分钟 2 / 花神游历各国

题意概述:

维护一个序列,支持区间开方与求和操作。

解析:

区间每个数都开方的话,显然无法在sum上直接操作,然而暴力的单点修改显然会超时,而 lazy标记显然也无法胜任。

本题的精髓在于 1=1
最大的数据是 1e12 的,向下取整后,最多进行 6sqrt操作后就能到达 1

所以,我们可以记录下当前区间的最大值,如果该区间的最大值 1 了,就可以不用管它了,否则,我们对每个区间都直接递归到底。

因为最多进行六次修改就到 1 了,所以均摊时间复杂度还是 O(logn) 的,总复杂度是 O(nlogn) 的,可过。

Code

#include<cmath>
#include<cstdio>
#include<algorithm>
#define LL long long
using namespace std;
const int MAXN = 1e5 + 10;
int n, m;
LL num[MAXN];
struct Segment_Tree{
struct Tree{
int l, r;
LL max;
LL sum;
}tr[MAXN << 2];
inline int lson(int rt){
return rt << 1;
}
inline int rson(int rt){
return rt << 1 | 1;
}
inline void Pushup(int rt){
tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
}
void Build(int rt, int l, int r){
tr[rt].l = l;
tr[rt].r = r;
if(l == r){
tr[rt].sum = num[l];
tr[rt].max = num[l];
return;
}
int mid = (l + r) >> 1;
Build(lson(rt), l, mid);
Build(rson(rt), mid + 1, r);
Pushup(rt);
}
void Update(int rt, int l, int r){
if(tr[rt].l == tr[rt].r){
tr[rt].sum = sqrt(tr[rt].sum);
tr[rt].max = sqrt(tr[rt].max);
return;
}
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(l <= mid && tr[lson(rt)].max > 1)
Update(lson(rt), l, r);
if(r > mid && tr[rson(rt)].max >1)
Update(rson(rt), l, r);
Pushup(rt);
}
LL Query_Sum(int rt, int l, int r){
if(tr[rt].l >= l && tr[rt].r <= r)
return tr[rt].sum;
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(r <= mid) return Query_Sum(lson(rt), l, r);
else if(l > mid) return Query_Sum(rson(rt), l, r);
else return Query_Sum(lson(rt), l, r) + Query_Sum(rson(rt), l, r);
}
}S;
inline LL read(){
LL x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
int main(){
n = read();
for(register int i = 1; i <= n; i++)
num[i] = read();
S.Build(1, 1, n);
m = read();
for(register int i = 1; i <= m; i++){
int opt, l, r;
opt = read(), l = read(), r = read();
if(l > r) swap(l, r);
if(opt == 0) S.Update(1, l, r);
else printf("%lld\n", S.Query_Sum(1, l, r));
}
return 0;
}

CF438D The Child and Sequence

题意概述:

维护一个序列,支持区间的取模、求和和单点修改。

解析:

同样的无法用lazy标记维护。

思路大致是一样的,同样维护区间的最大值,比较区间最大值是否大于等于模数,如果大于等于,进行递归,直到叶子节点。

均摊时间复杂度仍然是 O(logn) 的,可过。

Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 1e5 + 10;
int n, m;
int num[MAXN];
struct Segment_Tree{
struct Tree{
int l, r;
int max;
long long sum;
}tr[MAXN << 2];
inline int lson(int rt){
return rt << 1;
}
inline int rson(int rt){
return rt << 1 | 1;
}
inline void Pushup(int rt){
tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
}
void Build(int rt, int l, int r){
tr[rt].l = l;
tr[rt].r = r;
if(l == r){
tr[rt].sum = tr[rt].max = num[l];
return;
}
int mid = (l + r) >> 1;
Build(lson(rt), l, mid);
Build(rson(rt), mid + 1, r);
Pushup(rt);
}
void Update_Mod(int rt, int l, int r, int data){
if(tr[rt].l == tr[rt].r){
tr[rt].sum %= data;
tr[rt].max %= data;
return;
}
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(l <= mid && tr[lson(rt)].max >= data) Update_Mod(lson(rt), l, r, data);
if(r > mid && tr[rson(rt)].max >= data) Update_Mod(rson(rt), l, r, data);
Pushup(rt);
}
void Update_Point(int rt, int pos, int data){
if(tr[rt].l == tr[rt].r){
tr[rt].sum = data;
tr[rt].max = data;
return;
}
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(pos <= mid) Update_Point(lson(rt), pos, data);
else Update_Point(rson(rt), pos, data);
Pushup(rt);
}
long long Query_Sum(int rt, int l, int r){
if(tr[rt].l >= l && tr[rt].r <= r)
return tr[rt].sum;
long long ans = 0;
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(l <= mid) ans += Query_Sum(lson(rt), l, r);
if(r > mid) ans += Query_Sum(rson(rt), l, r);
return ans;
}
}S;
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
int main(){
n = read(), m = read();
for(register int i = 1; i <= n; i++)
num[i] = read();
S.Build(1, 1, n);
for(register int i = 1; i <= m; i++){
int opt;
opt = read();
if(opt == 1){
int l, r;
l = read(), r = read();
printf("%lld\n", S.Query_Sum(1, l, r));
}
else if(opt == 2){
int l, r, x;
l = read(), r = read(), x = read();
S.Update_Mod(1, l, r, x);
}
else{
int k, x;
k = read(), x = read();
S.Update_Point(1, k, x);
}
}
return 0;
}

CF920F SUM and REPLACE

题意概述:

维护一个序列,支持把区间内的每一个数 i,都变成 τ(i) 和求和。(τ(i) 表示 i 的正约数个数)。

解析:

看到这,聪明的你应该已经明白套路了吧。

先考虑怎么求出 τ(i)

有唯一分解定理:p=p1k1×p2k2×...×pnkn
τ(p)=i=1n(ki+1)

但每次这样求是 O(n) 的,会超时。
考虑线性筛,每个数有三种情况:

  1. i 是素数:
    i 只能被 1 和它本身整除,所以:τ(i)=2
  2. i 与一个不能整除它的素数相乘(设素数为 p):
    ip1 外没有相同的正约数,所以 i 的每个正约数和 p 的每个正约数分别相乘,得到的都是 i×p 的正约数,且两两不同,所以:τ(i)×τ(p) 即: τ(i×p)=τ(i)×2
  3. i 与一个能整除它的素数相乘(设素数为 p):
    ip 有除 1 外的正约数,则 τ(i)×τ(p) 时,i/p 的正约数在乘以 p 后都会变成 i 的正约数。
    i 的正约数与 p1(他们是 p 仅有的正约数)相乘后,相当于每个 i/p 的正约数乘以 p 的积都出现了两遍,所以:τ(i×p)=τ(i)×2τ(i/p)

特别的 τ(0)=0,τ(1)=1

之后,我们发现,最大的 τ(i)i2,所以仍然能保证复杂度是 O(logn) 的。

按照原套路,记录区间的最大值是否 1 来判断需不需要递归到叶子节点即可。

Code

#include<cstdio>
#include<algorithm>
using namespace std;
const int MAXN = 3e5 + 10, MAXM = 3e6 + 10;
int n, m, sum, max_a;
int num[MAXN];
int prime[MAXM], v[MAXM], d[MAXM];
inline int read(){
int x = 0, f = 1;
char c = getchar();
while(c < '0' || c > '9'){
if(c == '-') f = -1;
c = getchar();
}
while(c >= '0' && c <= '9'){
x = (x << 1) + (x << 3) + (c ^ 48);
c = getchar();
}
return x * f;
}
void Euler(int n){
sum = 0;
d[1] = 1;
for(register int i = 2; i <= n; i++){
if(!v[i]){
d[i] = 2;
v[i] = i;
prime[++sum] = i;
}
for(register int j = 1; j <= sum; j++){
if(prime[j] > v[i] || prime[j] * i > n)
break;
v[i * prime[j]] = prime[j];
if(i % prime[j]) d[i * prime[j]] = d[i] * 2; //等同于 d[i] * d[prime[j]]
else d[i * prime[j]] = d[i] * 2 - d[i / prime[j]];
}
}
}
struct Segment_Tree{
struct Tree{
int l, r;
int max;
long long sum;
}tr[MAXN << 2];
inline int lson(int rt){
return rt << 1;
}
inline int rson(int rt){
return rt << 1 | 1;
}
inline void Pushup(int rt){
tr[rt].sum = tr[lson(rt)].sum + tr[rson(rt)].sum;
tr[rt].max = max(tr[lson(rt)].max, tr[rson(rt)].max);
}
void Build(int rt, int l, int r){
tr[rt].l = l;
tr[rt].r = r;
if(l == r){
tr[rt].sum = tr[rt].max = num[l];
return;
}
int mid = (l + r) >> 1;
Build(lson(rt), l, mid);
Build(rson(rt), mid + 1, r);
Pushup(rt);
}
void Update(int rt, int l, int r){
if(tr[rt].l == tr[rt].r){
tr[rt].sum = d[tr[rt].sum];
tr[rt].max = d[tr[rt].max];
return;
}
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(l <= mid && tr[lson(rt)].max > 2) Update(lson(rt), l, r);
if(r > mid && tr[rson(rt)].max > 2) Update(rson(rt), l, r);
Pushup(rt);
}
long long Query_Sum(int rt, int l, int r){
if(tr[rt].l >= l && tr[rt].r <= r)
return tr[rt].sum;
long long ans = 0;
int mid = (tr[rt].l + tr[rt].r) >> 1;
if(l <= mid) ans += Query_Sum(lson(rt), l, r);
if(r > mid) ans += Query_Sum(rson(rt), l, r);
return ans;
}
}S;
int main(){
n = read(), m = read();
for(register int i = 1; i <= n; i++){
num[i] = read();
max_a = max(max_a, num[i]);
}
S.Build(1, 1, n);
Euler(max_a);
for(register int i = 1; i <= m; i++){
int opt;
opt = read();
if(opt == 1){
int l, r;
l = read(), r = read();
S.Update(1, l, r);
}
else{
int l, r;
l = read(), r = read();
printf("%lld\n", S.Query_Sum(1, l, r));
}
}
return 0;
}
posted @   TSTYFST  阅读(74)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示