Solution -「多校聯訓」戰神歸來
\(\mathcal{Description}\)
Link.
一條地鐵線路上共 \(m\) 個站點,\(n\) 個人乘坐地鐵,第 \(i\) 個人需要從 \(s_i\) 站坐到 \(e_i\) 站。你可以指揮他們在保證不走回頭路的情況下走到某個站,或指揮處於同一個站的兩人交換地鐵卡。一張從 \(x\) 站進站 \(y\) 站出站的地鐵卡花費為 \(|x-y|\),最小化花費和並給出可行方案。
\(n\le10^5\),\(m\le10^6\),方案步驟數 \(\le 4\times10^5\)。
\(\mathcal{Solution}\)
我切掉了,但沒有完全切掉。
畫畫圖,在數軸上,畫出向右行動的藍色箭頭和向左行動的紅色箭頭。注意到換卡本質上是交換始發站點,那麼重合的紅藍箭頭是能相互抵消的!如圖:
\(\vec{AB},\vec{CD}\) 互換起點,變為 \(\vec{AE},\vec{CF}\),長度之和減少 \(2|DF|\),並且 \(\vec{AE},\vec{FC}\) 仍舊能與其他向量進行互換操作。
不難證明,對於數軸上 \([k,k+1]\),覆蓋它的紅、藍向量的數量的較小者就是這段區間被抵消的次數,且我們一定能構造方案取到這一下界,用向量總長減去它就求得最小花費。
考慮方案的構造,注意到次數限制 \(4\times10^5=4\max\{n\}\),猜測“抵消”操作能夠在 \(n\) 次換卡操作內完成,那麼每次換卡就得讓一個向量無法再與其他向量抵消。理性分析,我們分別維護藍色向量和紅色向量的小根堆,按 \((\text{左側點},\text{右側點})\)
-
\(e(\boldsymbol u)\le s(\boldsymbol v)\),即紅色向量右端點靠右,如圖:
\(\boldsymbol u=\vec{AB}\) 用完必然被丟掉,而 \(\vec{FD}\) 會被抵消,我們可以放心地讓 \(A,C\) 在 \(B(F)\) 點會合換卡。但是!不能走回頭路的限制帶來一個問題:若存在 \(\vec{GH}\),\(C\) 就無法與 \(G\) 換卡了!
解決方法形象而自然:紅色向量向左而掃描方向向右,那麼紅色向量上的會合點不得不會從左到右出現,那我們反過來以棧的順序指揮紅色向量不就好啦?
-
\(e(\boldsymbol u)>s(\boldsymbol v)\),即藍色向量的右端靠右,與上個情況恰好相反,我們需要立即將操作方案加入答案再做後續計算。
最後,複雜度 \(\mathcal O(n\log n)\) 就能構造好方案啦。
\(\mathcal{Code}\)
/* Clearink */
#include <queue>
#include <cstdio>
#include <vector>
#include <cassert>
#define rep( i, l, r ) for ( int i = l, rep##i = r; i <= rep##i; ++i )
#define per( i, r, l ) for ( int i = r, per##i = l; i >= per##i; --i )
inline int rint() {
int x = 0, s = getchar();
for ( ; s < '0' || '9' < s; s = getchar() );
for ( ; '0' <= s && s <= '9'; s = getchar() ) x = x * 10 + ( s ^ '0' );
return x;
}
template<typename Tp>
inline void wint( const Tp x ) {
if ( 9 < x ) wint( x / 10 );
putchar( x % 10 ^ '0' );
}
inline int imin( const int a, const int b ) { return a < b ? a : b; }
inline int imax( const int a, const int b ) { return a < b ? b : a; }
typedef long long LL;
const int MAXN = 1e5, MAXM = 1e6;
int n, m, s[MAXN + 5], e[MAXN + 5], fin[MAXN + 5];
struct Atom {
int l, r, id;
inline bool operator < ( const Atom& t ) const {
return !( l != t.l ? l <= t.l : ( r != t.r ? r <= t.r : id <= t.id ) );
}
};
std::priority_queue<Atom> seg[2];
std::vector<Atom> plan;
inline void action( const Atom& u, const Atom& v, const int p ) {
if ( u.l != p ) plan.push_back( { u.id, p, 0 } );
if ( v.r + 1 != p ) plan.push_back( { v.id, p, 0 } );
plan.push_back( { u.id, v.id, 1 } );
}
inline void solve( LL& ans ) { // strange...
if ( seg[0].empty() || seg[1].empty() ) return ;
Atom u( seg[0].top() ), v( seg[1].top() );
if ( u.r < v.l ) seg[0].pop(), solve( ans );
else if ( v.r < u.l ) seg[1].pop(), solve( ans );
else {
seg[0].pop(), seg[1].pop();
int il = imax( u.l, v.l ), ir = imin( u.r, v.r );
ans -= ir - il + 1 << 1;
if ( u.r <= v.r ) {
if ( u.r < v.r ) seg[1].push( { u.r + 1, v.r, v.id } );
solve( ans ), action( u, v, u.r + 1 );
} else {
if ( v.r < u.r ) seg[0].push( { v.r + 1, u.r, u.id } );
action( u, v, v.r + 1 ), solve( ans );
}
}
}
int main() {
freopen( "subway.in", "r", stdin );
freopen( "subway.out", "w", stdout );
for ( int T = rint(); T--; ) {
n = rint(), m = rint();
for ( ; !seg[0].empty(); seg[0].pop() );
for ( ; !seg[1].empty(); seg[1].pop() );
plan.clear();
LL ans = 0;
rep ( i, 1, n ) {
fin[i] = s[i] = rint(), e[i] = rint();
if ( s[i] < e[i] ) {
ans += e[i] - s[i], seg[0].push( { s[i], e[i] - 1, i } );
} else {
ans += s[i] - e[i], seg[1].push( { e[i], s[i] - 1, i } );
}
}
solve( ans );
wint( ans ), putchar( ' ' );
for ( const Atom& a: plan ) if ( !a.id ) fin[a.l] = a.r;
rep ( i, 1, n ) if ( fin[i] != e[i] ) {
plan.push_back( { i, e[i], 0 } );
}
assert( plan.size() <= 4e5 );
wint( plan.size() ), putchar( '\n' );
for ( const Atom& a: plan ) {
wint( a.id ), putchar( ' ' );
wint( a.l ), putchar( ' ' );
wint( a.r ), putchar( '\n' );
}
}
return 0;
}
\(\mathcal{Details}\)
挺逗的,考場上想到了逆序操作卻忽略了一部分操作需要正序,“對稱”的坑點只注意到一個……補題發現把迴圈換成程式碼裡的遞迴再調一調語句順序就過了。長記性吶。