[CSP-S 2024] 超速检测 逐个测试点详解,带你骗分


考场上如果想不到正解,不妨先写其中测试点的答案,一方面是先拿到一些分数,另一方面也可以脱开思路,有助于想到正确的解法。

题目大意

一条路从南到北长度 \(L\),若干车辆从特定位置 \(d_i\),特定速度 \(v_i\),进入道路,以恒定加速度 \(a_i\) 行驶,速度为 \(0\) 算驶离。道路上有若干测速仪。问总共有多少车辆超速,最少用几个测速仪就可以测全部超速车辆。

特殊性质A 20分

特殊性质A:保证 \(a_i=0\) 即所有车都以恒定速度行驶,那么所有车都会从道路尽头离开道路,不会中间退出。我们只需要选取最远的测速点,就可以保证最大限度的不漏超速车辆。但如果最远的测速点也没有测到超速车辆,那么就可以撤掉所有超速点。

const int MAXN = 100000+5;
int T;
int n,m,L,V;
int d[MAXN],v[MAXN],a[MAXN],p[MAXN];

void input(){
	cin>>n>>m>>L>>V;
	for(int i=0;i<n;i++){
		cin>>d[i]>>v[i]>>a[i];
	}
	for(int i=0;i<m;i++){
		cin>>p[i];
	}
}

void solveA(){
	int ans = 0;
	if( m == 0){
		cout<<"0 0\n";
		return ;
	}
	// 找到最远的测速点 
	int pp = p[0];
	for(int i=0;i<m;i++){
		if(pp < p[i]){
			pp = p[i];
		}
	}
	for(int i=0;i<n;i++){
		if(v[i] > V && d[i] <= pp){
			ans ++;
		}
	} 
	if(ans > 0) 
		cout<<ans<<" "<<m-1<<endl;
	else
		cout<<ans<<" "<<m<<endl;
}

int main(){
	freopen("detect3.in","r",stdin);
	cin>>T;
	while(T--){
		input();
		bool checkA = true;
		for(int i=0;i<n;i++){
			if(a[i] != 0)checkA = false;
		}
		if(checkA){
			solveA();
		}
	}

	return 0;
}

特殊性质B 20分

特殊性质B,保证 \(a_i\gt0\) 即所有车都加速行驶,这里的情况和A类似,所有车辆都只能从道路尽头驶离道路。假设一辆车被某个测速点测到超速,由于一直在加速,那一定会被最后一个测速点测到超速。我们只需要在A的基础上稍加修改,并分类讨论初始速度的情况。

\[超速开始位置\begin{cases} d_i - esp &\text{if }v_i\gt V& 已经超速了,所以向前减一点 \\ d_i &\text{if }v_i=V& 刚好开始加速,从开始位置开始超速\\ d_i +(V^2-v_i^2)/2a_i+esp&\text{if }v_i\gt V & 经过一段路程后开始超速\\ \end{cases} \]

\[(esp:取0.0000001,为了解决超速点速度假如恰好等于V但不算超速的情况) \]

const double ESP = 0.0000001;
void solveB(){
	int ans = 0;
	if( m == 0){
		cout<<"0 0\n";
		return ;
	}
	// 找到最远的测速点 
	int pp = p[0];
	for(int i=0;i<m;i++){
		if(pp < p[i]){
			pp = p[i];
		}
	}
	for(int i=0;i<n;i++){
		double start ;
		if(v[i] > V) start = d[i] - ESP;
		else if(v[i] == V) start = d[i];
		else if(v[i] < V) start = d[i] + (V*V - v[i]*v[i])/(2.0*a[i]) + ESP;
		
		if(start < pp){
			ans++;
		}
	} 
	if(ans > 0) 
		cout<<ans<<" "<<m-1<<endl;
	else
		cout<<ans<<" "<<m<<endl;
}

特殊性质C 20分

这个没啥特殊的了,不如下面来看看小数据规模吧

\(n\le10\) 10分

最简单暴力的方法,就是首先预处理每辆车的超速区间,然后枚举每个测速点是否启用,再判断每辆车在哪些测速点超速了。

\(n\le10\) 时,方案总数\(2^n\approx10^3\),每一种方案需要枚举判断每辆车,复杂度 \(n\),一辆车的超速区间需要二分判断 \(logm\) ,总复杂度 \(T2^nnlogm \approx 10^6\)

const int MAXN = 100000+5;
int T;
int n,m,L,V;
int d[MAXN],v[MAXN],a[MAXN],p[MAXN];

void input(){
	cin>>n>>m>>L>>V;
	for(int i=0;i<n;i++){
		cin>>d[i]>>v[i]>>a[i];
	}
	for(int i=0;i<m;i++){
		cin>>p[i];
	}
}

int range[MAXN][2];
int rangeCnt;
void addRange(double start,double end){
	int* l = upper_bound(p,p+m,start); 	// 大于 
	int* r = upper_bound(p,p+m,end);	// 大于
	if(r > l){
		range[rangeCnt][0] = l-p;
		range[rangeCnt][1] = r-p;
		rangeCnt++;
	}
}

