CF1420E Battle Lemmings 斜率優化


題目來源:CodeforcesCodeforces Round #672 (Div. 2),CF1420,E 題,Battle Lemmings。

題目大意

題目鏈接

有一排 \(n\) 個[林檎實],有些[林檎實]拿着盾牌,有些沒有。我們稱一對[林檎實]是被保護的,當且僅當它們都沒有拿盾牌,且在他們之間有人拿着盾牌。

你可以進行若干次操作,每次操作是如下兩種之一:

  • 選擇一個拿着盾牌,且他左邊的人沒盾牌的[林檎實] \(i\) (\(1< i\leq n\)),將第 \(i\) 個[林檎實]的盾牌給他左邊的人。
  • 選擇一個拿着盾牌,且他右邊的人沒盾牌的[林檎實] \(j\) (\(1\leq j<n\)),將第 \(j\) 個[林檎實]的盾牌給他右邊的人。

現在,給你一個長度為 \(n\)\(01\) 序列,表示初始時每個[林檎實]是否拿着盾牌。

[林檎實]的妹子隨時會過來找他約會,所以他也不知道他還能進行幾次操作。因此,對於所有 \(k\in[0,\frac{n(n-1)}{2}]\),請算出做不超過 \(k\) 次操作時,最多能有多少對被保護的[林檎實]。也就是說,你需要輸出 \(\frac{n(n-1)}{2}\) 個數。

數據范圍:\(1\leq n\leq 80\)

本題題解

設沒拿盾牌的點數量為 \(c_0\),拿着盾牌的點數量為 \(c_1\)

初步轉化 1:考慮兩個 \(0\) 不是被保護的,當且僅當它們之間沒有 \(1\)。也就是說,被保護的對數 = 總對數 - 在同一段里的 \(0\) 的對數。具體來說,設有 \(k\) 段極長、連續的 \(0\),長度分別為 \(l_1,l_2,\dots,l_k\),則被保護的對數 \(=\frac{c_0(c_0-1)}{2}-\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\)。於是我們只要最小化 \(\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\) 即可。

初步轉化 2:考慮已知一個目標局面(也就是一個 \(01\) 序列),那么從初始局面到這個目標局面的最少操作次數,顯然就是兩個局面里 第一個 \(1\)、第二個 \(1\)、...、第 \(c_1\)\(1\) 分別的坐標差之和。也就是說,初始局面的每個 \(1\) 和目標局面的每個 \(1\),按順序依次對應,可以證明這樣操作是最優的(調整法)。記初始局面里每個 \(1\) 的位置分別為 \(p_1,p_2,\dots,p_{c_1}\)

有了上述兩個結論,我們可以通過 DP 來確定目標序列,順便求出未被保護的對數\(\sum_{i=1}^{k}\frac{l_i(l_i-1)}{2}\))和操作次數

\(dp[i][x][y][z]\) 表示考慮了前 \(i\) 位,用了 \(x\) 次操作,前 \(i\) 位里總共有 \(y\)\(1\),從最后一個 \(1\)\(i\) 之間這最后一段 \(0\) 的長度為 \(z\),這個局面下未被保護的對數的最小值。轉移時考慮第 \(i+1\) 位是填 \(0\) 還是填 \(1\) 即可。時間、空間復雜度都是 \(O(n^5)\)(因為 \(x\) 這一維大小是 \(\frac{n(n-1)}{2}=O(n^2)\) 的,不要忘了)。可以用滾動數組優化空間,不過這種做法時間、空間常數都較大。

考慮簡化狀態定義。設 \(dp[i][x][y]\) 表示考慮了前 \(i\) 位,\(i\) 位上填 \(1\),用了 \(x\) 次操作,前 \(i\) 位里總共有 \(y\)\(1\),這個局面下未被保護的對數的最小值。轉移時枚舉下一個 \(1\) 在哪里。這樣時間復雜度依然是 \(O(n^5)\),但空間復雜度優化到了 \(O(n^4)\)。本做法的 AC 代碼請見【參考代碼1】。

