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 都是合法匹配,则 ABASB 也都为合法匹配。
规则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
如果你想留言,可以在微信里面关注公众号进行留言。

关注公众号,接收最新消息

tiankonguse +
穿越