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\)段的每个武器炸掉,那么我们就连一条从\(i\)\(j\)的边。如果最后的二分图最大匹配为段的个数,意味着这个方案是合法的。

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

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

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

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

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

优化四:A*(最优性剪枝2)
我们设\(least(i)\)表示从武器\(i\)开始,最少需要多少个炸弹,才能将剩下的武器消灭,则若\(len-1+least(arr(len))\geq ans\),可以直接返回。(这里\(len\)\(1\)是因为我们目前只有\((arr_1,arr_2),(arr_2,arr_3),...,(arr_{len-1},arr_len)\)\((len-1)\)个段,而不是\(len\)个段)
\(least(i)\),我们可以不考虑是否重复使用炸弹的问题,从而得到答案的下界,即\(least(i)=\min_{j=1}^{n}(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(n^2)\)的检查,变成了\(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 @ 2022-07-06 14:18  Nastia  阅读(180)  评论(0)    收藏  举报