繼續優化。考慮先枚舉 \(x,y\),再枚舉 \(i\)。此時能轉移到 \(dp[i][x][y]\) 的,一定是 \(x'=x-|p_y -i|\)\(y'=y-1\)。也就是說 \(x'\)\(y'\) 都是確定的,我們只需要找到最優的 \(j\) (\(j<i\)),然后用 \(dp[j][x'][y']\) 更新 \(dp[i][x][y]\) 即可。\(O(n^5)\) 的做法相當於是枚舉了 \(j\)。考慮如何不枚舉,快速求出最優的 \(j\)

具體來說,我們先觀察一下轉移式:

\[dp[i][x][y] = \min_{j<i}\{dp[j][x'][y'] + \frac{(i - j - 1)(i - j - 2)}{2}\} \]

首先可以去掉“除以 \(2\)”,在求答案的時候一起除。另外,因為 \(x,y,x',y'\) 都是常數,不妨將它們寫在前面。於是我們重新定義狀態:\(f_{x,y}(i) = dp[i][x][y]\times2\)。然后把上式中的 \((i - j - 1)\cdot (i - j - 2)\) 拆開,得到:

\[f_{x,y}(i) = \min_{j<i}\{f_{x',y'}(j)+i^2-3i+j^2+3j-2ij+2\} \]

其中 \(i^2\), \(-3i\), \(2\) 都是常數,可以提出來。關鍵的部分是 \(-2ij\),可以用斜率優化搞定它。具體來說,把每個 \(j\) 看成二維平面上一個坐標為 \((j,f_{x',y'}(j)+j^2+3j)\) 的點,轉移看成一條斜率為 \(2i\) 的直線,我們要最小化直線的截距(也就是 \(f_{x,y}(i)\) 加上一堆關於 \(i\) 的常數)。寫成式子就是:

\[f_{x',y'}(j)+j^2+3j=2ij+f_{x,y}(i)-i^2+3i-2 \]

對每個 \((x',y')\) 分別維護一個下凸殼,然后在凸殼上二分出第一段斜率大於 \(2i\) 的位置即可。

時間復雜度 \(O(n^4\log n)\)。空間復雜度 \(O(n^4)\)。本做法的 AC 代碼請見【參考代碼2】。

參考代碼

參考代碼1:時間復雜度 \(O(n^5)\),空間復雜度 \(O(n^4)\)

// problem: CF1420E
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 80;
const int INF = 0x3f3f3f3f;
int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
int total, max_op;
int dp[MAXN + 5][MAXN * (MAXN - 1) / 2 + 5][MAXN + 5];
int calc(int len) { return len * (len - 1) / 2; }

int main() {
	cin >> n;
	for(int i = 1; i <= n; ++i) {
		cin >> a[i];
		cnt[a[i]]++;
		if(a[i] == 1) {
			pos[cnt[1]] = i;
		}
	}
	total = calc(cnt[0]);
	max_op = calc(n);
	if(!cnt[1]) {
		for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
		return 0;
	}
	
	memset(dp, 0x3f, sizeof(dp));
	for(int i = 1; i < n; ++i) {
		dp[i][abs(pos[1] - i)][1] = calc(i - 1);
		for(int j = 0; j <= max_op; ++j) {
			for(int k = 1; k <= i && k < cnt[1]; ++k) if(dp[i][j][k] != INF) {
				int rest = cnt[1] - k;
				for(int l = i + 1; l <= n - rest + 1; ++l) {
					int newj = j + abs(pos[k + 1] - l);
					if(newj <= max_op)
						ckmin(dp[l][newj][k + 1], dp[i][j][k] + calc(l - i - 1));
				}
			}
		}
	}
	int res = total;
	for(int j = 0; j <= max_op; ++j) {
		for(int i = cnt[1]; i <= n; ++i) if(dp[i][j][cnt[1]] != INF) {
			ckmin(res, dp[i][j][cnt[1]] + calc(n - i));
		}
		cout << total - res << " ";
	}
	cout << endl;
	return 0;
}

參考代碼2:時間復雜度 \(O(n^4\log n)\) 斜率優化做法。

// problem: CF1420E
#include <bits/stdc++.h>
using namespace std;

#define pb push_back
#define mk make_pair
#define lob lower_bound
#define upb upper_bound
#define fi first
#define se second
#define SZ(x) ((int)(x).size())