const double ESP = 0.0000001;
void pre(){
	for(int i=0;i<n;i++){
		if(a[i] == 0){
			if(v[i] > V){
				addRange(d[i]-ESP,L);
			}
		}else if(a[i] > 0){
			double start ;
			if(v[i] > V) start = d[i] - ESP;
			else if(v[i] == V) start = d[i];
			else if(v[i] < V) start = d[i] + (V*V - v[i]*v[i])/(2.0*a[i]) + ESP;
			
			addRange(start,L);
			
		}else if(a[i] < 0 && v[i] > V){
			double end = d[i]+(V*V - v[i]*v[i])/(2.0*a[i]) - ESP;
			addRange(d[i]-ESP,end);
		}
	}
}

int plan[MAXN],planCnt;
int ans;
int lastFailI;
bool check(){
	for(int i=0;i<rangeCnt;i++){
		// 车辆的超速区间是[range[i][0],range[i][1]) plan是启用了的测速点
		// 需要判断plan里面的点有没有在车辆的超速区间内 
		int* l = lower_bound(plan,plan+planCnt,range[i][0]); // l 是一个大于等于range[i][0]的 
		int* r = lower_bound(plan,plan+planCnt,range[i][1]); // r >= range[i][1]
		
		if(r <= l){
			return false;
		}
	}
	return true;
}
void dfs(int v){
	if(v == m){
		// 判断方案是否合法 
		if(check()){
			if(planCnt < ans){
				ans = planCnt;
			}
			
		}
		return;
	}
	dfs(v+1);
	plan[planCnt++] = v;
	dfs(v+1);
	planCnt--;
}

void solveS(){
	ans = m;
	planCnt = 0;
	rangeCnt = 0;
	// 分两步,首先预处理所有车辆的超速区间
	pre();
	// 其次枚举所有方案
	dfs(0);
	printf("%d %d\n",rangeCnt,m-ans);
}

int main(){
	cin>>T;
	while(T--){
		input();
		if(n <= 20)
			solveS();
	}
	return 0;
}

\(n\le20\) 10分

如果我们再采取上面的方法,\(n=20\)时,复杂度可以达到\(10^9\)。常数有一点点大,我们需要引入这样两个神奇的剪枝优化:

  1. 二分答案,如果保留10个测速点是答案,那么11个测速点肯定也能测到所有超速车辆,9个测速点一定会漏测。
  2. 由于枚举方案的时候,相邻方案之间十分相似,那么也大概率会因为没有测到同一辆超速的车而被淘汰,我们可以优先用上次方案当中没有测到的车辆来考验下次的方案。

实测: 在样例2当中,通过以上两个优化,可以减少90%的判断。

const int MAXN = 100000+5;
int T;
int n,m,L,V;
int d[MAXN],v[MAXN],a[MAXN],p[MAXN];

void input(){
	cin>>n>>m>>L>>V;
	for(int i=0;i<n;i++){
		cin>>d[i]>>v[i]>>a[i];
	}
	for(int i=0;i<m;i++){
		cin>>p[i];
	}
}

const double ESP = 0.0000001;
int range[MAXN][2];
int rangeCnt;
void addRange(double start,double end){
	int* l = upper_bound(p,p+m,start); 	// 大于 
	int* r = upper_bound(p,p+m,end);	// 大于
	if(r > l){
		range[rangeCnt][0] = l-p;
		range[rangeCnt][1] = r-p;
		rangeCnt++;
	}
}

void pre(){
	for(int i=0;i<n;i++){
		if(a[i] == 0){
			if(v[i] > V){
				addRange(d[i]-ESP,L);
			}
		}else if(a[i] > 0){
			double start ;
			if(v[i] > V) start = d[i] - ESP;
			else if(v[i] == V) start = d[i];
			else if(v[i] < V) start = d[i] + (V*V - v[i]*v[i])/(2.0*a[i]) + ESP;
			
			addRange(start,L);
			
		}else if(a[i] < 0 && v[i] > V){
			double end = d[i]+(V*V - v[i]*v[i])/(2.0*a[i]) - ESP;
			addRange(d[i]-ESP,end);
		}
	}
}

int plan[MAXN],planCnt;
int ans;
int lastFailI; // 上次失败的序号 
bool check(){
	for(int j=0;j<rangeCnt;j++){
		int i = (j + lastFailI)%rangeCnt; // 从上次失败的序号开始 
		// 车辆的超速区间是[range[i][0],range[i][1]) plan是启用了的测速点
		// 需要判断plan里面的点有没有在车辆的超速区间内 
		int* l = lower_bound(plan,plan+planCnt,range[i][0]); // l >= range[i][0]
		int* r = lower_bound(plan,plan+planCnt,range[i][1]); // r >= range[i][1]
		
		if(r <= l){
			lastFailI = i;
			return false;
		}
	}
	return true;
}
void dfs(int v,int maxPlanCnt){
	if(v==m){
		// 判断方案是否合法 
		if(check()){
			if(planCnt < ans){
				ans = planCnt;
			}
		}
		return;
	}
	if(maxPlanCnt - planCnt < m-v ){
		dfs(v+1,maxPlanCnt);
	}
	if(maxPlanCnt > planCnt){
		plan[planCnt++] = v;
		dfs(v+1,maxPlanCnt);
		planCnt--;
	}
}

