【斜率優化】【P5468】 [NOI2019]回家路線

Description

給定 \(n\) 點,這 \(n\) 個點由 \(m\) 班列車穿插連結。對於第 \(i\) 班列車,會在 \(p_i\) 時刻從 \(x_i\) 站點出發開向 \(y_i\) 站點,到站時間爲 \(q_i\)。現在從 \(1\) 號節點出發,經過多次換乘到達節點 \(n\)。一次換乘是指對於兩班列車,假設分別爲 \(u\) 號與 \(v\) 號列車,若 \(y_u = x_v\) 並且 \(q_u \leq p_v\),那麼小貓可以乘坐完 \(u\) 號列車後在 \(y_u\) 號站點等待 \(p_v - q_u\) 個時刻,並在時刻 \(p_v\) 乘坐 \(v\) 號列車。要求最小化路程中的煩躁值。煩躁值的計算方法是:給定參數 \(A,B,C\),對於每次等待,假設等待了 \(t\) 個時刻,那麼煩躁值增加 \(A t^2 + B t + C\)。另外如果在時刻 \(T\) 到達節點 \(n\),則狂躁值再增加 \(T\)

Limitations

\(1 \leq n,~m \leq 2 \times 10^5\)

\(0 \leq A \leq 10,~~0 \leq B,~C \leq 10^6\)

\(1 \leq p_i \leq q_i \leq 10^3\)

保證 \(1\) 可以到達 \(n\)

Solution

好像正解是以時間爲狀態DP來着……然而拿到這個題的第一反應是DP每條邊到 \(n\) 的貢獻。

考慮設 \(f_i\) 是第 \(i\) 條邊開始出發,到達 \(n\) 號節點對煩躁值的最小貢獻。於是有轉移方程:

\[f_i = \min_{p_j \geq q_i} ^{x_j = y_i} \{f_j + A (p_j - q_i)^2 + B(p_j - q_i) + C + p_j - q_i\} + q_i - p_i\]

\[f_i = \min_{p_j \geq q_i} ^{x_j = y_i} \{f_j + A p_j^2 - 2Ap_jq_i + Aq_i^2 + Bp_j - Bq_i + C + p_j - q_i\} + q_i - p_i\]

將與 \(j\) 無關的項提出大括號,得到

\[f_i = \min_{p_j \geq q_i} ^{x_j = y_i} \{f_j + A p_j^2 - 2Ap_jq_i + Bp_j + p_j \} + q_i - p_i+ Aq_i^2- q_i- Bq_i + C \]

整理得

\[f_i = \min_{p_j \geq q_i} ^{x_j = y_i} \{f_j + A p_j^2 - 2Ap_jq_i + (B+1)p_j\} + Aq_i^2- Bq_i + C- p_i \]

\(g_i = A q_i^2 - Bq_i - p_i + C\)\(h_i = Ap_i^2 + (B+1)p_j\)

上式即爲

\[f_i = f_j + h_j - 2Ap_jq_i + g_i\]

移相得到

\[f_j + h_j = 2Aq_iq_j + f_i - g_i\]

注意到 \(g_i\) 是一個與 \(i\) 有關的常數,那麼最小化 \(f_i\) 只需要最小化 \(f_i + g_i\)

如果將 \(f_j + h_j\) 看作縱座標, \(q_j\) 看作橫座標,上述方程可以看成一條斜率爲常數 \(2Aq_i\) 的直線,在所有滿足條件的 \(j\) 中選擇一個點,使得直線過這個點,最小化直線在 \(y\) 軸上的截距 \(f_i + g_i\)

那麼所有滿足條件的點顯然在一個下突殼上,證明上可以考慮如果一個點不在下突殼上那麼能找到一個更優的 \(j\)

然後考慮如果 \(i\) 會從 \(j\) 轉移過來,那麼一定有 \(q_i \leq p_j\),又因爲 \(p_j < q_j\),因此 \(q_i < q_j\),於是按照 \(q\) 的不升序進行排序即可。

在轉移時,只需要在每個點維護一個凸殼表示所有可能被選擇的點,然後再維護一個 set 記錄該點上已經被計算但是沒有插入凸殼的點。set 內部按照 \(p\) 的不升序排序,每次要轉移一條邊 \(i\) 的時候先將終點中 \(p\) 不小於 \(q_i\) 的邊插入凸殼,由於轉移是按照 \(q_i\) 的順序進行的,已經被插入凸殼的點一定是合法可以轉移的。

