很久沒有寫過博客了,這次 \(CSP\) 一塌糊塗,寫寫一些東西小結一下。
廊橋分配
\(T1\) 看完題之后就會有一個自然而然的想法,枚舉兩邊分多少個,然后算出兩邊的貢獻,每次取 \(max\)。
但是如果每次暴力求兩邊的答案的話,時間復雜度是 \(n ^ 2\) 的。
考慮優化,不難發現如果我們從小到大枚舉國內場的廊橋數的話,國內場的貢獻是單調不降,國際場的貢獻是單調不升的。
可能這里會想到三分,但是由於總貢獻是其之和,顯然是多峰的,惡心的是,它提供的大樣例三分可以過。
繼續剛才的思路。
這個時候你會發現,每次枚舉數變化一時,國內場增加的飛機, 國際場減少的飛機均滿足后一個抵達的飛機的抵達時間大於前一個抵達飛機的離開時間,而這個的原因自然很簡單,因為只有這樣它們才能被裝進同一個廊橋。
其實到這里已經有一點轉換題意了。
我們再轉換一下。
先把飛機按抵達時間從小到大排序。
我們按廊橋的編號從小到大枚舉,首先把剩余還未被裝進廊橋的飛機中編號抵達時間最早的裝進當前的廊橋,再把滿足先前那個條件的飛機們盡量裝進當前這個廊橋。
不難發現每次的能裝進這個廊橋的飛機數量對應當前變化的貢獻。
這是因為當我們增加一個廊橋之后,按照先來后到的原則,剩余最早到的飛機肯定要停到這個廊橋,而在它后面到達的飛機只有滿足先前那個條件,才會停在廊橋,因為能停必停,所以我們廊橋的編號要從小到大枚舉,才能滿足這個條件。
減少也是同樣的道理。
模擬一下應該就很好理解了。
這個過程我們可以用 \(set\) 優化到 \(nlogn\)。
通過這樣預處理,我們每次計算貢獻時是 \(O(1)\) 的。
總時間復雜度 \(O(n + nlogn)\)。
考場犯的錯誤
-
最開始沒有想清楚,枚舉飛機倒着裝進廊橋,但是這樣不能再時限內保證能停必停的條件,耽擱了我很多時間。
-
\(set\) 的 \(lower\)\(bound\) 沒有用其成員函數,用的是算法庫里的 \(lower\)\(bound\),因為后者實現過程以及 \(set\) 的非隨機訪問迭代器的問題,導致考場代碼比暴力還慢。
it = lower_bound(s.begin(), s.end(), x)// Wrong
it = s.lower_bound(x); // Right
#include<cstdio>
#include<cctype>
#include<set>
#include<algorithm>
using namespace std;
const int N = 1e5 + 5;
int n, m1, m2, ans, res1, res2, c1[N], c2[N], cnt1[N], cnt2[N];
struct data {
int x, y;
inline bool operator < (const data &a) {
return x < a.x;
}
} a[N], b[N];
set < pair < int, int > > s;
set < pair < int, int > > :: iterator it;
inline void read(int &x) {
x = 0; int c = getchar(), f = 1;
for(; !isdigit(c); c = getchar())
if(c == '-') f = -1;
for(; isdigit(c); c = getchar())
x = x * 10 + c - 48;
x *= f;
}
int main() {
read(n), read(m1), read(m2);
for(int i = 1; i <= m1; i++) read(a[i].x), read(a[i].y);
for(int i = 1; i <= m2; i++) read(b[i].x), read(b[i].y);
int tag = 0;
sort(a + 1, a + 1 + m1);
sort(b + 1, b + 1 + m2);
for(int i = 1; i <= m1; i++) s.insert(make_pair(a[i].x, i));
for(int i = 1; i <= m1; i++) {
if(c1[i]) continue ;
c1[i] = ++ tag; ++ cnt1[tag];
it = s.find(make_pair(a[i].x, i));
if(it != s.end()) s.erase(it);
int kym = i;
while(1) {
it = s.lower_bound(make_pair(a[kym].y, kym));
if(it == s.end()) break ;
kym = (*it).second, c1[kym] = tag, ++ cnt1[tag];
s.erase(it);
}
}
s.clear(), tag = 0;
for(int i = 1; i <= m2; i++) s.insert(make_pair(b[i].x, i));
for(int i = 1; i <= m2; i++) {
if(c2[i]) continue ;
c2[i] = ++ tag; ++ cnt2[tag];
it = s.find(make_pair(b[i].x, i));
if(it != s.end()) s.erase(it);
int kym = i;
while(1) {
it = s.lower_bound(make_pair(b[kym].y, kym));
if(it == s.end()) break ;
kym = (*it).second, c2[kym] = tag, ++ cnt2[tag];
s.erase(it);
}
}
for(int i = 1; i <= m2; i++) if(c2[i] && c2[i] <= n) ++ res2;
ans = res2;
for(int i = 1, p1 = 1, p2 = n; i <= n; i++) {
res1 += cnt1[p1], res2 -= cnt2[p2];
ans = max(ans, res1 + res2);
++ p1, -- p2;
}
printf("%d\n", ans);
return 0;
}
括號序列
模擬賽做過類似的題,但是因為在 \(t1\) 花費了比較多的時間,推了一下柿子,因為分類討論的情況比較多,沒有去開。
回文
受移球游戲的影響,以為也是一道陰間構造題,打了一個 \(dfs\) 就爬了,現在去做卻發現認真模擬一下樣例即可發現規律。
其規律就是每次選了之后標記這個數另一個出現位置,下次選的看是否選某個數就看它是否和被標記的的數的兩邊的數相等。
分兩次枚舉第一次選左邊還是右邊,然后每次判斷時先判左邊,保證字典序最小,因為是看是否和被標記的數的兩邊的數相等,所以被標記的數會連成一個區間,我們用四個指針模擬左右兩邊被選到哪里,以及被標記的區間的左右兩邊在哪里即可。
如果某一次選取,選左邊右邊的數都不行,那么這次首開的左邊或者右邊就無解,如果兩次都無解,那么就真的是無解。
至於正確性,可以感性理解一下。
首先它保證了字典序最小,其次如果不在被標記的區間兩邊選數的話,那么就會出現沒有被選的數夾在了被標記的數中間,那么到時候取回文數后一半的時候就會出現它被卡在中間取不出來的情況。
碼大力分類討論比較丑。
#include<cstdio>
#include<cctype>
#include<cstring>
using namespace std;
const int N = 5e5 + 5;
int t, n, a[N << 1], ans[N << 1], sta[N], top, p1, p2, p3, p4;
inline void read(int &x) {
x = 0; int c = getchar(), f = 1;
for(; !isdigit(c); c = getchar())
if(c == '-') f = -1;
for(; isdigit(c); c = getchar())
x = x * 10 + c - 48;
x *= f;
}
inline void solve() {
read(n);
for(int i = 1; i <= (n << 1); i++) read(a[i]);
p1 = 1, p4 = (n << 1) + 1;
for(int i = 2; i <= (n << 1); i++)
if(a[i] == a[1]) {
p2 = p3 = i; break ;
}
int now = 1, f = 1;
memset(ans, -1, sizeof ans);
ans[1] = 0, top = 0; sta[++ top] = a[1];
while(p4 - p1 - 1 > n) {
++ now;
int x = a[p2 - 1], y = a[p3 + 1];
if(p1 + 1 < p2 - 1 && a[p1 + 1] == x)
ans[now] = 0, ++ p1, -- p2, sta[++ top] = x;
if(a[p1 + 1] == y && ans[now] == -1)
ans[now] = 0, ++ p1, ++ p3, sta[++ top] = y;
if(ans[now] != -1) continue ;
if(p4 - 1 > p3 + 1 && a[p4 - 1] == y)
ans[now] = 1, -- p4, ++ p3, sta[++ top] = y;
if(a[p4 - 1] == x && ans[now] == -1)
ans[now] = 1, -- p4, -- p2, sta[++ top] = x;
if(ans[now] == -1) {
f = 0; break ;
}
}
if(f) {
for(int i = n + 1; i <= (n << 1); i++) {
if(a[p2] == sta[top]) ans[i] = 0, ++ p2;
else if(a[p3] == sta[top]) ans[i] = 1, -- p3;
else {
f = 0; break ;
} top --;
}
if(f) {
for(int i = 1; i <= n; i++) putchar(ans[i] == 0 ? 'L' : 'R');
for(int i = n + 1; i < (n << 1); i++) putchar(ans[i] == 0 ? 'L' : 'R');
putchar('L'), putchar('\n');
return ;
}
}
p1 = 0, p4 = (n << 1);
for(int i = 1; i < (n << 1); i++)
if(a[i] == a[n << 1]) {
p2 = p3 = i; break ;
}
now = 1, f = 1;
memset(ans, -1, sizeof ans);
ans[1] = 1, top = 0; sta[++ top] = a[n << 1];
while(p4 - p1 - 1 > n) {
++ now;
int x = a[p2 - 1], y = a[p3 + 1];
if(p1 + 1 < p2 - 1 && a[p1 + 1] == x)
ans[now] = 0, ++ p1, -- p2, sta[++ top] = x;
if(a[p1 + 1] == y && ans[now] == -1)
ans[now] = 0, ++ p1, ++ p3, sta[++ top] = y;
if(ans[now] != -1) continue ;
if(p4 - 1 > p3 + 1 && a[p4 - 1] == y)
ans[now] = 1, -- p4, ++ p3, sta[++ top] = y;
if(a[p4 - 1] == x && ans[now] == -1)
ans[now] = 1, -- p4, -- p2, sta[++ top] = x;
if(ans[now] == -1) {
f = 0; break ;
}
}
if(f) {
for(int i = n + 1; i <= (n << 1); i++) {
if(a[p2] == sta[top]) ans[i] = 0, ++ p2;
else if(a[p3] == sta[top]) ans[i] = 1, -- p3;
else {
f = 0; break ;
} top --;
}
if(f) {
for(int i = 1; i <= n; i++) putchar(ans[i] == 0 ? 'L' : 'R');
for(int i = n + 1; i < (n << 1); i++) putchar(ans[i] == 0 ? 'L' : 'R');
putchar('L'), putchar('\n');
return ;
}
}
puts("-1");
}
int main() {
read(t);
while(t--) solve();
return 0;
}
交通規划
考場上想出了 \(k = 2\) 的拓撲排序做法,但是因為此時前面耗費了太多時間,加上鍵盤和網格圖建邊太陰間,沒有打完。
總結
這次心態和策略徹徹底底的輸了,4點鍾的時候就一直在想人均兩百加,而自己 \(t1\) 還沒調出來,手都在抖。
而平時模擬賽的難題沒有去鑽,對自己的要求低了,所以很多時候到了關鍵時刻發揮不出全部的實力。
這次 \(CSP\) 給我的教訓很大,但是難過后悔之后也挺慶幸被其點醒,還有一個月,沖吧。
