很久没有写过博客了,这次 \(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\) 给我的教训很大,但是难过后悔之后也挺庆幸被其点醒,还有一个月,冲吧。