另外注意到 \(q_i\) 是單調不升的,於是斜率 \(2Aq_i\) 也是單調升的,再考慮到插入的橫軸 \(p_j\) 也是單調不升的,因此可以使用單調隊列維護每個點的凸殼即可。

需要注意的一點細節是凸殼的橫軸是從大到小插入的,在維護的時候不要把大於號小於號寫反。

一共進行了 \(m\) 次轉移,每次轉移複雜度 \(O(1)\),因此 DP 過程的時間複雜度是 \(O(m)\),但是由於進行了排序,且每條邊插入在 set 中 1 次,所以整個算法的時間複雜度 \(O(m \log m)\)

Code

#include <cstdio>
#include <set>
#include <queue>
#include <algorithm>

const int maxn = 200005;
const ll INF = 1ll << 50;

int n, m;
ll A, B, C, ans;
ll frog[maxn], g[maxn], h[maxn];

struct M {
  int x, y, p, q;

  inline bool operator<(const M &_others) const {
    return this->q > _others.q;
  }
};
M MU[maxn];

struct Cmp {
  inline bool operator() (const int &_a, const int &_b) {
    if (MU[_a].p != MU[_b].p) {
      return MU[_a].p > MU[_b].p;
    } else {
      return _a < _b;
    }
  }
};

std::deque<int>Q[maxn];
std::set<int, Cmp>s[maxn];

int query(const int x);
void free(const int x, const int y);
void insert(const int x, const int y);

signed main() {
  freopen("route.in", "r", stdin);
  freopen("route.out", "w", stdout);
  qr(n); qr(m); qr(A); qr(B); qr(C);
  for (int i = 1; i <= m; ++i) {
    qr(MU[i].x); qr(MU[i].y); qr(MU[i].p); qr(MU[i].q);
  }
  frog[m + 1] = 1ll << 50;
  std::sort(MU + 1, MU + 1 + m);
  for (int i = 1; i <= m; ++i) {
    g[i] = A * MU[i].q * MU[i].q - B * MU[i].q -  MU[i].p + C;
    h[i] = A * MU[i].p * MU[i].p + (B + 1) * MU[i].p;
  }
  for (int i = 1; i <= m; ++i) if (MU[i].y == n) {
    frog[i] = MU[i].q - MU[i].p;
    s[MU[i].x].insert(i);
  } else {
    int y = MU[i].y;
    free(y, MU[i].q);
    int j = query(i);
    if (!j) {
      frog[i] = INF;
      continue;
    }
    frog[i] = frog[j] + g[i] + h[j] - ((A * MU[i].q * MU[j].p) << 1);
    s[MU[i].x].insert(i);
  }
  ans = 1ll << 50;
  for (int i = 1; i <= m; ++i) if (MU[i].x == 1) {
    ans = std::min(frog[i] + A * MU[i].p * MU[i].p + B * MU[i].p + C + MU[i].p, ans);
  }
  qw(ans, '\n', true);
  return 0;
}

void free(const int x, const int y) {
  while (!s[x].empty()) {
    auto u = *s[x].begin();
    if (MU[u].p >= y) {
      insert(x, u);
      s[x].erase(u);
    } else {
      break;
    }
  }
}

inline std::pair<ll, ll> calc(const int x) {
  return std::make_pair(frog[x] + h[x], 1ll * MU[x].p);
}

inline bool judge(const int x, const int y, const int z) {
  auto i = calc(x), j = calc(y), k = calc(z);
  return (k.first <= j.first) && ((i.first - k.first) * (i.second - j.second) < (i.first - j.first) * (i.second - k.second));
}

void insert(const int x, const int y) {
  while (Q[x].size() > 1) {
    int i = Q[x].back(); Q[x].pop_back(); int j = Q[x].back();
    if (judge(j, i, y)) {
      Q[x].push_back(i); break;
    }
  }
  Q[x].push_back(y);
}

int query(const int p) {
  int x = MU[p].y;
  while (Q[x].size() > 1) {
    int i = Q[x].front(); Q[x].pop_front(); int j = Q[x].front();
    auto s = calc(i), t = calc(j); ll k = ((A * MU[p].q) << 1);
    if ((k * (s.second - t.second)) > (s.first - t.first)) {
      Q[x].push_front(i);
      break;
    }
  }
  return Q[x].size () ? Q[x].front() : 0;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章