NOI 2003 智破连环阵

一开始我们能发现答案和时间没有关系。(因为时间是300秒,而一共最多就100个数,不会出现炸弹还能摧毁,但是超过引爆时间的现象)问题得到简化。

之后我的思路有些偏。我想用类似DP+乱搞或模拟退火的方式,用正确性换时间复杂度正确,但是是没有用的。(DP主要是太慢而不正确;模拟退火是没法较好处理当前排列无解的情况)

所以我们要用时间换正确性。考虑比较暴力做法,进行剪枝。

首先我们能想到的暴力当然是枚举排列。但是这个做法貌似除了最优性剪枝没什么好的剪枝了,并且还是n!级别的。

所以我们不能暴力枚举这个。考虑我们枚举连环阵的段,相同段内的独立武器都是被同一个炸弹摧毁的。比如样例:

4 3 6
0 6
6 6
6 0
0 0
1 5
0 3
1 1

中,我们分为(1,2),(3,4)两段。第一段是被炸弹1摧毁的;第二段是被炸弹3摧毁的。

枚举完一个可能的方案后,我们就可以处理出,对于每一段,我们可以用哪些炸弹把整段炸掉。这显然是一个二分图问题:炸弹在一边,每个段在另一边;当炸弹i可以将第j段的每个武器炸掉,那么我们就连一条从ij的边。如果最后的二分图最大匹配为段的个数,意味着这个方案是合法的。

这样时间复杂度为O(n22m)。相比于O(n!)的直接暴力,我们有更好的基础去优化它。

在枚举时,我们记录数组arr,从小到大存储每一个段的开头;并且lenarr当前的长度。则我们可以得到下面一系列优化:

优化一:最优性剪枝
如果当前的段数len比答案ans更多(或相等),可以直接返回。

优化二:可行性剪枝
如果我们枚举的段,没有一个炸弹可以炸掉它。那么我们不必要枚举。我们记录fst(i,j)表示炸弹i从第j个武器开始,第一个炸不到的武器是多少,most(i)表示从武器i开始,第一个没有炸弹可以炸掉的武器,则most(i)=maxj=1n(fst(j,i))。我们枚举时,只要从arr(len)+1most(i)1就可以了,没必要枚举到m

优化三:改变枚举顺序
通常我们从arr(len+1)从小到大枚举到most(i)1。但是我们发现,从大到小枚举,可以使得先枚举到的arr是覆盖尽可能多的区间,从而有希望能使得最后段数尽可能小。这样可以更好配合最优性剪枝。(ans可以更早的更新到一个更小的值)

优化四:A*(最优性剪枝2)
我们设least(i)表示从武器i开始,最少需要多少个炸弹,才能将剩下的武器消灭,则若len1+least(arr(len))ans,可以直接返回。(这里len1是因为我们目前只有(arr1,arr2),(arr2,arr3),...,(arrlen1,arrlen)(len1)个段,而不是len个段)
least(i),我们可以不考虑是否重复使用炸弹的问题,从而得到答案的下界,即least(i)=minj=1n(least(fst(j,i))+1

至此,我们写出下面的程序:

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

const int maxn=205;

int n,m,k,a[maxn],b[maxn],c[maxn],d[maxn];
int fst[maxn][maxn],least[maxn],most[maxn];

int arr[maxn],len,ans=1e9;

int from[maxn],vis[maxn];
std::vector<int> v[maxn];

bool match(int pos) {
	vis[pos]=1;
	for(auto to : v[pos])
		if(from[to]==-1||!vis[from[to]]&&match(from[to])) {
			from[to]=pos;
			return true;
		}
	return false;
}

bool check() {
	memset(from,-1,sizeof from);
	arr[len+1]=m+1;
	for(int i=1;i<=n+len;i++) v[i].clear();
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=len;j++) {
			if(fst[i][arr[j]]>=arr[j+1]) {
				v[i].push_back(n+j);
				v[n+j].push_back(i);
			}
		}
	}
	int cnt=0;
	for(int i=1;i<=n;i++) {
		memset(vis,0,sizeof vis);
		if(match(i)) cnt++;
		if(cnt==len) return true;
	}
	return false;
}

void dfs() {
	if(len+least[arr[len]]-1>=ans||len>n) return;
	if(check()) {ans=std::min(ans,len); return;}
	for(int i=most[arr[len]];i>arr[len];i--) {
		arr[++len]=i;
		dfs();
		len--;
	}
}

int dist(int sx,int sy,int tx,int ty) {
	return (sx-tx)*(sx-tx)+(sy-ty)*(sy-ty);
}

