CSP-S 2021 编程算法比赛
作者:
| 更新日期:对偶图上最短路后转环形DP
本文首发于公众号:天空的代码世界,微信号:tiankonguse
零、背景
最近我计划研究 CSP-J 与 CSP-S 的比赛题目,之前已经完成了 7 场比赛的题解,今天将分享 2021 年 CSP-S 第二轮编程算法比赛的详细题解。
A: 二分+线段树
B: 语法解析+动态规划
C: 贪心搜索
D: 对偶图+最短路+环形DP
代码地址: https://github.com/tiankonguse/leetcode-solutions/tree/master/other/CSP-S/
比赛题目分类与题解 |
---|
CSP-J 2024 题解 A:扑克牌 入门 B: 地图探险 普及− C: 小木棍 普及/提高− D: 接龙 提高+/省选− |
CSP-S 2024 题解 A:决斗 普及− B: 超速检测 普及+/提高 C: 染色 提高+/省选− D: 擂台游戏 NOI/NOI+/CTSC |
CSP-J 2023 题解 A:小苹果 普及− B: 公路 普及− C: 一元二次方程 普及/提高− D: 旅游巴士 普及+/提高 |
CSP-S 2023 题解 A:密码锁 普及− B: 消消乐 提高+/省选− C: 结构体 提高+/省选− D: 种树 提高+/省选− |
CSP-J 2022 题解 A:乘方 入门 B: 解密 普及− C: 逻辑表达式 普及+/提高 D: 上升点列 普及/提高− |
CSP-S 2022 题解 A:假期计划 提高+/省选− B: 策略游戏 普及+/提高 C: 星战 省选/NOI− D: 数据传输 省选/NOI− |
CSP-J 2021 题解 A:分糖果 普及− B: 插入排序 普及/提高− C: 网络连接 普及/提高− D: 小熊的果篮 普及+/提高 |
CSP-S 2021 题解 A:廊桥分配 普及+/提高 B: 括号序列 提高+/省选− C: 回文 普及/提高− D: 交通规划 省选/NOI− |
一、廊桥分配
题意:一个机场有 n 个廊桥,需要分给国际机场航班和国内机场航班。
国际航班只能进入国际机场的廊桥,如果满了,就需要停到远机位。
国内航班只能进入国内机场的廊桥,如果停满了,一样也只能停到远机位。
每个航班都是优先停到廊桥,其次是远机位。
问如何分配,才能使得停留在廊桥的飞机尽量多。
思路:二分+线段树
如果可以预处理出所有数量各自廊桥的飞机数量,枚举所有分配方案,求和,取最大值即可。
假设廊桥是无限的,但是有编号,会优先选择最小编号的廊桥,看下各个航班会怎么匹配廊桥吧。
题意要求先到先得,所以先对航班到达时间 t 排序。
t0 时刻到达一个飞机后,我们需要找到所有空廊桥的最小编号。
空廊桥,意味着廊桥里面的飞机的离开时间小于 t0,即找到所有离开时间小于 t0 且编号最小的廊桥。
对于这个求小于一个值且最小编号的问题,可以使用二分线段树来做。
二分求线段树的前缀的最小值,直到找到第一个不满足的边界,则下一个就是满足的。
sort(flights.begin(), flights.end());
segTree.Init(nm);
segTree.Build();
for (auto [a, b] : flights) {
int l = 1, r = nm + 1;
while (l < r) {
int mid = (l + r) >> 1;
if (segTree.QueryMin(1, mid) <= a) {
r = mid;
} else {
l = mid + 1;
}
}
nums[l]++; // [a,b] 可以使用第 l 个廊桥
ll oldVal = segTree.QueryMin(l, l);
segTree.Update(l, b - oldVal);
}
每个航班最终选择哪个编号储存在 nums 中。
如果分配了 n 个廊桥,答案就是前 n 个 nums 的和,故预处理求前缀和。
for (int i = 1; i <= n; i++) {
nums[i] += nums[i - 1]; // 前 i 个廊桥,最多可以容纳的飞机数量
}
最后枚举国际廊桥和国内廊桥的个数,求最大值。
int ans = 0;
for (int i = 0; i <= n; i++) {
ans = max(ans, nums1[i] + nums2[n - i]);
}
printf("%d\n", ans);
二、括号序列
题意:给一个星号与括号匹配的规则,部分位置使用问号代替,问有多少种匹配方式。
规则1:()
和 (S)
为合法匹配,其中 S
为至少1和不超过 k 个 *
。
规则2:如果 A 与 B 都是合法匹配,则 AB
和 ASB
也都为合法匹配。
规则3:如果 A 是合法匹配,则 (A)
、(AS)
、(SA)
也都为合法匹配。
思路:语法解析动态规划
观察三个规则,可以确定一个结论:所有合法匹配,最左边肯定是左括号,最右边肯定是右括号。
这个结论是语法解析的基础,可以用来快速剪枝。
另外,这里的关键是对规则2进行划分,避免重复计算。
可以建最后 状态1的状态转移方程解释。
状态1定义:Dfs(l,r)
区间 [l,r]
内的合法匹配数量。
状态2定义:Dfs3(l,r)
命中规则3或规则1时,区间 [l,r]
内的合法匹配数量。
状态3定义:DfsLeft(l,r)
命中 SA
时,区间 [l,r]
内的合法匹配数量。
状态4定义:DfsRight(l,r)
命中 AS
时,区间 [l,r]
内的合法匹配数量。
状态3的转移方程:枚举星号的个数,至少1个,至多k个,然后递归求解。
ll DfsLeft(const int l, const int r) {
if (l > r) return 0;
ll& ret = dpLeft[l][r];
if (ret != -1) return ret;
if (r - l + 1 < 3) return ret = 0;
ret = 0; // 至少1个,至多 k 个
for (int i = 1; IsStart(l + i - 1) && i <= k && l + i <= r; i++) {
ret = (ret + Dfs(l + i, r)) % mod;
}
return ret;
}
状态4的转移方程与装填3类似。
ll DfsRight(const int l, const int r) {
if (l > r) return 0;
ll& ret = dpRight[l][r];
if (ret != -1) return ret;
if (r - l + 1 < 3) return ret = 0;
ret = 0; // 至少1个,至多 k 个
for (int i = 1; IsStart(r - i + 1) && i <= k && l <= r - i; i++) {
ret = (ret + Dfs(l, r - i)) % mod;
}
return ret;
}
状态2的转移方程:根据题意分三种情况
ll DfsBracket(int l, int r) {
if (l > r) return 0;
ll& ret = dpBracket[l][r];
if (ret != -1) return ret;
if (l == r) return ret = 0;
if (!MatchLeftBracket(l)) return ret = 0;
if (!MatchRightBracket(r)) return ret = 0;
ret = 0;
// case: ()
if (l + 1 == r) return ret = 1;
// case: (*)
if (IsAllStar(l + 1, r - 1) && r - l - 1 <= k) {
ret = (ret + 1) % mod;
}
// case: (A)
ret = (ret + Dfs(l + 1, r - 1)) % mod;
// case: (*A)
ret = (ret + DfsLeft(l + 1, r - 1)) % mod;
// case: (A*)
ret = (ret + DfsRight(l + 1, r - 1)) % mod;
return ret;
}
状态1用于处理多个状态的拼接情况。
第一种是 ABSD
,可以把 BSD
当做一个合法匹配,按 AB
规则来处理,即只需要枚举找到第一个 A
即可。
第二种是 ASCD
,可以把CD
当做一个整体,按照 ASB
规则来处理。
ll Dfs(const int l, const int r) { // [l,r]
if (l > r) return 0;
// 出口
ll& ret = dp[l][r];
if (ret != -1) return ret;
if (l == r) return ret = 0;
if (!MatchLeftBracket(l)) return ret = 0;
if (!MatchRightBracket(r)) return ret = 0;
if (l + 1 == r) return ret = 1;
ret = 0;
// case: (...)
ret = (ret + DfsBracket(l, r)) % mod;
// case: AB
// bad case: ABCD, 只需要枚举到 A,故 A 必须是左右括号
for (int i = l + 1; i + 1 < r; i++) {
ret = (ret + DfsBracket(l, i) * Dfs(i + 1, r) % mod) % mod;
}
// case: A*B
// bad case: AB*C, A*BC, AB*CD
for (int i = l + 1; i + 1 <= r; i++) {
ret = (ret + DfsBracket(l, i) * DfsLeft(i + 1, r) % mod) % mod;
}
return ret;
}
三、回文
四、交通规划
五、最后
《完》
-EOF-
本文公众号:天空的代码世界
个人微信号:tiankonguse
公众号 ID:tiankonguse-code
本文首发于公众号:天空的代码世界,微信号:tiankonguse
如果你想留言,可以在微信里面关注公众号进行留言。