# 枚举切点思想

枚举切点思想

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

例题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\) 架国际航班飞机依如下次序抵达:

  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 \%\) 的数据,\(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\)的解,然后枚举断点,将两个部分的解合并

posted @ 2022-11-15 08:34  spdarkle  阅读(54)  评论(0编辑  收藏  举报