# 枚举切点思想

枚举切点思想

枚举切点思想是一个非常常用的思想方法,大致就是说有两个部分,将一些东西枚举划到一个部分,其余划到另一个部分的最优解

例题1.CSP-2021廊桥分配
题目描述

当一架飞机抵达机场时,可以停靠在航站楼旁的廊桥,也可以停靠在位于机场边缘的远机位。乘客一般更期待停靠在廊桥,因为这样省去了坐摆渡车前往航站楼的周折。然而,因为廊桥的数量有限,所以这样的愿望不总是能实现。

机场分为国内区和国际区,国内航班飞机只能停靠在国内区,国际航班飞机只能停靠在国际区。一部分廊桥属于国内区,其余的廊桥属于国际区。

L 市新建了一座机场,一共有 n 个廊桥。该机场决定,廊桥的使用遵循“先到先得”的原则,即每架飞机抵达后,如果相应的区(国内/国际)还有空闲的廊桥,就停靠在廊桥,否则停靠在远机位(假设远机位的数量充足)。该机场只有一条跑道,因此不存在两架飞机同时抵达的情况。

现给定未来一段时间飞机的抵达、离开时刻,请你负责将 n 个廊桥分配给国内区和国际区,使停靠廊桥的飞机数量最多。

输入格式

输入的第一行,包含三个正整数 n,m1,m2,分别表示廊桥的个数、国内航班飞机的数量、国际航班飞机的数量。

接下来 m1 行,是国内航班的信息,第 i 行包含两个正整数 a1,i,b1,i,分别表示一架国内航班飞机的抵达、离开时刻。

接下来 m2 行,是国际航班的信息,第 i 行包含两个正整数 a2,i,b2,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 离开的飞机;用 表示该飞机停靠在廊桥,用 × 表示该飞机停靠在远机位。