int main() {
	scanf("%d%d%d",&m,&n,&k);
	for(int i=1;i<=m;i++) scanf("%d%d",&a[i],&b[i]);
	for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&d[i]);
	for(int i=1;i<=n;i++) {
		fst[i][m+1]=m+1;
		for(int j=m;j>=1;j--) {
			if(dist(c[i],d[i],a[j],b[j])>k*k) fst[i][j]=j;
			else fst[i][j]=fst[i][j+1];
		}
	}
	for(int i=1;i<=m;i++) least[i]=1e9;
	for(int i=m;i>=1;i--) {
		most[i]=i;
		for(int j=1;j<=n;j++) {
			most[i]=std::max(most[i],fst[j][i]);
			least[i]=std::min(least[i],least[fst[j][i]]+1);
		}
	}
	arr[1]=1; len=1;
	dfs();
	printf("%d\n",ans);
	return 0;
} 

此时我们得到70分,仍然有三个点过不去。

优化五:观察匈牙利算法的性质继续优化
我们发现,匈牙利算法是对于某一侧的每个点跑一边增广路。
所以,我们可以在搜索的时候建图、对每一个新加入的段(注意不是炸弹)跑增广路,注意要开一个数组来存在跑增广路之前的from数组,方便进行回溯。
这样原来是O(n2)的检查,变成了O(n)的跑增广路。但是这个优化仍然不明显。

优化六:观察二分图匹配的性质
我们发现,最后一定匹配数要等于len才是合法的方案。那么中间的任何一次,我们没法跑增广路,都会使得答案小于len。所以当调用match函数时返回0,我们就不就像往下搜索。

至此,所有的优化都齐全了,程序也很快,10个点一共就38ms。

#include<bits/stdc++.h>
#define debug(...) std::cerr<<#__VA_ARGS__<<" : "<<__VA_ARGS__<<std::endl

const int maxn=205;

int n,m,k,a[maxn],b[maxn],c[maxn],d[maxn];
int fst[maxn][maxn],least[maxn],most[maxn];
int arr[maxn],len,ans=1e9;
int from[maxn],vis[maxn],g[maxn][maxn];

bool match(int pos) {
	vis[pos]=1;
	for(int to=1;to<=n+len;to++)
		if(g[pos][to]) if(from[to]==-1||!vis[from[to]]&&match(from[to])) {
			from[to]=pos;
			return true;
		}
	return false;
}

void dfs() { 
	if(arr[len]<=m&&len+least[arr[len]]-1>=ans||len-1>n) return;
	if(arr[len]==m+1) {ans=std::min(ans,len-1); return;}
	int temp[maxn];
	for(int i=most[arr[len]];i>arr[len];i--) {
		arr[++len]=i;
		for(int j=0;j<=n+len;j++) temp[j]=from[j];
		for(int j=1;j<=n;j++) {
			if(fst[j][arr[len-1]]>=arr[len]) {
				g[len-1+n][j]=g[j][len-1+n]=1;
			} else {
				g[len-1+n][j]=g[j][len-1+n]=0;
			}
		}
		memset(vis,0,sizeof vis);
		if(match(len-1+n)) dfs();
		for(int j=0;j<=n+len;j++) from[j]=temp[j];
		len--;
	}
}

int dist(int sx,int sy,int tx,int ty) {
	return (sx-tx)*(sx-tx)+(sy-ty)*(sy-ty);
}

int main() {
	scanf("%d%d%d",&m,&n,&k);
	for(int i=1;i<=m;i++) scanf("%d%d",&a[i],&b[i]);
	for(int i=1;i<=n;i++) scanf("%d%d",&c[i],&d[i]);
	for(int i=1;i<=n;i++) {
		fst[i][m+1]=m+1;
		for(int j=m;j>=1;j--) {
			if(dist(c[i],d[i],a[j],b[j])>k*k) fst[i][j]=j;
			else fst[i][j]=fst[i][j+1];
		}
	}
	for(int i=1;i<=m;i++) least[i]=1e9;
	for(int i=m;i>=1;i--) {
		most[i]=i;
		for(int j=1;j<=n;j++) {
			most[i]=std::max(most[i],fst[j][i]);
			least[i]=std::min(least[i],least[fst[j][i]]+1);
		}
	}
	memset(from,-1,sizeof from);
	arr[1]=1; len=1;
	dfs();
	printf("%d\n",ans);
	return 0;
}
posted @   Nastia  阅读(97)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示