typedef unsigned int uint;
typedef long long ll;
typedef unsigned long long ull;
typedef pair<int, int> pii;

template<typename T> inline void ckmax(T& x, T y) { x = (y > x ? y : x); }
template<typename T> inline void ckmin(T& x, T y) { x = (y < x ? y : x); }

const int MAXN = 80, MAXOP = MAXN * (MAXN - 1) / 2;
const int INF = 0x3f3f3f3f;
int n, a[MAXN + 5], cnt[2], pos[MAXN + 5];
int total, max_op;
int dp[MAXOP + 5][MAXN + 5][MAXN + 5];
struct Queue {
	int a[MAXN + 5];
	int ql, qr;
	int size() { return qr - ql + 1; }
	bool empty() { return ql > qr; }
	int front() { return a[ql]; }
	int back() { return a[qr]; }
	int back2() { return a[qr - 1]; }
	void push_back(int e) { a[++qr] = e; }
	void pop_back() { --qr; }
	void init() { ql = 1; qr = 0; }
}que[MAXOP + 5][MAXN + 5];

int calc(int len) { return len * (len - 1) / 2; }
int calc2(int len) { return len * (len - 1); }

int main() {
	cin >> n;
	for(int i = 1; i <= n; ++i) {
		cin >> a[i];
		cnt[a[i]]++;
		if(a[i] == 1) {
			pos[cnt[1]] = i;
		}
	}
	total = calc(cnt[0]);
	max_op = calc(n);
	if(!cnt[1]) {
		for(int i = 0; i <= max_op; ++i) cout << 0 << " "; cout << endl;
		return 0;
	}
	
	memset(dp, 0x3f, sizeof(dp));
	for(int j = 0; j <= max_op; ++j) {
		for(int k = 1; k <= cnt[1]; ++k) {
			que[j][k].init();
		}
	}
	for(int i = 1; i <= n; ++i) {
		#define Q que[abs(pos[1] - i)][1]
		#define F dp[abs(pos[1] - i)][1]
		#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
		
		F[i] = calc2(i - 1);
		while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
			Q.pop_back();
		Q.push_back(i);
		
		#undef Q
		#undef F
		#undef Y
	}
	for(int j = 0; j <= max_op; ++j) {
		for(int k = 2; k <= cnt[1]; ++k) {
			for(int i = k; i <= n - (cnt[1] - k); ++i) {
				if(j < abs(pos[k] - i)) continue;
				int last_j = j - abs(pos[k] - i);
				#define Q que[last_j][k - 1]
				#define F dp[last_j][k - 1]
				#define Y(p) (F[(p)] + (p) * (p) + 3 * (p))
				if(Q.empty()) continue;
				if(Q.front() >= i) continue;
				int l = Q.ql;
				int r = Q.qr;
				while(l < r) {
					int mid = (l + r + 1) >> 1;
					if(Q.a[mid] < i) {
						l = mid;
					} else {
						r = mid - 1;
					}
				}
				l = Q.ql;
				while(l < r) {
					int mid = (l + r) >> 1;
					int p1 = Q.a[mid], p2 = Q.a[mid + 1];
					if(Y(p2) - Y(p1) > 2 * i * (p2 - p1)) {
						r = mid;
					} else {
						l = mid + 1;
					}
				}
				ckmin(dp[j][k][i], F[Q.a[l]] + calc2(i - Q.a[l] - 1));
				#undef Q
				#undef F
				
				#define Q que[j][k]
				#define F dp[j][k]
				while(Q.size() >= 2 && (Y(Q.back()) - Y(Q.back2())) * (i - Q.back()) > (Y(i) - Y(Q.back())) * (Q.back() - Q.back2()))
					Q.pop_back();
				Q.push_back(i);
				
				#undef Q
				#undef F
				#undef Y
			}
		}
	}
	int res = total;
	for(int j = 0; j <= max_op; ++j) {
		for(int i = cnt[1]; i <= n; ++i) if(dp[j][cnt[1]][i] != INF) {
			ckmin(res, (dp[j][cnt[1]][i] + calc2(n - i)) / 2);
		}
		cout << total - res << " ";
	}
	cout << endl;
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM