區間資結的應用

建國中學 賴昭勳

區間詢問,區間修改

之前介紹了懶標(lazy tag),說了這句話

懶標分成兩種:

  • 不用下推操作的

\(seg[cur]\)代表當前節點經過子節點修改後的答案

  • 需要下推操作的

該節點的懶標對\(seg[cur]\)做事,遇到的時候再處理。

 

 

這份講義會討論第二種

用例題來解釋吧!

給你一個長度為\(n\)的序列,維護以下操作:

1. 把區間\([l, r]\)的數字全部改成\(x\)

2. 詢問\([l, r]\)的總和

 

\(n, q \leq 2 * 10^5\)

之前的做法是不是不太可行了?

假設這兩個地方都有標記...

5

8

標記的先後會互相影響!

把懶標下推(push)

5

5

5

5, 5, 5, 5, 5...

  1. 用懶標上的東西更新當前節點
  2. 更新左右子樹上的懶標
  3. 清空當前節點上的 tag

上拉資訊(pull)

X

4

\(seg[cur]\)紀錄的是「左右子節點更新完答案之後的結果」

合併時考慮左右節點與他們的懶標

2, 1, 4, 7

4, 4, 4, 4

相加

重新看看modify在幹嘛

#include <iostream>
#include <algorithm>
#define maxn 100005
using namespace std;
int seg[4 * maxn], lazy[4 * maxn];
void modify(....) {
	if (r <= l || ql >= r || qr <= l) return;
    
	if (ql <= l && qr >= r) {
		PUT_TAG;
		return;
	}
   
	int mid = (l + r) / 2;
	modify(LEFT_CHILD);
	modify(RIGHT_CHILD);
    
	PULL seg[cur];
}

應該在哪裡加上push操作?

PUSH(cur);

PUSH(LEFT);

PUSH(RIGHT);

重新看看modify在幹嘛

#include <iostream>
#include <algorithm>
#define maxn 100005
using namespace std;
int seg[4 * maxn], lazy[4 * maxn];
void modify(....) {
	if (r <= l || ql >= r || qr <= l) return;
    
	if (ql <= l && qr >= r) {
		PUT_TAG;
		return;
	}
   
	int mid = (l + r) / 2;
	modify(LEFT_CHILD);
	modify(RIGHT_CHILD);
    
	PULL seg[cur];
}

應該在哪裡加上push操作?

PUSH(cur);

PUSH(LEFT);

PUSH(RIGHT);

重新看一下query

int query(....) {
    if (r <= l || ql >= r || qr <= l) return 0;
    
    
    
    if (ql <= l && qr >= r) {
        return seg[cur];
    }
    
    
    int mid = (l + r) / 2;
    return COMBINE(query(LEFT) + query(RIGHT));
}

push(cur);

應該在哪裡加上push操作?

重新看一下query

int query(....) {
    if (r <= l || ql >= r || qr <= l) return 0;
    
    
    
    if (ql <= l && qr >= r) {
        return seg[cur];
    }
    
    
    int mid = (l + r) / 2;
    return COMBINE(query(LEFT) + query(RIGHT));
}

push(cur);

應該在哪裡加上push操作?

回頭看一下複雜度

整體執行次數和原本的做法一樣!每次push, pull 只有常數次

修改,詢問皆為\(O(logn)\)

push 的實作細節

注意:如果當前節點是葉節點的話,不要把懶標下推,要不然會有各種怪事發生

void push(int cur, int l, int r) { //[l, r)
    seg[cur] = (r - l) * tag[cur];
    if (r - l > 1) {
        tag[cur * 2] = tag[cur];
        tag[cur * 2 + 1] = tag[cur];
    } 
    tag[cur] = 0;
}

還有更複雜的

剛剛的問題,再加上區間加值操作!

給你一個長度為\(n\)的序列,維護以下操作:

1. 把區間\([l, r]\)的數字全部改成\(x\)

2. 把區間\([l, r]\)的數字加上\(x\)

3. 詢問\([l, r]\)的總和

這也太難寫...

紀錄兩種不同的懶標,同時在push的時候處理兩個操作就好了啊!

 

但是在push裡面要怎麼寫...

修改操作的優先序

如果在同一個區間,我之前有加值的標記,現在新增一個改值操作,那改值操作會把加值覆蓋掉

 

