事先宣告
本題解文字比較多,較為詳細,演算法為離散化和差分,如會的大佬可以移步去別處看這道題的思路(因為作者比較懶,不想新開兩個專題)。
題目簡要
給定每個起火部分的起點和終點,請你求出燃燒位置的長度之和。
注意:左閉右開
淺淺來談一下
看到這道題時,你肯定有很多疑問。
-
為什麼作者要搞左閉右開啊?
-
有重複的區間怎麼搞?
-
我c, \(a\) 和 \(b\) 的範圍在 \(-2^{31} \le a,b \le 2^{31}\) 之間?
光是這幾個問題就可以把你難倒了嗎?
不可能!我們先由簡到繁,轉換問題。
轉換問題
有一段區間,範圍為 \(0 \le 10^6\) ,其他的和原題一樣。
我們在把問題抽象化。
問題描述
一段區間,每個點有 \(0,1\) 兩個狀態,初始每個點都是 \(0\) 。
每次操作選擇一對 \(l,r\) ,將區間 \(l,r\) 變成 \(1\) ,問最後這一段區間 \(1\) 的個數?
因為題目的範圍不支援我們進行 \(O(MAX\ a)\) 的遍歷,所以,我們要想辦法進行時間複雜度的最佳化,讓他變成 \(O(1)\) 或 \(O(\log_a)\) 。
這樣涉嫌區間問題,時間複雜度壓縮的問題,可以考慮差分。
差分
繞過本題,我們看另外一題
問題描述:P2367 語文成績
給定一對 \(n,p\) ,表示有 \(n\) 名同學,需要進行 \(p\) 次操作。
每次操作給定 \(x,y,z\) 表示從第 \(x\) 名同學到第 \(y\) 同學加上 \(z\) 的分數。
問全班最低分?
滿足 \(1 \le n,p \le 5*10^6\)
淺淺談一下
乍一看,第一反應就是暴力。
暴力遍歷 \(x,y\) ,時間複雜度為 \(O(pn)\) ,明顯支援不了。
這裡考慮一種新的方法:差分。
我們設 \(cf_i=a_i-a_{i-1}\) ,特殊的,我們令 \(cf_1=a_1\) 。
明顯的,我們會發現:
於是,我們發現了一個很重要的結論,差分陣列的字首和等於原陣列。
我們在來看怎麼修改。
我們把求原陣列當成把差分陣列從前往後掃一遍。
假設我們要修改 \([x,y]\) 的值,我們從前往後掃,掃到 \(x\) 前都沒有問題,掃到了 \(x\) 了!誒,要加 \(w\) ,我們再次往後掃, \(y\) 前面也暢通無阻,但是掃到 \(y+1\) 的時候多了 \(w\) ,我們把他減掉,再往後掃,發現都沒問題了,因為一加一減抵消了!
所以,我們可以歸納得:修改 \([x,y]\) 時, \(cf_x++\) , \(cf_{y+1}--\) 。
所以,這道題我們就做出來了。
Code
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 5e6 + 7;
int n, p, a[MAXN], w[MAXN];
int main() {
cin >> n >> p;
for (int i = 1; i <= n; i ++) cin >> a[i], w[i] = a[i] - a[i - 1];
for (int i = 1; i <= p; i ++) {
int x, y, z;
cin >> x >> y >> z;
w[x] += z, w[y + 1] -= z;
}
int ans = 1e9, sum = 0;
for (int i = 1; i <= n; i++) sum += w[i], ans = min(ans, sum);
cout << ans << endl;
return 0;
}
回到抽象化的問題
我們重新看這題,是不是很簡單了?這不就是差分的模板嘛。
我們按照剛才的處理方法處理一遍,求下字首和,看看哪些是 \(1\) 統計就行。
回到原題
我們可以發現,我們剛才一個很重要的元素就是:差分陣列。
但是原題是: \(-2^{31} \le a,b \le 2^{31}\) ,怎麼搞陣列?
這裡就要用到另外一種方法:離散化。
又離開本題(QwQ)
問題描述:
給定一串數,求出每個數在數列中的排名。
滿足 \(-10^9 \le a_i \le 10^9\) 。
又淺淺談一下(TwT)?
我們發現問題求的是排名,很容易發現,排名符合兩個性質。
- 把很大的區間對映到很小的區間。
- 原陣列每兩個元素的大小關係不變。
只要遇到的問題符合這種性質,我們就考慮用離散化。
離散化三部曲
- 排序
一定要排序!!因為後面的函式需要排序,時間為 \(O(n\log_n)\) ,這裡不考慮值域(你看看值域多大?) - 去重
使用 \(unique\) 函式,使用格式: \(unique(a.begin(),a.end())\) 這裡 \(a.begin(),a.end()\) 分別指陣列的頭指標和尾指標,普通陣列用 \(a+1,a+1+n\)
注意一點:該函式的返回值是不屬於去重陣列的第一個地址,比如有個陣列長度為 \(n\) ,去重陣列長度為 \(k\) ,他的返回值就是 \(a_{k+1}\)的地址。
根據上面幾點,我們可以推出公式。
這樣我們就可以求出去重陣列的長度了。
3. 求名次了
我們這裡瞭解一個函式: \(lower_\ bound(a+1,a+1+n,x)\) 他返回的是第一個大於等於 \(x\) 的元素的地址,注意:必須有序。
於是我們就可以推出一個公式:( \(rk\) 陣列為名詞陣列)。
\(b\) 陣列為去重陣列, \(a\) 陣列為原陣列(應該可以理解吧……)。
至此,第二個分任務已經完成。
Code:
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 7;
int T, a[MAXN], b[MAXN], rk[MAXN];
int main() {
for (cin >> T; T; T--) {
int n;
cin >> n;
for (int i = 1; i <= n; i++) cin >> a[i];
memcpy(b + 1, a + 1, n * sizeof(int));
sort(b + 1, b + 1 + n);
int *ed = unique(b + 1, b + 1 + n);
for (int i = 1; i <= n; i++) {
rk[i] = lower_bound(b + 1, ed, a[i]) - (b + 1) + 1;
cout << rk[i] << ' ';
}
cout << endl;
}
return 0;
}
再一次回到原題
我們有離散化,差分,天下不就容易打了嗎?(bushi)
最後一次淺淺談一下
我們考慮把讀進來的 \(2n\) 個數全部進行離散化(先別問為什麼)。
這樣就會形成 \(2*n-1\) 個區間,我們把每一個區間當成一個元素進行差分。
比如 \(1\) 和 \(2\) 中間是 \(1\) 區間,所以要更新 \([l,r]\) 時,就是更新 \([l,r-1]\) 個區間,具體的:
最後,求出字首和,若第 \(i\) 區間之間是 \(\ge 1\) 的,就把 \(b[i+1] - b[i]\) 累加到 \(ans\) 裡就行。
所以,這道題我們就做完了。
但是真的做完了嗎?
question1
- 邊界條件考慮了嗎?
既然是左閉右開,右邊的就不算。
剛剛我在講的時候快速忽略了一個點,為什麼區間的長度為 \(b[i+1]-b[i]\) ,不是 \(b[i+1]-b[i]+1\) 嗎?
我們來舉一個例子:
區間 \([2,7)\) 分為 \([2,4),[4,7)\) 他們的和是不是相等的。
為什麼舉這個例子呢?如果你認真去做這道題目,發現離散化把他們拆成許多個小的區間,要把他們加起來湊成一個大區間,所以我才舉了這個例子。
可以發現 \([2,7)\) 為 \(2,3,4,5,6\) ,而 \([2,4),[4,7)\) 為 \(2,3,\ 4,5,6\) 發現是完全一樣的,所以作者左閉右開是幫助我們更好的寫程式碼(謝謝~~)
question2
這是一個擴充套件,有什麼公式可以用 \(b\) 和 \(rk\) 表示 \(a\) 嗎?
這裡是有的,這裡給出公式。
至於證明,請讀者(包括以後的我)自己思考,如果想不會請在評論區打出來,我會解答。
Code
#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e4 + 7;
int n, a[2 * MAXN], b[2 * MAXN], rk[2 * MAXN], cf[2 * MAXN], cnt;
int main() {
ios::sync_with_stdio(false);
cin.tie(NULL);
cin >> n;
for (int i = 1; i <= n; i ++)
cin >> a[++ cnt], cin >> a[++ cnt];
memcpy(b + 1, a + 1, 2 * n * sizeof(int));
sort(b + 1, b + 1 + 2 * n);
int k = unique(b + 1, b + 1 + 2 * n) - (b + 1);
for (int i = 1; i <= 2 * n; i++)
rk[i] = lower_bound(b + 1, b + 1 + k, a[i]) - b;
for (int i = 1; i <= 2 * n; i += 2) {
int r =rk[i + 1], l = rk[i];
cf[l] ++, cf[r] --;
}
int ans = 0, sum = 0;
for (int i = 1; i <= k; i++) {
sum += cf[i];
if (sum > 0) ans += b[i + 1] - b[i];
}
cout << ans;
return 扶蘇咕咕咕~~~;
}
完結撒花✿✿ヽ(°▽°)ノ✿