勢能線段樹(吉司機線段樹)
簡單介紹和理解
我們知道傳統的支援區間修改的線段樹,我們都是靠\(lazy\)標記來節省開銷的。可以使用\(lazy\)標記必須要滿足下面兩個條件:
- 區間節點的值可以根據\(lazy\)標記來更新.
- \(lazy\)標記之間可以快速相互合併.
但是很多時候我們要完成的區間修改操作是不能依靠\(lazy\)標記來完成的,比如區間開根號,區間位運算。因為這些運算都是依賴於葉子節點的值的。我們無法直接對\(lazy\)標記或者是區間的值進行修改。但是如果一直無腦遞迴到葉子節點,一個一個修改的話,顯然時間成本我們是無法接受的。所以我們就要使用勢能線段樹,其實就是類似於在BFS裡進行剪枝。我們發現每一個操作,總會使得其能夠接受的繼續進行修改的次數越來越少,就好像你一開始位於高空,每次修改會讓你的高度下降,當你落到地面時,再對你修改就已經沒有意義了。就是這個操作對你而言已經"退化"了。
所以我們可以這樣來建立和操作這棵線段樹:
- 在每個節點額外加入一個"勢能標記",來記錄和維護當前區間結點的勢能情況。
- 對於每次的區間修改,若當前區間內所有結點的勢能皆已為零,直接退出遞迴不再修改.
- 若當前區間內還存在勢能不為零的結點,則繼續向下遞迴,暴力修改要求區間內每一個勢能不為零的結點.
題目
A. 上帝造題的七分鐘 2 / 花神遊歷各國
連結: https://www.luogu.com.cn/problem/P4145
題意:
給定\(n\)個數,兩種操作:
- 區間開根號(向下取整)。
- 區間詢問和。
思路:
顯然,我們無法使用\(lazy\)標記來節省對區間開根號的開銷,因為開根號是由每個葉子節點自己的值決定的。但我們很容易發現當一個數小於等於\(1\)以後,再對其開根號是無效的,所以我們可以維護區間最大值作為標記。一旦區間修改時發現此區間的最大值小於等於\(1\)時,我們不需要再次修改,直接\(return\)即可,否則繼續向下遞迴修改。
程式碼:
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-6;
const ll N = 2e5 + 10;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
int l, r;
ll mx, sum;
}tree[4 * N];
ll a[N];
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;
if(l == r){
tree[id].mx = a[l];
tree[id].sum = a[l];
return ;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
}
ll ask(int id, int l, int r){
int L = tree[id].l ;
int R = tree[id].r ;
if(L >= l && R <= r) return tree[id].sum;
int mid = (L + R) >> 1;
ll val = 0;
if(l <= mid) val += ask(id << 1, l, r);
if(r > mid) val += ask(id << 1 | 1, l, r);
return val;
}
void change(int id, int l, int r){
int L = tree[id].l;
int R = tree[id].r;
if(tree[id].mx <= 1) return;
if(L == R) {
tree[id].mx = sqrt(tree[id].mx);
tree[id].sum = tree[id].mx;
return;
}
int mid = (L + R) >> 1;
if(l <= mid ) change(id << 1, l, r);
if(r > mid ) change(id << 1 | 1, l, r);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
}
int main(){
ywh666;
ll n ;
cin >> n;
for(int i = 1; i <= n ; i ++) cin >> a[i];
int q;
cin >> q;
build(1, 1, n);
while(q --){
int op, l, r;
cin >> op >> l >> r;
if(l > r) swap(l, r);
if(op == 0){
change(1, l, r);
}else{
cout << ask(1, l, r) << endl;
}
}
return 0 ;
}
B. The Child and Sequence
連結: https://codeforces.com/problemset/problem/438/D
題意:
給定\(n\)個數,三種操作:
- 區間詢問和。
- 區間取模。
- 單點修改。
思路:
如上題目一樣,我們還是無法使用\(lazy\)標記來方便的完成區間的修改,但是我們很容易發現如果一個數已經小於模數了,那對其取模與否是沒有影響的。所以我們可以維護區間最大值,當區間修改到這個區間時,如果其最大值已經小於模數,我們直接\(return\),否則繼續遞迴修改。
程式碼:
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-6;
const ll N = 1e5 + 10;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
int l, r;
ll mx, sum;
}tree[4 * N];
ll a[N];
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;
if(l == r){
tree[id].mx = a[l];
tree[id].sum = a[l];
return ;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
}
ll qurry(int id, int l, int r){
int L = tree[id].l ;
int R = tree[id].r ;
if(L >= l && R <= r) return tree[id].sum;
int mid = (L + R) >> 1;
ll val = 0;
if(l <= mid) val += qurry(id << 1, l, r);
if(r > mid) val += qurry(id << 1 | 1, l, r);
return val;
}
void change(int id, int l, int r, int x){
int L = tree[id].l;
int R = tree[id].r;
if(tree[id].mx < x) return;
if(L == R) {
tree[id].mx %= x;
tree[id].sum = tree[id].mx;
return;
}
int mid = (L + R) >> 1;
if(l <= mid ) change(id << 1, l, r, x);
if(r > mid ) change(id << 1 | 1, l, r, x);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
}
void change2(int id, int idx, int x){
if(tree[id].l == tree[id].r ){
tree[id].mx = x;
tree[id].sum = x;
return;
}
int mid = (tree[id].l + tree[id].r) >> 1;
if(idx <= mid) change2(id << 1, idx, x);
if(idx > mid) change2(id << 1 | 1, idx, x);
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
}
int main(){
ywh666;
ll n, m ;
cin >> n >> m;
for(int i = 1; i <= n ; i ++) cin >> a[i];
build(1, 1, n);
while(m --){
int op, k, l , r, x;
cin >> op ;
if(op == 1){
cin >> l >> r;
cout << qurry(1, l, r) << endl;
}else if(op == 2){
cin >> l >> r >> x;
change(1, l, r, x);
}else{
cin >> k >> x;
change2(1, k, x);
}
}
return 0 ;
}
C. SUM and REPLACE
連結: https://codeforces.com/contest/920/problem/F
題意:
定義\(f(x) = x\)的因子個數
給定\(n\)個數,有兩種操作:
1.區間修改\(x = f(x)\)。
2.區間詢問和。
思路:
還是一樣,\(lazy\)標記是無法傳遞我們的區間修改的。但是我們可以發現一個當\(x\leq 2\)的時候,對其操作又是無效的。那麼我們還是可以記錄一個區間最大值,當區間最大值小於等於\(2\)的時候就可以直接\(return\),否則我們繼續向下遞迴,暴力修改即可。考慮到反覆執行操作\(1\)之後,一個數只會越來越小,而且數字最大不超過\(1e6\),我們可以事先預處理出每個數的因子個數儲存下來。修改的時候直接呼叫即可,避免反覆求同一個數所產生的多餘開銷。
程式碼:
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-6;
const ll N = 3e5 + 10;
const ll M = 1e6;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
int l, r, mx;
ll sum;
}tree[4 * N];
int a[M + 7];
int b[N];
void push_up(int id){
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum;
}
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;;
if(l == r){
tree[id].sum = b[l];
tree[id].mx = b[l];
return ;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
push_up(id);
}
void modify(int id, int l, int r){
int L = tree[id].l;
int R = tree[id].r;
if(tree[id].mx <= 2) return;
if(L == R){
tree[id].sum = a[tree[id].sum];
tree[id].mx = tree[id].sum;
return;
}
int mid = (L + R) >> 1;
if(l <= mid) modify(id << 1, l, r);
if(r > mid) modify(id << 1 | 1, l, r);
push_up(id);
}
ll qurry(int id, int l, int r){
int L = tree[id].l;
int R = tree[id].r;
if(L >= l && R <= r) return tree[id].sum;
ll sum = 0;
if(tree[id << 1].r >= l) sum += qurry(id << 1, l, r);
if(tree[id << 1 | 1].l <= r) sum += qurry(id << 1 | 1, l, r);
return sum;
}
int main(){
ywh666;
for(int i = 1 ; i <= M ; i ++){
for(int j = i; j <= M ; j += i){
a[j] ++;
}
}
int n, m;
cin >> n >> m;
for(int i = 1 ; i <= n ; i ++) cin >> b[i];
build(1, 1, n);
while(m --){
int op, l, r;
cin >> op >> l >> r;
if(op == 1){
modify(1, l, r);
}else{
cout << qurry(1, l, r) << endl;
}
}
return 0 ;
}
前三題的勢能減少情況很明顯就可以看出來,只要對這類線段樹有所瞭解,甚至對於剪枝理解較深的話很快就可以做出來。接下來的題目勢能的減少稍有難度。
D. And RMQ
連結: https://codeforces.com/gym/103107/problem/A
題意:
給定\(n\)個數,有三種操作:
- 區間按位與。
- 區間詢問最大值。
思路:
顯然區間按位與的操作我們仍舊不能使用\(lazy\)標記來便捷的完成區間修改。那麼我們要怎麼來減少操作\(1\)的開銷呢?我們來考慮什麼時候對於一個區間而言,做一次操作\(1\)對於操作\(2\)的詢問的結果是不影響的。我們假設對一個區間內的所有數都按位與\(x\),我們發現對於\(x\)的二進位制下是\(0\)的位,原來區間內所有的數在該位都會變成\(0\),那麼很顯然如果原來的最大值在這些位置上是\(1\),其大小會減小很多,我們無法保證在它減小的時候,該區間其他數也會都減小,或者減小的很多。那麼我們怎麼保證其他的數在按位與\(x\)以後還是比原來的最大值小呢?稍加思考我們可以發現,對於區間\([l,r]\),若$(a_i | a_{i + 1} | a_{i + 2} \dots a_{r - 1} | a_r) $ & $x = $$(a_i | a_{i + 1} | a_{i + 2} \dots a_{r - 1} | a_r) $,那麼這次操作\(1\)我們可以不做修改。因為此時證明\(x\)二進位制下為\(0\)的位置在該區間內沒有一個數在該位置上為\(0\),所以對於每個數都不會減小,也就可以保證原來的最大數還是在該區間最大的。所以我們只要多記錄一個區間或的和,在區間修改時如果其滿足上述式子,便可以直接\(return\),不然繼續向下遞迴,暴力修改即可。
程式碼:
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-9;
const ll N = 4e5 + 10;
const ll INF = 1e18+10;
const ll mod = 1e9+7;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
int l, r, orsum,mx;
}tree[N << 2];
int a[N];
void push_up(int id){
tree[id].mx = max(tree[id << 1].mx, tree[id << 1 | 1].mx);
tree[id].orsum = tree[id << 1].orsum | tree[id << 1 | 1].orsum;
}
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;
if(l == r){
tree[id].mx = a[l];
tree[id].orsum = a[l];
return;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
push_up(id);
}
void modify(int id, int l, int r, int x){
if((tree[id].orsum & x) == tree[id].orsum) return ;
if(tree[id].l == tree[id].r){
tree[id].mx &= x;
tree[id].orsum &= x;
return;
}
if(tree[id << 1].r >= l) modify(id << 1, l, r, x);
if(tree[id << 1 | 1].l <= r) modify(id << 1 | 1, l, r, x);
push_up(id);
}
void change(int id, int x, int v){
if(tree[id].l == tree[id].r){
tree[id].mx = v;
tree[id].orsum = v;
return ;
}
if(tree[id << 1].r >= x) change(id << 1, x, v);
if(tree[id << 1 | 1].l <= x) change(id << 1 | 1, x, v);
push_up(id);
}
int qurry(int id, int l, int r){
if(tree[id].l >= l && tree[id].r <= r) return tree[id].mx;
int val = -1;
if(tree[id << 1].r >= l) val = max(val, qurry(id << 1, l, r));
if(tree[id << 1 | 1].l <= r) val = max(val, qurry(id << 1 | 1, l, r));
return val;
}
int main(){
ywh666;
int n, q ;
cin >> n >> q;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
build(1, 1, n);
while(q --){
string s;
cin >> s;
if(s == "AND"){
int l, r, x;
cin >> l >> r >> x;
modify(1, l, r, x);
}else if(s == "UPD"){
int x, v;
cin >> x >> v;
change(1, x, v);
}else{
int l, r;
cin >> l >> r;
cout << qurry(1, l, r) << endl;
}
}
return 0 ;
}
E. Euler Function
連結:https://pintia.cn/market/tag/1439767147859537920
簽到獲得5金幣以後花費1金幣購買,一次購買只有5小時。(巨坑!!!)
題意:
給定\(n\)個數,兩種操作:
- 區間乘法。
- 區間詢問尤拉函式和(對大質數取模)。
思路:
顯然,我們這次終於可以使用\(lazy\)標記了。但是它只能幫我們解決操作\(1\)。我們來思考一下怎麼快速解決操作\(2\),顯然暴力修改是不現實的。我們注意到尤拉函式有這樣的性質:
對於一個質數\(p\)和一個數\(x\):
若 \(p | x\) = \(0\),則 \(\phi(p\times x) = p \times \phi (x)\),
否則 \(\phi(p\times x) = (p-1) \times \phi (x)\)。
我們注意到這個條件的\(p\)只能是質數的,但是我們進行操作\(1\)時的數是什麼都不保證的,所以我們很自然的可以想到將操作\(1\)進行轉化。我們可以不直接區間乘\(x\),我們可以把\(x\)先分解質因數,在此基礎上,將其質因數分別做操作\(1\),所以這會使得我們的操作\(1\)的次數大大增加,但是我們可以更好的維護區間的尤拉函式和。下面我們來介紹操作\(2\)如何完成。我們利用上面的尤拉函式的性質,當我們操作\(1\)乘的全是質數的時候,我們只要統計,在這個區間內的所有數的質因數都中是否都存在操作\(1\)乘的這個數,如果存在,那麼我們對於這個區間的尤拉函式和不就又變成了一個區間乘法嗎?如果不都存在,我們便可以一直暴力遞迴下去,一直到葉子節點。再獨立判單是否存在,來決定對這個葉子節點的尤拉函式值修改多少。
那麼怎麼實現呢?考慮到操作\(1\)的數最大隻有\(100\),我們完全可以先預處理分解好\(100\)以內所有數的質因數。但是顯然我們線上段樹的每個節點除了維護其尤拉函式值以外,我們還要維護這個區間裡所有數的共同質因子,但是每次查詢的時候單獨去分解質因數顯然開銷是特別大的。所以我們在每個節點可以維護一個\(bitset\),這樣不光儲存方便,常數小,在push_up的時候我們可以直接將兩個\(bitset\)按位與,快速得到共同質因子。
程式碼:
(預處理寫的有點醜,篩法部分大家可以自己用更快的)
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
typedef long double ld;
const double eps = 1e-9;
const ll N = 1e5 + 10;
const ll INF = 1e18+10;
const ll mod = 998244353;
const ll maxm = 110;
#define ywh666 std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
#define all(a) a.begin(),a.end()
struct node{
int l, r;
ll sum, lz;
bitset<30> bt;
}tree[N << 2];
int a[N], phi[maxm];
bitset<30> sat[maxm];
int bh[maxm];
int bh2[maxm];
void init(){
bh[2] = 1;
bh[3] = 2;
bh2[1] = 2;
bh2[2] = 3;
int st = 3;
for(int i = 4; i <= 100 ; i ++){
bool f = 1;
for(int j = 2 ; j * j <= i ; j ++){
if(i % j == 0){
f = 0;
break;
}
}
if(f){
bh[i] = st ;
bh2[st] = i;
st ++;
}
}
for(int i = 2; i <= 100 ; i ++){
if(bh[i] != 0){
for(int j = i ; j <= 100 ; j += i){
sat[j][bh[i]] = 1;
}
}
}
}
void euler(int n = 100){
for(int i = 2 ; i <= n ; i ++) phi[i] = i;
for(int i = 2 ; i <= n ; i ++){
if(phi[i] == i){
for(int j = i ; j <= n ; j += i){
phi[j] = phi[j] / i * (i - 1);
}
}
}
phi[1] = 1;
}
void push_up(int id){
tree[id].bt = tree[id << 1].bt & tree[id << 1 | 1].bt;
tree[id].sum = tree[id << 1].sum + tree[id << 1 | 1].sum ;
tree[id].sum %= mod;
}
void push_down(int id){
tree[id << 1].sum =tree[id << 1].sum * tree[id].lz % mod ;
tree[id << 1 | 1].sum =tree[id << 1 | 1].sum * tree[id].lz % mod ;
tree[id << 1].lz = tree[id << 1].lz * tree[id].lz % mod;
tree[id << 1 | 1].lz =tree[id << 1 | 1].lz * tree[id].lz % mod;
tree[id].lz = 1;
}
void build(int id, int l, int r){
tree[id].l = l;
tree[id].r = r;
tree[id].lz = 1;
if(l == r){
tree[id].sum = phi[a[l]];
tree[id].bt = sat[a[l]];
return;
}
int mid = (l + r) >> 1;
build(id << 1, l, mid);
build(id << 1 | 1, mid + 1, r);
push_up(id);
}
void modify(int id, int l, int r, int x){
if(tree[id].l >= l && tree[id].r <= r){
if(tree[id].bt[bh[x]]){
tree[id].lz = 1ll * tree[id].lz * x % mod;
tree[id].sum = 1ll * tree[id].sum * x % mod;
return;
}
if(tree[id].l == tree[id].r){
tree[id].lz = 1ll * tree[id].lz * (x - 1) % mod;
tree[id].sum = 1ll * tree[id].sum * (x - 1) % mod;
tree[id].bt[bh[x]] = 1;
return;
}
}
push_down(id);
if(tree[id << 1].r >= l) modify(id << 1, l, r, x);
if(tree[id << 1 | 1].l <= r) modify(id << 1 | 1, l, r, x);
push_up(id);
}
int qurry(int id, int l, int r){
if(tree[id].l >= l && tree[id].r <= r) return tree[id].sum % mod;
ll val = 0;
push_down(id);
if(tree[id << 1].r >= l) val += qurry(id << 1, l, r);
if(tree[id << 1 | 1].l <= r) val += qurry(id << 1 | 1, l, r);
return val % mod;
}
int main(){
ywh666;
init();
euler();
int n, q ;
cin >> n >> q;
for(int i = 1 ; i <= n ; i ++) cin >> a[i];
build(1, 1, n);
while(q --){
int op;
cin >> op;
if(op == 0){
int l, r, x;
cin >> l >> r >> x;
while(x != 1){
int nn = x;
for(int i = 1; i <= 29 ; i ++){
if(sat[x][i]== 1){
modify(1, l, r, bh2[i]);
nn /= bh2[i];
}
}
x = nn;
}
}else{
int l, r;
cin >> l >> r;
cout << qurry(1, l, r) % mod << endl;
}
}
return 0 ;
}