void solveS(){
	ans = m;
	planCnt = 0;
	rangeCnt = 0;
	// 分两步,首先预处理所有车辆的超速区间
	pre();
	// 其次枚举所有方案
	int L=-1,R=m;
	while(true){
		if(L+1 == R)break;
		int M = (L+R)/2;
		dfs(0,M);
		if(ans == M){
			R = M;
		}else{
			L = M;
		}
	}
	printf("%d %d\n",rangeCnt,m-ans);
}

int main(){
	cin>>T;
	while(T--){
		input();
		if(n <= 20)
			solveS();
	}
	return 0;
}

最终正解

  1. 首先计算每辆车的超速区间。
  2. 然后按区间左端点为第一关键字,右端点为第二关键字进行排序。
  3. 然后删除多余的无用区间,如果区间a在区间b的内部,也就是la <= lb,ra>=rb,那么区间a就是对答案没有贡献的,因为能测到b超速的测速仪一定会测到a超速。删除多余区间之后,可以保证右端点也是升序排列了。
  4. 在排好序的区间内,选第一个区间,取右端点测速仪。这个测速仪就是需要保留的,判断所有超速的区间,并从列表当中剔除,然后重复此步骤。

优化:实际写代码的时候,我们可以利用右端点必须升序排列这一特点来筛选多余区间。在第二步骤的排序之后,我们可以倒序遍历列表,找到右端点突出的删掉即可。

1: ---
2:  -----
3:    ------   <-这个就是需要删除的,因为右端点比4号的大
4:     ---
5:       -----
6:         ------
const int MAXN = 100000+5;
int T;
int n,m,L,V;
int d[MAXN],v[MAXN],a[MAXN],p[MAXN];

void input(){
	cin>>n>>m>>L>>V;
	for(int i=0;i<n;i++){
		cin>>d[i]>>v[i]>>a[i];
	}
	for(int i=0;i<m;i++){
		cin>>p[i];
	}
}

const double ESP = 0.0000001;
int range[MAXN][2];
int rangeCnt;
void addRange(double start,double end){
	int* l = upper_bound(p,p+m,start); 	// 大于 
	int* r = upper_bound(p,p+m,end);	// 大于
	if(r > l){
		range[rangeCnt][0] = l-p;
		range[rangeCnt][1] = r-p;
		rangeCnt++;
	}
}

void pre(){
	for(int i=0;i<n;i++){
		if(a[i] == 0){
			if(v[i] > V){
				addRange(d[i]-ESP,L);
			}
		}else if(a[i] > 0){
			double start ;
			if(v[i] > V) start = d[i] - ESP;
			else if(v[i] == V) start = d[i];
			else if(v[i] < V) start = d[i] + (V*V - v[i]*v[i])/(2.0*a[i]) + ESP;
			
			addRange(start,L);
			
		}else if(a[i] < 0 && v[i] > V){
			double end = d[i]+(V*V - v[i]*v[i])/(2.0*a[i]) - ESP;
			addRange(d[i]-ESP,end);
		}
	}
}

int id[MAXN],id2[MAXN],removed[MAXN];
int com(int a,int b){
	if(range[b][0] != range[a][0]){
		return range[a][0] < range[b][0];
	}
	return range[a][1] > range[b][1];
}

void solve(){
	for(int i=0;i<n;i++){
		id[i] = i;
	}
	rangeCnt = 0;
	sort(p,p+m);
	// 预处理所有车辆的超速区间 
	pre();
	
	for(int i=0;i<rangeCnt;i++){
		removed[i] = false;
	}
	//按区间左端点为第一关键字,右端点为第二关键字进行排序。 
	sort(id,id+rangeCnt,com);
	
	int minR = range[id[rangeCnt-1]][1];
	for(int i=rangeCnt-1;i>=0;i--){
		if(range[id[i]][1] > minR){
			removed[i] = true;
		}else{
			minR = range[id[i]][1];
		}
	}
	int ans = 0;
	for(int i=0;i<rangeCnt;i++){
		if(!removed[i]){
			int cnt = range[id[i]][1]-1;
			ans++;
			for(;i<rangeCnt;i++){
				if(range[id[i]][0] <= cnt){
					removed[i] = true;
				}else{
					i--;
					break;
				}
			}
		}
	}
	cout<<rangeCnt<<" "<<m-ans<<endl;
}

int main(){
	cin>>T;
	while(T--){
		input();
		solve();
	}
	return 0;
}

posted @ 2025-04-28 14:42  ChenyangDu  阅读(178)  评论(0)    收藏  举报