勢能線段樹專題

_etilletas發表於2022-04-10

勢能線段樹(吉司機線段樹)

簡單介紹和理解

我們知道傳統的支援區間修改的線段樹,我們都是靠\(lazy\)標記來節省開銷的。可以使用\(lazy\)標記必須要滿足下面兩個條件:

  1. 區間節點的值可以根據\(lazy\)標記來更新.
  2. \(lazy\)標記之間可以快速相互合併.

但是很多時候我們要完成的區間修改操作是不能依靠\(lazy\)標記來完成的,比如區間開根號,區間位運算。因為這些運算都是依賴於葉子節點的值的。我們無法直接對\(lazy\)標記或者是區間的值進行修改。但是如果一直無腦遞迴到葉子節點,一個一個修改的話,顯然時間成本我們是無法接受的。所以我們就要使用勢能線段樹,其實就是類似於在BFS裡進行剪枝。我們發現每一個操作,總會使得其能夠接受的繼續進行修改的次數越來越少,就好像你一開始位於高空,每次修改會讓你的高度下降,當你落到地面時,再對你修改就已經沒有意義了。就是這個操作對你而言已經"退化"了。
所以我們可以這樣來建立和操作這棵線段樹:

  1. 在每個節點額外加入一個"勢能標記",來記錄和維護當前區間結點的勢能情況。
  2. 對於每次的區間修改,若當前區間內所有結點的勢能皆已為零,直接退出遞迴不再修改.
  3. 若當前區間內還存在勢能不為零的結點,則繼續向下遞迴,暴力修改要求區間內每一個勢能不為零的結點.

題目

A. 上帝造題的七分鐘 2 / 花神遊歷各國

連結: https://www.luogu.com.cn/problem/P4145

題意:

給定\(n\)個數,兩種操作:

  1. 區間開根號(向下取整)。
  2. 區間詢問和。

思路:

顯然,我們無法使用\(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\)個數,三種操作:

  1. 區間詢問和。
  2. 區間取模。
  3. 單點修改。

思路:

如上題目一樣,我們還是無法使用\(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\)個數,有三種操作:

  1. 區間按位與。
  2. 區間詢問最大值。

思路:

顯然區間按位與的操作我們仍舊不能使用\(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\)個數,兩種操作:

  1. 區間乘法。
  2. 區間詢問尤拉函式和(對大質數取模)。

思路:

顯然,我們這次終於可以使用\(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 ;
}

F.