1. 程式人生 > 其它 >[NOI2019] 回家路線 題解

[NOI2019] 回家路線 題解

[NOI2019] 回家路線 題解

第一次這麼深入理解斜率優化的習題。

題意

現在有 \(n\) 個城市,城市之間有 \(m\) 條火車可以到達。

第i條火車是從第 \(x_i\) 出發併到達 \(y_i\),是在 \(p_i\) 時間出發,並在 \(q_i\) 時間到達。

火車只能夠在前一輛到達後才能乘坐。

設共乘坐了k輛火車,那麼他的代價是\(q_{s_{k}}+\left(A \times p_{s_{1}}^{2}+B \times p_{s_{1}}+C\right)+\sum_{j=1}^{k-1}\left(A\left(p_{s j+1}-q_{s_{j}}\right)^{2}+B\left(p_{s_{j+1}}-q_{s_{j}}\right)+C\right)\)

.

求最小代價。

資料範圍

原版:\(2≤n≤10^5,1≤m≤2×10^5,0\le p_i<q_i \le 10^3\)

加強版:\(2≤n≤10^5,1\le m\le 10^6,0\le p_i<q_i\le 4\times 10^4\)

題解

對於原版這個極小的資料範圍,有優雅的暴力dp方法:

\(dp(i,j)\) 為在時間 \(j\) 到達 \(i\) 號點最優方案。

把所有火車按 \(p\)\(q\) 排序後列舉火車進行轉移即可。

複雜度 \(O(n\times q_i)\) 。勉強可過。


但是對於加強版呢?

我們來考慮使用斜率優化dp。

\(dp_i\) 表示乘坐第 \(i\)

輛火車後最小代價。

那麼有狀態轉移方程:

\[dp_i = \min\limits_{y_j=x_i\ ,\ q_j\le p_i}\{dp_j+A\times(p_i-q_j)^2 + B\times(p_i-q_j)+C \} . \]

嘗試把這個式子化簡一下:

\[dp_i = dp_j+A\times p_i^2-2A\times p_i\cdot q_j+A\times q_j^2 + B\times p_i-B\times q_j+C .\qquad ({y_j=x_i\ ,\ q_j\le p_i}) \]

移項可得:

\[dp_j+A\cdot q_j^2-B\cdot q_j = (2A\cdot p_i\cdot q_j)+(dp_i-A\cdot p_i^2-B\cdot p_i-C). \]

這是斜率優化的標準式子。其中

\[y=dp_j+A\cdot q_j^2-B\cdot q_j,\\ kx = (2A\cdot p_i\cdot q_j),\\ b = (dp_i-A\cdot p_i^2-B\cdot p_i-C) \]

但是這樣就忽略了限制條件 \(({y_j=x_i\ ,\ q_j\le p_i})\).

  • 對於前一個限制:
    • 我們對於每一個節點 \(i\) 都維護一個下凸包。這樣對於每一輛車 \(i\) ,它從 \(x_i\) 轉移,並且計入 \(y_i\) 所在的凸包裡。
    • 這個過程就不能自己開陣列來解決了,必須藉助vector。
  • 對於第二個限制:
    • 首先我們考慮:最開始一定要進行一步排序,讓 \(p_i\) 單調遞增。因為 \(p_i\ge q_j\, , \ q_j > p_j \Rightarrow p_i>p_j\) 可以考慮所有情況。
    • 既然 \(p_i\) 已經是單調遞增的了,這裡計算需要讓凸包中所有的 \(q_j\le p_j\)\(j\) 都存在於凸包中。
    • 我們用一個優先佇列維護已經轉移過的 \(j\) ,按 \(q\) 排序 。對於一個準備計算的 \(i\) ,將所有滿足第二個條件的 \(j\) 塞入所屬凸包內。

整理一下思路:

  • 對於一個 \(i\) ,先將優先佇列內滿足 \(q_j\le p_i\) 的都取出放入凸包內。
  • 使用斜率優化轉移求 \(dp_i\)
  • \(i\) 放入優先佇列內等待轉移給其他量。

思考

這道題提供了很好的思路。

當用一個數據結構來優化dp時,一個狀態被轉移完,如果還不符合用它轉移其它狀態的要求,就先用另一個數據結構存起來。(本題使用優先佇列)

另外,現在才發現自己在優先佇列優先順序設定上還有知識漏洞。

這裡簡單概述一下:優先佇列是優先順序大的在前。理解為與 sortcmp 相反。

還有,斜率優化時應儘量把除化乘,避免精度丟失。(可能導致巨大的失分!)

這道題是一道很好的題,大概調了兩個小時才出來。它是值得的。

程式碼

#include <bits/stdc++.h>
#define fo(a) freopen(a".in","r",stdin),freopen(a".out","w",stdout);
using namespace std;
const int INF = 0x3f3f3f3f, N = 4e6+5, M = 4e6+5;
typedef long long ll;
typedef unsigned long long ull;
inline ll read(){
  ll ret = 0 ; char ch = ' ' , c = getchar();
  while(!(c >= '0' && c <= '9')) ch = c , c = getchar();
  while(c >= '0' && c <= '9')ret = (ret << 1) + (ret << 3) + c - '0' , c = getchar();
  return ch == '-' ? -ret : ret;
}
#define int ll
int n,m,A,B,C;
struct Edge{int u,v,p,q,id;}e[M];
inline bool sotcmp(const Edge a,const Edge b){return a.p < b.p;}
struct pqcmp{inline bool operator () (const Edge a,const Edge b){return a.q > b.q;}};
vector<int> vc[N];
priority_queue<Edge,vector<Edge>,pqcmp> pq;
int dp[M];
inline int f(int x){return dp[x] + A*e[x].q*e[x].q - B*e[x].q;}
inline int X(int x){return e[x].q;}
inline void push(Edge a){
	int v = a.v, id = a.id;
	while(vc[v].size() >= 2){
		int x = vc[v][vc[v].size()-2], y = vc[v][vc[v].size()-1];
		if((f(x)-f(y)) * (X(x)-X(id)) >= (f(x)-f(id)) * (X(x)-X(y))) vc[v].pop_back(); 
		else break;
	}
	vc[v].push_back(id);
}
int ans = 4e16;
signed main(){
	n = read(), m = read(), A = read(), B = read(), C = read();
	for(int i = 1 ; i <= m ; i ++){
		int u = read(), v = read(), p = read(), q = read();
		e[i] = (Edge){u,v,p,q,i};
	}
	sort(e+1,e+m+1,sotcmp);
	vc[1].push_back(0);
	dp[0] = 0;
	while(!pq.empty()) pq.pop();
	for(int i = 1 ; i <= m ; i ++){
		int u = e[i].u, v = e[i].v, p = e[i].p, q = e[i].q; e[i].id = i;
		while(!pq.empty() && pq.top().q <= p) push(pq.top()), pq.pop();
		while(vc[u].size() >= 2){
			int x = vc[u][0], y = vc[u][1];
			if(1.0*(f(x)-f(y))/(X(x)-X(y)) <= 2*A*p) vc[u].erase(vc[u].begin());
			else break;
		}
		if(vc[u].size()){
			int j = vc[u][0];
			dp[i] = dp[j] + A*(e[i].p-e[j].q)*(e[i].p-e[j].q) + B*(e[i].p-e[j].q) + C;
			pq.push(e[i]);
			if(v == n) ans = min(ans,dp[i]+q);
		}
	}
	printf("%lld",ans);
}