洛谷P1496 火燒赤壁【題解】

Phrvth發表於2023-01-16

事先宣告

本題解文字比較多,較為詳細,演算法為離散化差分,如會的大佬可以移步去別處看這道題的思路(因為作者比較懶,不想新開兩個專題)。

題目簡要

給定每個起火部分的起點終點,請你求出燃燒位置的長度之和
注意:左閉右開

淺淺來談一下

看到這道題時,你肯定有很多疑問。

  • 為什麼作者要搞左閉右開啊?

  • 有重複的區間怎麼搞?

  • 我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\)
明顯的,我們會發現:

\[\begin{aligned} \sum_{i=1}^ncf_i&=a_1 + (a_2-a_1) + (a_3-a_2) + ....(a_n-a_{n-1})\\ &=a_n \end{aligned} \]

於是,我們發現了一個很重要的結論,差分陣列的字首和等於原陣列

我們在來看怎麼修改。
我們把求原陣列當成把差分陣列從前往後掃一遍。

假設我們要修改 \([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)?

我們發現問題求的是排名,很容易發現,排名符合兩個性質。

  1. 把很大的區間對映到很小的區間。
  2. 原陣列每兩個元素的大小關係不變。

只要遇到的問題符合這種性質,我們就考慮用離散化。

離散化三部曲

  1. 排序
    一定要排序!!因為後面的函式需要排序,時間為 \(O(n\log_n)\) ,這裡不考慮值域(你看看值域多大?)
  2. 去重
    使用 \(unique\) 函式,使用格式: \(unique(a.begin(),a.end())\) 這裡 \(a.begin(),a.end()\) 分別指陣列的頭指標和尾指標,普通陣列用 \(a+1,a+1+n\)
    注意一點:該函式的返回值是不屬於去重陣列的第一個地址,比如有個陣列長度為 \(n\) ,去重陣列長度為 \(k\) ,他的返回值就是 \(a_{k+1}\)的地址。
    根據上面幾點,我們可以推出公式。

\[k=unique(a+1,a+1+n)-(a+1) \]

這樣我們就可以求出去重陣列的長度了。
3. 求名次了
我們這裡瞭解一個函式: \(lower_\ bound(a+1,a+1+n,x)\) 他返回的是第一個大於等於 \(x\) 的元素的地址,注意:必須有序
於是我們就可以推出一個公式:( \(rk\) 陣列為名詞陣列)。

\[rk[i] = lower_\ bound(b + 1, b + 1 + k, a[i]) - b; \]

\(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]\) 個區間,具體的:

\[cf_l++,cf_r-- \]

最後,求出字首和,若第 \(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\) 嗎?
這裡是有的,這裡給出公式。

\[b[rk[i]]=a[i] \]

至於證明,請讀者(包括以後的我)自己思考,如果想不會請在評論區打出來,我會解答。

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 扶蘇咕咕咕~~~;
}

完結撒花✿✿ヽ(°▽°)ノ✿

相關文章