我们以表格中阴影部分的计算方式为例,说明该表的含义。在这一部分中,国际区有 2 个廊桥,4 架国际航班飞机依如下次序抵达:

  1. 首先 (2,11) 在时刻 2 抵达,停靠在廊桥。
  2. 然后 (4,15) 在时刻 4 抵达,停靠在另一个廊桥。
  3. 接着 (7,17) 在时刻 7 抵达,这时前 2 架飞机都还没离开、都还占用着廊桥,而国际区只有 2 个廊桥,所以只能停靠远机位。
  4. 最后 (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% 的数据,n100m1+m2100
对于 40% 的数据,n5000m1+m25000
对于 100% 的数据,1n105m1,m21m1+m2105,所有 a1,i,b1,i,a2,i,b2,i 为数值不超过 108 的互不相同的正整数,且保证对于每个 i[1,m1],都有 a1,i<b1,i,以及对于每个 i[1,m2],都有 a2,i<b2,i

先来考虑简化版问题:如果只有一类飞机抵达,那么如何求出前n个飞机停靠的廊桥的最小值

很简单,用堆维护各个廊桥的结束使用时间,当每一个新飞机来的时候,就判断一下这个飞机是否可以用已经空出的机位,不行就新开,在每个飞机来的时候将已经空出的机位弹出堆顶,这样我们就可以得到一个t,为最大使用的廊桥数量。那么进一步思考,我们能否在一个足够优秀的时间复杂度内求出i[1,n]个廊桥最最多容纳的飞机数量
这个比较好解决,试问我们如果知道每一个飞机都停在哪一个廊桥(分配廊桥的时候以编号最小的空闲廊桥为准,正确性显然),那么是不是就可以轻易的前缀和求出答案了?

那么我们对国内国外两类都进行这样的操作,记录答案为S1,S2总复杂度仍然是O(mlogn)的,进一步的,我们就可以枚举断点了,对于每一个x[0,n],令ans=max(ans,S1x+S2nx),最终便可以求出解

#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,,n 的都是景点。部分点对之间有双向直达的公交线路。如果点 xz1z1z2、……、zk1zkzky 之间均有直达的线路,那么我们称 xy 之间的行程可转车 k 次通达;特别地,如果点 xy 之间有直达的线路,则称可转车 0 次通达。

很快就要放假了,小熊计划从家出发去 4不同的景点游玩,完成 5 段行程后回家:家 景点 A 景点 B 景点 C 景点 D 家且每段行程最多转车 k 次。转车时经过的点没有任何限制,既可以是家、也可以是景点,还可以重复经过相同的点。例如,在景点 A 景点 B 的这段行程中,转车时经过的点可以是家、也可以是景点 C,还可以是景点 D 家这段行程转车时经过的点。

假设每个景点都有一个分数,请帮小熊规划一个行程,使得小熊访问的四个不同景点的分数之和最大。

输入格式

第一行包含三个正整数 n,m,k,分别表示地图上点的个数、双向直达的点对数量、每段行程最多的转车次数。

第二行包含 n1 个正整数,分别表示编号为 2,3,,n 的景点的分数。

接下来 m 行,每行包含两个正整数 x,y,表示点 xy 之间有道路直接相连,保证 1x,yn,且没有重边,自环。

输出格式

输出一个正整数,表示小熊经过的 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】

当计划的行程为 123571 时,4 个景点的分数之和为 9+7+8+3=27,可以证明其为最大值。

行程 135781 的景点分数之和为 24、行程 132871 的景点分数之和为 25。它们都符合要求,但分数之和不是最大的。

行程 123581 的景点分数之和为 30,但其中 58 至少需要转车 2 次,因此不符合最多转车 k=1 次的要求。

行程 123231 的景点分数之和为 32,但游玩的并非 4 个不同的景点,因此也不符合要求。

【数据范围】

对于所有数据,保证 5n25001m100000k100,所有景点的分数 1si1018。保证至少存在一组符合要求的行程。

测试点编号 n m k
13 10 20 0
45 10 20 5
68 20 50 100
911 300 1000 0
1214 300 1000 100
1517 2500 10000 0
1820 2500 10000 100

看题目四个点貌似不可做,但此题由于是无向图,所以有A>BC>D实质上是等价的,有了这一条性质,便可以有解决思路了,这题的断点就是B,C两点

于是我们可以单独处理每一条满足要求的1>A>B路径,然后找合法的B,C,满足dis(B,C)kdis表示最小转点次数(事实上就是两点最短路径上的点的数量减2)

由于边权都是1,可以BFS处理全源最短路O(n2),枚举B,C的复杂度是O(n2)的,至于路径1>A>B的处理很简单,对于每一个B枚举A,只需要A满足dis(1,A)k,dis(A,B)k即可,然后因为是最优性问题,我们很明显需要A的最大值,但由于需要枚举两点,并且枚举B,C的时候可能CA相撞,所以我们应该存下满足条件的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的桥可以使用路径条数取模法,防止冲突的话用孪生素数109+7,109+9,很明显的是最短路一定不会比其他路更差,于是我们求出一条最短路,问题就变成了用两条长度为q的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。

但因为S>T的最短路不止一条,于是我们现在来证明选择其中任意一条即可

引理1:任意两条最短路经过的桥的顺序相同

证明:反证法,如果不相同,我们假设最短路1是从ij,最短路2是从ji,那么一定存在一条从ij的路径,也一定存在一条从ji的路径,这就形成了环,与DAG定义不符,故假设不成立,原命题成立

引理2:任意两条最短路经过的两个桥中间相隔的距离相同

证明:反证法,假设最短路1经过的距离为D1,最短路2经过的距离为D2,不妨设D1>D2,那么最短路2可以用D1替换掉D2,这与最短路的定义不符,故假设不成立,原命题成立

由此,我们任选一条最短路即可,至于DAG的最短路,可以直接拓扑排序,DP求出,然后pre递归回溯即可。顺带我们还可以求出路径条数以寻找桥

现在我们将最短路抽离出来单独放进数组里面,并且做一个路径长度前缀和,桥的长度前缀和,这时候这条最短路就成为了一条线段,其上有若干个点
那么我们回到解决这个问题:求用两条长度为q的线段在这条最短路上扫(最短路一定是一条链),使得扫到的桥的长度最大。

这个扫描,因为两条不太好处理,我们不妨考虑如果只有一条的情况。设ds[i]表示线段上从第一个点到第i个点用一条q所能覆盖掉桥的最大长度

考虑转移:这里的i表示边(i1,i)

若这条边不是必经边,直接从ds[i1]继承
若这条边是必经边,那么我们令这条覆盖线段的右端点放在i,此时它覆盖桥的最大长度为qdis(i,j)+len(i,j),其中dis表示两点距离,len表示两点间桥的总长度,j是满足dis(i,j)q并且(j1,j)是桥的最小的j,很明显j具有单调性,所以我们可以采用双指针扫描的方式更新答案,类似的我们处理出dt[i]表示线段上从第i个点到最后个点用一条q所能覆盖掉桥的最大长度,此时我们便可以枚举断边(i1,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)枚举断点得出解

一般情况下,套路就是先思考简化版问题,尝试以划分子问题的思想,将简化版问题用类似于递推思想的求出数据范围为0n的解,然后枚举断点,将两个部分的解合并

posted @   spdarkle  阅读(55)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示