slope tricl

yabnto發表於2024-07-13

一個奇奇怪怪的包

額,直接從題開始講吧。

P4597 序列 sequence

這道題,先說暴力

暴力

考慮 \(dp\),設 \(f_{ij}\) 表示考慮到第 \(i\) 個數,當前數改為 \(j\),那麼很明顯,我們可以透過列舉上一個 \(k\),來形成一個 \(O(n^3)\)\(dp\)

最佳化 1

我們發現,由於是單調不減,所以 \(k\) 是有取值範圍的,並且這個取值範圍是連續的,於是我們設 \(g_{ij}\) 表示在 \(f_{ik}\)\(k\) 小於 \(j\) 的所有的 \(f_{ik}\) 的最小值,所以轉移方程為:

\(f_{ij} = g_{ij} + |a[i]-j|\)

最佳化 2

啊,終於進正題了。

考慮將 \(f_i\) 變為一個函式,即對於每一個 \(j\)\(f_i\) 這個函式都有一個值,由於後面的絕對值必然是一個凸函式,而前面的 \(f\) 等都可以從 \(f_0\) 轉移,而 \(f_0\) 是條平線(額,這個沒必要解釋吧,兩個 0 相減不會有人不會算吧),所以在累加的過程中 \(f_i\) 必然是個凸函式。

所以我們要求 \(f_i\) 函式中的最低處,即 \(f_{i}\) 這個分段函式中斜率為 1 的時候。

那麼同理可以將 \(g_{i}\) 也視為一個函式,那麼轉移便成了將兩函式疊加,如何快速疊加呢?

考慮維護函式中的 \(x\) 最大的一段,記下它的解析式,然後每段記錄轉折,並記錄轉折次數。

突然想起,\(f_i\) 為凸函式,我們可將其形狀是為一個拋物線,而 \(g_i\) 的形狀可視為一個反比例函式(形象的視為,並不是真的),而 \(g_i\) 函式必然會從一個點開是變為一條射線,而這條射線,放到 \(f_i\) 處,便是我們要知道的東西,所以 \(f_i\),OUT!

其實我們也沒必要維護 \(g_i\) 的最後的解析式,直接將點記錄下來,反正在疊加的時候加的都是一個 \(k = 1 或 -1\) 的東西。

那麼我們設 \(h_{i}\)\(f_i(j)=g_i(j)\) 的時候即 \(f_i\) 的最低點。

考慮當新加點比 \(h_{i - 1}\)(說的是值啊) 等於或還大,那麼肯定沒有 \(h_{i - 1}\) 更優,直接丟進點列以後再說。

如果新加點比 \(h_{i - 1}\) 小,那麼由於是比 \(h_{i - 1}\) 更優,所以在加進點列是肯定會放在最前面(即 \(h_i\) 的值為新加點),且由於比 \(h_{i-1}\) 要小,最低的 \(x\) 的必然比 \(h_{i - 1}\) 的要靠前,此時出現了一個折返,便不是最優點了,所以刪掉,而此時的最後解析式和第一個轉折點都是新加點,需要加兩次。

code

#include <iostream>
#include <queue>

using namespace std;

const int MaxN = 5e5 + 10;

int a[MaxN], n;
long long ans;
priority_queue<int> q; // 雖然說這個函式會一點點變小,不過由於哪裡的為 k = -1 的,所以反過來就是一點點變大啦!

int main() {
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
    q.push(a[i]); // 不是要加兩次嗎?我這裡投個懶
    if (a[i] < q.top()) {
      ans += q.top() - a[i]; // 記得要算貢獻,只有將 a[i] 提上來才能保證單調不減
      q.pop();
      q.push(a[i]);
    }
  }
  cout << ans << endl;
  return 0;
}

CF713C Sonya and Problem Wihtout a Legend

哇,自己會寫模板了,好厲害!結果一看,哇單調遞增,吐血三升……

思路

由於我們指揮單調不減,所以可以將單調遞增轉化為單調不減。

反著來考慮,如果是單調不減,那麼會有相同的地方,所以加一個 \(i\) 即可,那麼單調遞增就反過來減一個 \(i\) 即可,其餘一樣。