把每对友好城市看成点 (南岸, 北岸),两条航道相交等价于坐标逆序;按南岸排序后求北岸序列的最长不下降子序列,用 tails 数组加二分做到 O(n log n)。
OJ: roj
题目 ID: 1263
难度:普及+/提高-
标签:排序二分动态规划最长不下降子序列
创建: 2026-09-30 02:25
更新: 2026-10-07 11:01
形式化题目
河的两岸各有若干城市,给出
要求从这
把每对看成平面上的点
即一个点的横坐标更小而纵坐标更大(或反之)。坐标相等时两条航道共端点或平行,不算交叉。
样例中共有 7 对申请。按南岸坐标排好序后,把两岸的坐标各画成一排,一对城市之间连一条线:
南岸 x: 2 4 9 10 15 17 22
│ │ │ │ │ │ │
北岸 y: 6 2 8 3 12 17 4
选中 : ✓ ✓ ✓ ✓观察重点是:排序后
正解
思路
第一步:把几何条件翻译成一维顺序。 两条航道是否相交,只取决于两个端点的相对顺序。把所有申请按南岸坐标
- 左边那条的
更小:两条线是"左下 → 右上"的平行走向,不相交; - 左边那条的
更大:两条线一条向右上、一条向右下,必然相交。
于是"航道两两不交叉"就等价于:按南岸排序后,被选中的点其北岸坐标单调不下降。
排序时还要处理南岸坐标相同的情况:两对城市的南岸是同一个点,两条航道只在这个岸上端点处相接,不算交叉,所以它们可以同时被选中。把排序键写成整个元组 (x, y)(即
第二步:识别模型。 排序只是预处理,真正的任务是:从北岸坐标序列
第三步:为什么是"不下降"而不是"严格上升"。 题面声称两岸坐标互不相同,但真实数据中确实出现了重复坐标;而
第四步:优化 DP。 最朴素的 LNDS 是
瓶颈在"前面所有满足 tails,规定
tails[k]= 目前所有长度为的不下降子序列中,最小的结尾值。
这个数组关于下标
pos = bisect_right(tails, y_i)找到第一个大于的位置; - 若
pos == len(tails),说明能接在当前最长链后面,链长加一; - 否则把
tails[pos]覆盖成:同样能得到长度 pos+1的链,但结尾更小,只会给后面留出更多空间,不会丢掉任何可能。
用样例走一遍,排序后的北岸序列是
| 新值 |
tails 变化 |
len(tails) |
|---|---|---|
| 6 | 1 | |
| 2 | 1 | |
| 8 | 2 | |
| 3 | 2 | |
| 12 | 3 | |
| 17 | 4 | |
| 4 | 4 |
最后一行的 tails 是 tails 只是"每个长度对应的最小结尾"这张查询表,长度才是答案。最终长度为 4,与样例一致。
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 02:26
# update_at: 2026-09-30 02:26
import sys
from bisect import bisect_right
from collections.abc import Iterator
from itertools import islice
def read_links(data: Iterator[int], n: int) -> list[tuple[int, int]]:
"""读出 n 对坐标 (南岸, 北岸),并按南岸坐标升序排列。
zip(data, data) 复用同一个迭代器,天然把相邻的两个数配成一对。
"""
return sorted(islice(zip(data, data), n))
def longest_chain(north: list[int]) -> int:
"""求 north 的最长不下降子序列长度。
tails[k] 记录“长度为 k+1 的不下降子序列”中最小的结尾值,它关于 k 单调不减,
所以新值 value 接在哪个长度后面可以直接二分。相等也能接(两条航道只是平行不交叉),
因此插入点取右侧 bisect_right。
"""
tails: list[int] = []
for value in north:
pos = bisect_right(tails, value)
if pos == len(tails):
tails.append(value)
else:
tails[pos] = value # 用更小的结尾值换掉旧的,给后面的坐标留出更多空间
return len(tails)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
links = read_links(data, n) # 按南岸排序后,问题变成只看北岸的最长不下降子序列
print(longest_chain([north for _south, north in links]))
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-04 23:37
* update_at: 2026-10-04 23:37
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 5005;
struct Link {
ll south; // 南岸坐标
ll north; // 北岸坐标
};
ll n;
Link link[MAXN]; // 全部友好城市对
ll tails[MAXN]; // tails[k] 表示所有长度为 k+1 的不下降子序列中最小的结尾值
ll tails_len;
// 先按南岸坐标升序,南岸相同时再按北岸坐标升序。
// 这样南岸相同的两条航道(共端点,不算交叉)会自然按北岸递增排列。
bool cmp_link(Link a, Link b) {
if (a.south != b.south) return a.south < b.south;
return a.north < b.north;
}
void read_input() {
cin >> n;
for (ll i = 1; i <= n; i++) {
cin >> link[i].south >> link[i].north;
}
}
// 按南岸排好序后,“航道两两不交叉”等价于北岸坐标单调不下降,
// 于是问题变成在 north 序列上求最长不下降子序列。
// 用 upper_bound 找第一个大于当前值的位置:取右侧插入点,允许相等的值接在后面。
void solve() {
sort(link + 1, link + n + 1, cmp_link);
tails_len = 0;
for (ll i = 1; i <= n; i++) {
ll value = link[i].north;
ll pos = upper_bound(tails, tails + tails_len, value) - tails;
tails[pos] = value;
if (pos == tails_len) {
tails_len++;
}
}
cout << tails_len << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
——排序 ,之后每个元素一次二分 ;优于标准 的 DP, 时毫无压力。 - 空间复杂度:
——存排好序的点对与 tails数组。
总结
这道题的难点不在算法,而在建模:把"航道不交叉"这个几何条件,翻译成"按南岸排序后北岸坐标单调不下降"这一条一维条件,题目就退化为经典的最长不下降子序列。具体到一个细节:坐标允许相等,两条共端点或平行的航道不算交叉,因此是不下降(bisect_right)而不是严格上升(bisect_left)——这一点在真实数据里会直接改变答案。最后用 tails 数组加二分把