NOI 2003 智破连环阵
一开始我们能发现答案和时间没有关系。(因为时间是秒,而一共最多就个数,不会出现炸弹还能摧毁,但是超过引爆时间的现象)问题得到简化。
之后我的思路有些偏。我想用类似DP+乱搞或模拟退火的方式,用正确性换时间复杂度正确,但是是没有用的。(DP主要是太慢而不正确;模拟退火是没法较好处理当前排列无解的情况)
所以我们要用时间换正确性。考虑比较暴力做法,进行剪枝。
首先我们能想到的暴力当然是枚举排列。但是这个做法貌似除了最优性剪枝没什么好的剪枝了,并且还是级别的。
所以我们不能暴力枚举这个。考虑我们枚举连环阵的段,相同段内的独立武器都是被同一个炸弹摧毁的。比如样例:
4 3 6
0 6
6 6
6 0
0 0
1 5
0 3
1 1
中,我们分为两段。第一段是被炸弹摧毁的;第二段是被炸弹摧毁的。
枚举完一个可能的方案后,我们就可以处理出,对于每一段,我们可以用哪些炸弹把整段炸掉。这显然是一个二分图问题:炸弹在一边,每个段在另一边;当炸弹可以将第段的每个武器炸掉,那么我们就连一条从到的边。如果最后的二分图最大匹配为段的个数,意味着这个方案是合法的。
这样时间复杂度为。相比于的直接暴力,我们有更好的基础去优化它。
在枚举时,我们记录数组,从小到大存储每一个段的开头;并且为当前的长度。则我们可以得到下面一系列优化:
优化一:最优性剪枝
如果当前的段数比答案更多(或相等),可以直接返回。
优化二:可行性剪枝
如果我们枚举的段,没有一个炸弹可以炸掉它。那么我们不必要枚举。我们记录表示炸弹从第个武器开始,第一个炸不到的武器是多少,表示从武器开始,第一个没有炸弹可以炸掉的武器,则。我们枚举时,只要从到就可以了,没必要枚举到。
优化三:改变枚举顺序
通常我们从从小到大枚举到。但是我们发现,从大到小枚举,可以使得先枚举到的是覆盖尽可能多的区间,从而有希望能使得最后段数尽可能小。这样可以更好配合最优性剪枝。(可以更早的更新到一个更小的值)
优化四:A*(最优性剪枝2)
我们设表示从武器开始,最少需要多少个炸弹,才能将剩下的武器消灭,则若,可以直接返回。(这里减是因为我们目前只有这个段,而不是个段)
求,我们可以不考虑是否重复使用炸弹的问题,从而得到答案的下界,即。
至此,我们写出下面的程序:
#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;
}
此时我们得到分,仍然有三个点过不去。
优化五:观察匈牙利算法的性质继续优化
我们发现,匈牙利算法是对于某一侧的每个点跑一边增广路。
所以,我们可以在搜索的时候建图、对每一个新加入的段(注意不是炸弹)跑增广路,注意要开一个数组来存在跑增广路之前的数组,方便进行回溯。
这样原来是的检查,变成了的跑增广路。但是这个优化仍然不明显。
优化六:观察二分图匹配的性质
我们发现,最后一定匹配数要等于才是合法的方案。那么中间的任何一次,我们没法跑增广路,都会使得答案小于。所以当调用match函数时返回,我们就不就像往下搜索。
至此,所有的优化都齐全了,程序也很快,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;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!