【二分答案】P2390 地標訪問

Weekoder發表於2024-06-09

\(\color{black}\text{P2390 地標訪問 (傳送門)}\)

學過區間 DP 的,看到這題的第一反應都是:訪問的地標一定是一個區間,並且在不斷擴大,區間 DP!可看到資料範圍,又瞬間放棄了。與 P1220 關路燈 不同,這題由於沒有電量的消耗等額外因素,有這樣一個小性質:

  • 貝西的行走路線只可能是三種:一路向左,一路向右或者在中途折返一次。

一路向左和一路向右倒還好理解,可為什麼最多隻會折返一次呢?考慮以下情況:

如圖,貝西從起點出發,折返了多次以訪問所有地標。可問題是,以下做法不僅能滿足要求,也更優:

換句話說,由於我們訪問的地標總是一個區間,而從某個點開始走完整個區間分為三種情況:

  1. 起點在區間左邊,一路向右;
  2. 起點在區間右邊,一路向左;
  3. 起點在區間裡,先前往比較近的端點(左端點或者右端點),再前往另一個端點走完整個區間。

那麼,現在問題來了:我們到底能走多少個地標呢?這取決於我們擁有的時間 \(t\)。換句話說,這是在滿足條件(所用時間 \(\bm{\le t}\))的情況下找最值。這不就是一個二分答案嗎?而且單調性也是顯然的:如果不能訪問 \(x\) 個地標,那也訪問不了 \(y(y>x)\) 個地標;如果能訪問 \(x\) 個地標,那也訪問的了 \(y(y<x)\) 個地標。

考慮二分答案。難點在於,如何設計 \(\text{check}\) 函式?假設當前二分答案猜測可以訪問 \(X\) 個地標,則需要選擇連續的 \(X\) 個地標(地標已經按座標大小排序),即 \(x_1,x_2,\dots,x_X\) 或者 \(x_2,x_3,\dots,x_{X+1}\) 或者 \(x_3,x_4,\dots,x_{X+2}\ \dots\) 我們可以列舉這些區間的終點 \(i\in[X,n]\),則區間的起點為 \(st=i-X+1\)。考慮上面的三種情況:一路向右,一路向左,起點在區間裡,只要有一個滿足條件就返回 \(\text{True}\)。條件判斷的表示式分別為:

x[st] >= 0 && x[i] <= t

x[i] <= 0 && abs(x[st]) <= t

x[st] <= 0 && x[i] >= 0 && min(abs(x[st]), x[i]) + x[i] - x[st] <= t

完全再現了上面的三種情況。

如此一來,程式碼也就非常好寫了:

#include <bits/stdc++.h>

#define int long long

using namespace std;

const int N = 5e4 + 5;

int t, n, a[N];

bool check(int X) {
	for (int i = X; i <= n; i++) {
		int st = i - X + 1;
		if (a[st] >= 0 && a[i] <= t)
			return 1;
		else if (a[i] <= 0 && abs(a[st]) <= t)
			return 1;
		else if (a[st] <= 0 && a[i] >= 0 && min(abs(a[st]), a[i]) + a[i] - a[st] <= t)
			return 1;
	}
	return 0;
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> t >> n;
	for (int i = 1; i <= n; i++)
		cin >> a[i];
	sort(a + 1, a + 1 + n);
	int l = -1, r = 5e4 + 1;
	while (l + 1 < r) {
		int mid = l + r >> 1;
		if (check(mid))
			l = mid;
		else
			r = mid;
	}
	cout << l;
	return 0;
}

相關文章