# 枚举切点思想
枚举切点思想
枚举切点思想是一个非常常用的思想方法,大致就是说有两个部分,将一些东西枚举划到一个部分,其余划到另一个部分的最优解
例题1.CSP-2021廊桥分配
题目描述
当一架飞机抵达机场时,可以停靠在航站楼旁的廊桥,也可以停靠在位于机场边缘的远机位。乘客一般更期待停靠在廊桥,因为这样省去了坐摆渡车前往航站楼的周折。然而,因为廊桥的数量有限,所以这样的愿望不总是能实现。
机场分为国内区和国际区,国内航班飞机只能停靠在国内区,国际航班飞机只能停靠在国际区。一部分廊桥属于国内区,其余的廊桥属于国际区。
L 市新建了一座机场,一共有 \(n\) 个廊桥。该机场决定,廊桥的使用遵循“先到先得”的原则,即每架飞机抵达后,如果相应的区(国内/国际)还有空闲的廊桥,就停靠在廊桥,否则停靠在远机位(假设远机位的数量充足)。该机场只有一条跑道,因此不存在两架飞机同时抵达的情况。
现给定未来一段时间飞机的抵达、离开时刻,请你负责将 \(n\) 个廊桥分配给国内区和国际区,使停靠廊桥的飞机数量最多。
输入格式
输入的第一行,包含三个正整数 \(n, m_1, m_2\),分别表示廊桥的个数、国内航班飞机的数量、国际航班飞机的数量。
接下来 \(m_1\) 行,是国内航班的信息,第 \(i\) 行包含两个正整数 \(a_{1, i}, b_{1, i}\),分别表示一架国内航班飞机的抵达、离开时刻。
接下来 \(m_2\) 行,是国际航班的信息,第 \(i\) 行包含两个正整数 \(a_{2, i}, b_{2, i}\),分别表示一架国际航班飞机的抵达、离开时刻。
每行的多个整数由空格分隔。
输出格式
输出一个正整数,表示能够停靠廊桥的飞机数量的最大值。
样例 #1
样例输入 #1
3 5 4
1 5
3 8
6 10
9 14
13 18
2 11
4 15
7 17
12 16
7
2 4 6
20 30
40 50
21 22
41 42
1 19
2 18
3 4
5 6
7 8
9 10
4
提示
【样例解释 #1】
在图中,我们用抵达、离开时刻的数对来代表一架飞机,如 \((1, 5)\) 表示时刻 \(1\) 抵达、时刻 \(5\) 离开的飞机;用 \(\surd\) 表示该飞机停靠在廊桥,用 \(\times\) 表示该飞机停靠在远机位。
我们以表格中阴影部分的计算方式为例,说明该表的含义。在这一部分中,国际区有 \(2\) 个廊桥,\(4\) 架国际航班飞机依如下次序抵达:
- 首先 \((2, 11)\) 在时刻 \(2\) 抵达,停靠在廊桥。
- 然后 \((4, 15)\) 在时刻 \(4\) 抵达,停靠在另一个廊桥。
- 接着 \((7, 17)\) 在时刻 \(7\) 抵达,这时前 \(2\) 架飞机都还没离开、都还占用着廊桥,而国际区只有 \(2\) 个廊桥,所以只能停靠远机位。
- 最后 \((12, 16)\) 在时刻 \(12\) 抵达,这时 \((2, 11)\) 这架飞机已经离开,所以有 \(1\) 个空闲的廊桥,该飞机可以停靠在廊桥。
根据表格中的计算结果,当国内区分配 \(2\) 个廊桥、国际区分配 \(1\) 个廊桥时,停靠廊桥的飞机数量最多,一共 \(7\) 架。
【样例解释 #2】
当国内区分配 \(2\) 个廊桥、国际区分配 \(0\) 个廊桥时,停靠廊桥的飞机数量最多,一共 \(4\) 架,即所有的国内航班飞机都能停靠在廊桥。
需要注意的是,本题中廊桥的使用遵循“先到先得”的原则,如果国际区只有 \(1\) 个廊桥,那么将被飞机 \((1, 19)\) 占用,而不会被 \((3, 4)\)、\((5, 6)\)、\((7, 8)\)、\((9, 10)\) 这 \(4\) 架飞机先后使用。
【数据范围】
对于 \(20 \%\) 的数据,\(n \le 100\),\(m_1 + m_2 \le 100\)。
对于 \(40 \%\) 的数据,\(n \le 5000\),\(m_1 + m_2 \le 5000\)。
对于 \(100 \%\) 的数据,\(1 \le n \le {10}^5\),\(m_1, m_2 \ge 1\),\(m_1 + m_2 \le {10}^5\),所有 \(a_{1, i}, b_{1, i}, a_{2, i}, b_{2, i}\) 为数值不超过 \({10}^8\) 的互不相同的正整数,且保证对于每个 \(i \in [1, m_1]\),都有 \(a_{1, i} < b_{1, i}\),以及对于每个 \(i \in [1, m_2]\),都有 \(a_{2, i} < b_{2, i}\)。
先来考虑简化版问题:如果只有一类飞机抵达,那么如何求出前\(n\)个飞机停靠的廊桥的最小值
很简单,用堆维护各个廊桥的结束使用时间,当每一个新飞机来的时候,就判断一下这个飞机是否可以用已经空出的机位,不行就新开,在每个飞机来的时候将已经空出的机位弹出堆顶,这样我们就可以得到一个\(t\),为最大使用的廊桥数量。那么进一步思考,我们能否在一个足够优秀的时间复杂度内求出\(i\in[1,n]\)个廊桥最最多容纳的飞机数量
这个比较好解决,试问我们如果知道每一个飞机都停在哪一个廊桥(分配廊桥的时候以编号最小的空闲廊桥为准,正确性显然),那么是不是就可以轻易的前缀和求出答案了?
那么我们对国内国外两类都进行这样的操作,记录答案为\(S1,S2\)总复杂度仍然是\(O(m\log n)\)的,进一步的,我们就可以枚举断点了,对于每一个\(x\in[0,n]\),令\(ans=max(ans,S1_x+S2_{n-x})\),最终便可以求出解
#include<bits/stdc++.h>
using namespace std;
#define mkp make_pair
#define int long long
#define in read()
inline int read(){
int p=0,f=1;
char c=getchar();
while(!isdigit(c)){if(c=='-')f=-1;c=getchar();}
while(isdigit(c)){p=p*10+c-'0';c=getchar();}
return p*f;
}
const int N=1e5+5;
int n,m1,m2,ans1[N],ans2[N],ans;
struct plane{int x,y;}a[N],b[N];
bool cmp(plane x,plane y){return x.x<y.x;}
priority_queue<pair<int,int> >q;
priority_queue<int>p;
signed main(){
n=in,m1=in,m2=in;
for(int i=1;i<=m1;i++)
a[i].x=in,a[i].y=in;
for(int i=1;i<=m2;i++)
b[i].x=in,b[i].y=in;
sort(a+1,a+1+m1,cmp);
sort(b+1,b+1+m2,cmp);
for(int i=1;i<=n;i++)p.push(-i);
for(int i=1;i<=m1;i++){
int s=a[i].x,t=a[i].y;
while(!q.empty()&&-q.top().first<s){p.push(-q.top().second);q.pop();}
if(!p.empty()){int tp=-p.top();q.push(mkp(-t,tp));ans1[tp]++;p.pop();}
}
while(!q.empty())q.pop();
while(!p.empty())p.pop();
for(int i=1;i<=n;i++)p.push(-i);
for(int i=1;i<=m2;i++){
int s=b[i].x,t=b[i].y;
while(!q.empty()&&-q.top().first<s){p.push(-q.top().second);q.pop();}
if(!p.empty()){int tp=-p.top();q.push(mkp(-t,tp));ans2[tp]++;p.pop();}
}
for(int i=1;i<=n;i++)
ans1[i]+=ans1[i-1],
ans2[i]+=ans2[i-1];
for(int i=0;i<=n;i++)
ans=max(ans,ans1[i]+ans2[n-i]);
cout<<ans;
return 0;
}
例题2:CSP-S2022假期计划
题目描述
小熊的地图上有 \(n\) 个点,其中编号为 \(1\) 的是它的家、编号为 \(2, 3, \ldots, n\) 的都是景点。部分点对之间有双向直达的公交线路。如果点 \(x\) 与 \(z_1\)、\(z_1\) 与 \(z_2\)、……、\(z_{k - 1}\) 与 \(z_k\)、\(z_k\) 与 \(y\) 之间均有直达的线路,那么我们称 \(x\) 与 \(y\) 之间的行程可转车 \(k\) 次通达;特别地,如果点 \(x\) 与 \(y\) 之间有直达的线路,则称可转车 \(0\) 次通达。
很快就要放假了,小熊计划从家出发去 \(4\) 个不同的景点游玩,完成 \(5\) 段行程后回家:家 \(\to\) 景点 A \(\to\) 景点 B \(\to\) 景点 C \(\to\) 景点 D \(\to\) 家且每段行程最多转车 \(k\) 次。转车时经过的点没有任何限制,既可以是家、也可以是景点,还可以重复经过相同的点。例如,在景点 A \(\to\) 景点 B 的这段行程中,转车时经过的点可以是家、也可以是景点 C,还可以是景点 D \(\to\) 家这段行程转车时经过的点。
假设每个景点都有一个分数,请帮小熊规划一个行程,使得小熊访问的四个不同景点的分数之和最大。
输入格式
第一行包含三个正整数 \(n, m, k\),分别表示地图上点的个数、双向直达的点对数量、每段行程最多的转车次数。
第二行包含 \(n - 1\) 个正整数,分别表示编号为 \(2, 3, \ldots, n\) 的景点的分数。
接下来 \(m\) 行,每行包含两个正整数 \(x, y\),表示点 \(x\) 和 \(y\) 之间有道路直接相连,保证 \(1 \le x, y \le n\),且没有重边,自环。
输出格式
输出一个正整数,表示小熊经过的 \(4\) 个不同景点的分数之和的最大值。
样例 #1
样例输入 #1
8 8 1
9 7 1 8 2 3 6
1 2
2 3
3 4
4 5
5 6
6 7
7 8
8 1
样例输出 #1
27
样例 #2
样例输入 #2
7 9 0
1 1 1 2 3 4
1 2
2 3
3 4
1 5
1 6
1 7
5 4
6 4
7 4
样例输出 #2
7
提示
【样例解释 #1】
当计划的行程为 \(1 \to 2 \to 3 \to 5 \to 7 \to 1\) 时,\(4\) 个景点的分数之和为 \(9 + 7 + 8 + 3 = 27\),可以证明其为最大值。
行程 \(1 \to 3 \to 5 \to 7 \to 8 \to 1\) 的景点分数之和为 \(24\)、行程 \(1 \to 3 \to 2 \to 8 \to 7 \to 1\) 的景点分数之和为 \(25\)。它们都符合要求,但分数之和不是最大的。
行程 \(1 \to 2 \to 3 \to 5 \to 8 \to 1\) 的景点分数之和为 \(30\),但其中 \(5 \to 8\) 至少需要转车 \(2\) 次,因此不符合最多转车 \(k = 1\) 次的要求。
行程 \(1 \to 2 \to 3 \to 2 \to 3 \to 1\) 的景点分数之和为 \(32\),但游玩的并非 \(4\) 个不同的景点,因此也不符合要求。
【数据范围】
对于所有数据,保证 \(5 \le n \le 2500\),\(1 \le m \le 10000\),\(0 \le k \le 100\),所有景点的分数 \(1 \le s_i \le {10}^{18}\)。保证至少存在一组符合要求的行程。
测试点编号 | \(n \le\) | \(m \le\) | \(k \le\) |
---|---|---|---|
\(1 \sim 3\) | \(10\) | \(20\) | \(0\) |
\(4 \sim 5\) | \(10\) | \(20\) | \(5\) |
\(6 \sim 8\) | \(20\) | \(50\) | \(100\) |
\(9 \sim 11\) | \(300\) | \(1000\) | \(0\) |
\(12 \sim 14\) | \(300\) | \(1000\) | \(100\) |
\(15 \sim 17\) | \(2500\) | \(10000\) | \(0\) |
\(18 \sim 20\) | \(2500\) | \(10000\) | \(100\) |
看题目四个点貌似不可做,但此题由于是无向图,所以有\(A->B\)和\(C->D\)实质上是等价的,有了这一条性质,便可以有解决思路了,这题的断点就是\(B,C\)两点
于是我们可以单独处理每一条满足要求的\(1->A->B\)路径,然后找合法的\(B,C\),满足\(dis(B,C)\le k,dis\)表示最小转点次数(事实上就是两点最短路径上的点的数量减2)
由于边权都是1,可以\(BFS\)处理全源最短路\(O(n^2)\),枚举\(B,C\)的复杂度是\(O(n^2)\)的,至于路径\(1->A->B\)的处理很简单,对于每一个\(B\)枚举\(A\),只需要\(A\)满足\(dis(1,A)\le k,dis(A,B)\le k\)即可,然后因为是最优性问题,我们很明显需要\(A\)的最大值,但由于需要枚举两点,并且枚举\(B,C\)的时候可能\(C\)与\(A\)相撞,所以我们应该存下满足条件的\(A\)的权值前三大
#define int long long
using namespace std;
int dis[2600][2650],mn[20550][5],val[25050],n,m,k,head[100005],nxt[25000],ver[25000],p[5],tot,ans;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void bfs(int s){
queue<int>q;
q.push(s);
dis[s][s]=1;
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(dis[s][v]>dis[s][u]+1){
dis[s][v]=dis[s][u]+1;
q.push(v);
}
}
}
}
void init(){
scanf("%lld%lld%lld",&n,&m,&k);
k+=2;
for(int i=2;i<=n;i++)scanf("%lld",&val[i]);
for(int i=1;i<=m;i++){
int u,v;
scanf("%lld%lld",&u,&v);
add(u,v);
add(v,u);
}
memset(dis,0x3f,sizeof dis);
for(int i=1;i<=n;i++)
bfs(i);
for(int i=2;i<=n;i++){
for(int j=2;j<=n;j++){
if(j==i)continue;
if(dis[1][j]<=k&&dis[j][i]<=k){
if(val[j]>=val[mn[i][0]])mn[i][2]=mn[i][1],mn[i][1]=mn[i][0],mn[i][0]=j;
else if(val[j]>=val[mn[i][1]])mn[i][2]=mn[i][1],mn[i][1]=j;
else if(val[j]>=val[mn[i][2]])mn[i][2]=j;
}
}
}
int Ans=-1ll<<60;
for(int i=2;i<=n;i++){
for(int j=2;j<=n;j++){
if(i==j||dis[i][j]>k)continue;
int ans=-(1ll<<60);
for(int k=0;k<3;k++){
for(int x=0;x<3;x++)if(mn[i][k]!=j&&mn[j][x]!=i&&mn[i][k]!=mn[j][x]&&min(mn[i][k],mn[j][x])!=0ll)ans=max(ans,val[mn[i][k]]+val[mn[j][x]]);
}
Ans=max(Ans,ans+val[i]+val[j]);
}
}
printf("%lld",Ans);
}
signed main(){
init();
}
例题3.北大ACM队的远足
给定一张 N 个点 M 条边的有向无环图,点的编号从 0 到 N−1,每条边都有一个长度。
给定一个起点 S 和一个终点 T。
若从 S 到 T 的每条路径都经过某条边,则称这条边是有向图的必经边或桥。
北大 ACM 队要从 S 点到 T 点。
他们在路上可以搭乘两次车。
每次可以从任意位置(甚至是一条边上的任意位置)上车,从任意位置下车,但连续乘坐的长度不能超过 q 米。
除去这两次乘车外,剩下的路段步行。
定义从 S 到 T 的路径的危险程度等于步行经过的桥上路段的长度之和。
求从 S 到 T 的最小危险程度是多少。
首先求\(DAG\)的桥可以使用路径条数取模法,防止冲突的话用孪生素数\(10^9+7,10^9+9\),很明显的是最短路一定不会比其他路更差,于是我们求出一条最短路,问题就变成了用两条长度为\(q\)的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。
但因为\(S->T\)的最短路不止一条,于是我们现在来证明选择其中任意一条即可
引理1:任意两条最短路经过的桥的顺序相同
证明:反证法,如果不相同,我们假设最短路1是从\(i\)到\(j\),最短路2是从\(j\)到\(i\),那么一定存在一条从\(i\)到\(j\)的路径,也一定存在一条从\(j\)到\(i\)的路径,这就形成了环,与DAG定义不符,故假设不成立,原命题成立
引理2:任意两条最短路经过的两个桥中间相隔的距离相同
证明:反证法,假设最短路1经过的距离为\(D1\),最短路2经过的距离为\(D2\),不妨设\(D1>D2\),那么最短路2可以用\(D1\)替换掉\(D2\),这与最短路的定义不符,故假设不成立,原命题成立
由此,我们任选一条最短路即可,至于\(DAG\)的最短路,可以直接拓扑排序,\(DP\)求出,然后\(pre\)递归回溯即可。顺带我们还可以求出路径条数以寻找桥
现在我们将最短路抽离出来单独放进数组里面,并且做一个路径长度前缀和,桥的长度前缀和,这时候这条最短路就成为了一条线段,其上有若干个点
那么我们回到解决这个问题:求用两条长度为\(q\)的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。
这个扫描,因为两条不太好处理,我们不妨考虑如果只有一条的情况。设\(ds[i]\)表示线段上从第一个点到第\(i\)个点用一条\(q\)所能覆盖掉桥的最大长度
考虑转移:这里的\(i\)表示边\((i-1,i)\)
若这条边不是必经边,直接从\(ds[i-1]\)继承
若这条边是必经边,那么我们令这条覆盖线段的右端点放在\(i\),此时它覆盖桥的最大长度为\(q-dis(i,j)+len(i,j)\),其中\(dis\)表示两点距离,\(len\)表示两点间桥的总长度,\(j\)是满足\(dis(i,j)\le q\)并且\((j-1,j)\)是桥的最小的\(j\),很明显\(j\)具有单调性,所以我们可以采用双指针扫描的方式更新答案,类似的我们处理出\(dt[i]\)表示线段上从第\(i\)个点到最后个点用一条\(q\)所能覆盖掉桥的最大长度,此时我们便可以枚举断边\((i-1,i)\)了,更新答案即可
最后我们还需要考虑两个线段连在一起的情况,类比上面以一条长度为\(2q\)的线段扫就是
#include<bits/stdc++.h>
using namespace std;
const int N = 100005, M = 200005, mod = 1000000007;
int ver[M*2], edge[M*2], nxt[M*2], head[N], tot;
int f[2][N], deg[2][N], d[N], pre[N], n, m, s, t, bus;
bool bridge[M*2];
int a[N], b[N], cnt; // 长度、是不是桥
int sum[N], sum_bri[N], ds[N], dt[N], ds_min[N];
int occur[N], first_occur[N];
queue<int> q;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
void topsort(int s, int bit) {
if (bit == 0) { // 只有正图需要求最短路
memset(d, 0x3f, sizeof(d));
d[s] = 0;
}
f[bit][s] = 1;
for (int i = 1; i <= n; i++)
if (deg[bit][i] == 0) q.push(i);
while (!q.empty()) {
int x = q.front(); q.pop();
for (int i = head[x]; i; i = nxt[i])
if ((i & 1) == bit) {
int y = ver[i];
f[bit][y] = (f[bit][y] + f[bit][x]) % mod; // 路径条数
if (bit == 0 && d[y] > d[x] + edge[i]) { // 最短路
d[y] = d[x] + edge[i];
pre[y] = i;
}
if (--deg[bit][y] == 0) q.push(y);
}
}
}
int main() {
int C; cin >> C;
while (C--) {
memset(head, 0, sizeof(head));
memset(deg, 0, sizeof(deg));
memset(f, 0, sizeof(f));
memset(bridge, 0, sizeof(bridge));
memset(occur, 0, sizeof(occur));
tot = 1; cnt = 0;
cin >> n >> m >> s >> t >> bus;
s++; t++;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
x++, y++;
add(x, y, z); // 偶数边是正边(邻接表2, 4, 6,...位置)
add(y, x, z); // 奇数边是反边
deg[0][y]++; // 入度
deg[1][x]++; // 出度
}
topsort(s, 0);
if (f[0][t] == 0) { puts("-1"); continue; }
topsort(t, 1);
for (int i = 2; i <= tot; i += 2) {
int x = ver[i ^ 1], y = ver[i];
if ((long long)f[0][x] * f[1][y] % mod == f[0][t]) {
bridge[i] = true;
}
}
// O(M)判重边,用map可能超时
for (int x = 1; x <= n; x++) {
for (int i = head[x]; i; i = nxt[i]) {
if (i & 1) continue; // 只考虑正边
int y = ver[i];
if (occur[y] == x) {
bridge[i] = false;
bridge[first_occur[y]] = false;
} else {
occur[y] = x;
first_occur[y] = i;
}
}
}
while (t != s) {
a[++cnt] = edge[pre[t]];
b[cnt] = bridge[pre[t]];
t = ver[pre[t] ^ 1];
}
// reverse(a + 1, a + cnt + 1); 不反过来也可以
// reverse(b + 1, b + cnt + 1);
for (int i = 1; i <= cnt; i++) {
sum[i] = sum[i - 1] + a[i]; // 以i这条边为结尾(包含i)的前缀总长度
sum_bri[i] = sum_bri[i - 1] + (b[i] ? a[i] : 0);
}
ds_min[0] = 1 << 30;
for (int i = 1, j = 0; i <= cnt; i++) { // 恰好在i这条边的结尾处下车,前面的最小危险程度:ds[i]
// 双指针扫描,让j+1~i这些边乘车,j这条边有可能部分乘车
while (sum[i] - sum[j] > bus) j++;
ds[i] = sum_bri[j];
if (j > 0 && b[j]) ds[i] -= min(a[j], bus - (sum[i] - sum[j]));
ds_min[i] = min(ds[i], ds_min[i - 1] + (b[i] ? a[i] : 0)); // i之前搭一次车:ds_min[i],即书上的"ds[i]"
}
for (int i = cnt, j = cnt + 1; i; i--) { // 恰好在i这条边的开头处上车,后面的最小危险程度:ds[i]
// 双指针扫描,让i~j-1这些边乘车,j这条边有可能部分乘车
while (sum[j - 1] - sum[i - 1] > bus) j--;
dt[i] = sum_bri[cnt] - sum_bri[j - 1];
if (j <= cnt && b[j]) dt[i] -= min(a[j], bus - (sum[j - 1] - sum[i - 1]));
}
// 两段乘车分开的情况
int ans = 1 << 30;
for (int i = 1; i <= cnt; i++)
ans = min(ans, dt[i] + ds_min[i - 1]);
// 两段乘车接在一起,2*bus覆盖一次的情况
for (int i = 1, j = 0; i <= cnt; i++) {
while (sum[i] - sum[j] > 2 * bus) j++;
int temp = sum_bri[j];
if (j > 0 && b[j]) temp -= min(a[j], 2 * bus - (sum[i] - sum[j]));
ans = min(ans, temp + sum_bri[cnt] - sum_bri[i]);
}
cout << ans << endl;
}
}
现在我们来对适合运用切点方法的题目做个总结
题目一般都是关于2或者2的幂次,然后将一个大的整体划分为两个小的分别计算,进而\(O(1)\)得枚举一个方案,进而\(O(n)\)枚举断点得出解
一般情况下,套路就是先思考简化版问题,尝试以划分子问题的思想,将简化版问题用类似于递推思想的求出数据范围为\(0\sim n\)的解,然后枚举断点,将两个部分的解合并