因此,實作的時候要保證:如果一個節點同時有加值和改值標記,那一定代表是先改值後加值

 

寫push 的時候:

  1. 先處理改值,並在下推標記時把左右節點的加值tag清空
  2. 正常處理加值操作

困難練習題..IOI 2014 - Wall

這邊的東西在之後的題目可能會用到

給你一個正整數序列\(A\),支援兩種操作

1. 把區間\([l, r]\)的數字\(a_i\)全部改成\(min(a_i, x)\)

2. 把區間\([l, r]\)的數字\(a_i\)全部改成\(max(a_i, x)\)

 

最後請輸出整個序列

\(n \leq 2 \times 10^6, q \leq 5 \times 10^5\) 

線段樹+特殊的合併操作

線段樹不是只有加加減減最大最小這麼死板的..

所有有某種結合律的函數都可以套線段樹!

區間最大連續和

給你一個序列,支援兩種操作:

1. 單點修改某個元素的值

2. 詢問區間\([l, r]\)的最大連續和

用分治的角度看區間連續和

現在有兩個相鄰的區間,你需要知道這些區間的什麼資訊才能找到最大連續和?

有三種可能:

  • 答案在左半
  • 答案在右半
  • 答案橫跨左右邊

不能只存一個數字

對於每個區間,我們需要知道:

  • 最大連續和
  • 最大前綴和
  • 最大後綴和
  • (整個區間的和)

答案會是:\(\max(left.all, right.all, left.suf + right.pref)\)

給你一個序列,支援兩種操作:

1. 把\([l, r]\)的值加上\(k\)

2. 詢問區間\([l, r]\)的最大公因數

 

\(n, q \leq 10^5\) 

給你一個序列,支援兩種操作:

1. 把\(a_i\)加上\(k\)

2. 詢問整個序列,選取任意多個互不相鄰元素的最大和

 

\(n \leq 40000, \ q \leq 50000\) 

線段樹+二分搜

假設你想要做這樣的事...

給你一個序列,支援兩種操作:

1. 單點改值

2. 詢問總和\(\leq x\)的最長前綴長度(aka對前綴取lower_bound)

 

\(n, q \leq 5 \times 10^5, a_i \geq 0\)

可以二分搜!

對位置(答案)二分搜

每次詢問一個位置,看那個位置的前綴和是否超過 \(O(logn)\)

 

總複雜度:\(O(n + qlog^2n)\)

可不可以更好?

利用線段樹的特質

如果左半的總和超過\(x\),答案就在左半,否則就在右半。

 

Query: 30

Sum: 13

Query: 17

Sum: 20

來看一題:TIOJ 1941

https://tioj.ck.tp.edu.tw/problems/1941

 

你要從(0, 0)走到(n-1, n-1),只能往上或往右。每一橫排都有一個區間,走過那個區間就會得到一分(但同一個區間走兩次只有一分)。求最大得分 。

\(n \leq 8 \times 10^5\)

Hint: 先想想\(O(n^2)\)的DP解...

掃描線技巧

還記得矩形覆蓋面積的做法嗎?

大致想法:把資料的兩個維度畫在空間平面上。枚舉一個維度,並用線段樹解決掉另一個維度。

不管是原本的資料,修改,或是詢問都可以對應到空間上的點、線、或平面!

 

也就是說,往往是離線處理詢問

給你一個兩個1~n的排列\(a, b\),有\(q\)筆詢問。每次詢問\(a[l_1...r_1]\)和\(b[l_2...r_2]\)的組成元素是否相同

 

\(n, q \leq 10^5\)

IZHO Sortbooks

 

動態開點

當值域很大,然後無法壓縮的時候...

直接開整個範圍的線段樹會MLE

但我們需要用到所有的節點嗎?

動態開點的想法

每次修改時只會動到\(O(logn)\)個節點,那我要修改節點的時候再新增就好了

動態開點的實作

改寫指標型線段樹。在遞迴下去之前特判該節點是不是Null。

畈勵:No Judge

給你一個序列,支援兩種操作:

1. 單點改值

2. 給你一個\(x\),詢問\(max(a_i \oplus x)\)

 

\(n, q \leq 2 \times 10^5, 0 \leq a_i \leq 10^9\)

謝謝大家

祝各位魔境加油 ;P

Made with Slides.com