又是被楼教主虐的体无完肤的一天

题意描述

在一个平面坐标系中,有 \(M\) 个目标点和 \(N\) 个炸弹,你的目标是用尽量少的炸弹炸毁所有目标。

目标点是有顺序的,只有每个目标点的前一个被毁灭,它才能被攻击。

每个炸弹可以炸到距离它不超过 \(k\) 个单位长度的点,一旦一个炸弹被启用,就可以一直对攻击范围内的点攻击。

求使用炸弹的最少数目。

康不懂走传送门

算法分析

闲话

忙人自动略过。

为了写一道题康懂了一篇论文。(《匹配算法在搜索中的应用 楼天城》)

推荐大家都上网找找康康(找不到可以找我要),讲的肯定比蒟蒻我好啊。

初步分析

面对此类问题,一般只能使用搜索策略。

但是纯粹的搜索必然是超时的(不要问我为什么),所以可以结合二分图解决。

当然二分图不是一上来就可以想出来的,所以下面要一步一步引出思路。

具体思路

显然,一个目标点会且仅会被一个炸弹摧毁,所以可以建立二分图。

  1. 运用搜索策略将目标点分成 \(x\) 个集合,每个集合都由标号连续的一段目标点,并且可以被同一个炸弹摧毁。

  2. 将每个集合与能消灭它的炸弹连边。

  3. 进行二分图匹配。

  4. 每个集合都能匹配到炸弹时,\(x\) 即为一种合法解。

  5. 找到合法解中的最小值并输出。

自己可以试着实现以下。

但是打完之后发现自己还是 T 了,所以要剪枝,然后后面就是神仙思路了。

剪枝一

最优化剪枝。

可以预处理出炸掉目标 \(i...N\)(即 \(i\) 之后的目标点)的最少炸弹使用量 \(score[i]\)。

当搜索到目标点 \(i\) 时,如果“当前已经使用的炸弹数量 \(+ score[i] \geq\) 已有答案”,就剪枝。

这个剪枝还算好想的,具体实现见后。

剪枝二

可行性剪枝。

可以在搜索时计算出 \(MaxL\) 表示从当前点开始的集合的最大容量。

这样如果这个 \(MaxL=0\) 的话就没有往后搜的必要了(因为没有可行方案)。

具体实现之后再说。

剪枝三

减小搜索范围。

利用上面的 \(MaxL\),可以将搜索范围调整为 \(i+1...i+MaxL\),而不用 \(i...m\) 了。

这是个看似很简单但是很强力的剪枝。

总结一下

搜索+二分图匹配+三个剪枝=AC。

代码实现

代码实现较为冗长复杂,但是可以很好的锻炼代码能力。

预处理

首先声明几个变量:

\(f(i,s,t)\) 表示炸弹 \(i\) 能否炸毁 \(s..t\) 号目标点。

\(Max(i,s)\) 表示炸弹 \(i\) 从 \(s\) 开始炸能炸到的最远点,特殊的,如果 \(i\) 炸不到 \(s\),\(Max(i,s)=s-1\)。

然后就一步步预处理了:(注意注释)

double dis(node a,node b){return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);}

for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
f[i][j][j]=(dis(b[i],a[j])<=(k*k));//先判断单点。
for(int k=1;k<=n;k++)
for(int i=1;i<=m;i++){
Max[k][i]=i-1;//赋初值。
if(f[k][i][i]) Max[k][i]=i;
for(int j=i+1;j<=m;j++){
f[k][i][j]=(f[k][i][j-1]&&f[k][j][j]);//简单的DP。
if(f[k][i][j]) Max[k][i]=j;//简单的判断。
}
}

剪枝一

\(score(i)\) 表示炸掉目标 \(i...N\)(即 \(i\) 之后的目标点)的最少炸弹使用量。

for(int i=m;i>=1;i--){
int j=i-1;
for(int k=1;k<=n;k++) j=max(Max[k][i],j);//用一颗炸弹能到达的最远点。
score[i]=score[j+1]+1;//需知定理:score(i)>=score(j)(i<j)
}

剪枝二

定义几个变量:

\(now\) 表示当前搜索的目标点,\(sum\) 表示当前搜索的炸弹。

\(match(i)\) 表示与炸弹 \(i\) 匹配的集合的开始节点。

\(from(i)\) 表示以这个节点开始的集合匹配的炸弹。(如果它并非开始节点而是中间节点,\(from(i)=0\))

\(map(i,j)\) 表示炸弹 \(i\) 与节点 \(j\) 之间是否有边。

memset(vis,false,sizeof(vis));//标记是否使用过
queue<int>q;
int Maxl=now-1;//赋初值。
for(int i=1;i<=n;i++)
if(!match[i])
vis[i]=true,q.push(i);//找到没有匹配的炸弹。
while(!q.empty()){
int x=q.front();q.pop();
Maxl=max(Maxl,Max[x][now]);//刷新答案。
for(int i=1;i<=now;i++){
if(map[i][x] && !vis[from[i]]){
//如果前面的节点(i)与当前炸弹(x)有边,且这个节点(i)原本匹配的炸弹(from(i))还没有访问过。
//这就意味着可以通过:将 i 与 x 匹配,now 与 from(i) 匹配来增加匹配的数量。
vis[from[i]]=true;//标记。
q.push(from[i]);//往下传递。
}
}
}
if(Maxl==now-1) return;//剪枝。(没有可行计划就不要往下走)

剪枝三

挺简单的吧...

for(int i=Maxl;i>=now;i--){//改变顺序,记得倒序搜索。
for(int j=1;j<=n;j++) map[sum+1][j]=f[j][now][i];//建边(倒序便是为了方便建边)
dfs(sum+1,i+1);//往下搜。
}

二分图匹配

看了半天你可能会问:二分图有什么用啊。

不着急,我们知道二分图的核心是寻找增广路,而这就可以增加匹配的数量。

而我们的目的就是每次增加匹配数量,所以在每次搜所时,过了剪枝二之后都要进行匹配。

因剪枝二已经证明其可以增广,所以不用再判断。

具体代码如下:

bool Dfs(int x){//其实不用 bool,但是写习惯了...。
for(int i=1;i<=n;i++)
if(map[x][i] && !vis[i]){
vis[i]=true;
if(!match[i] || Dfs(match[i])){
from[x]=i;
match[i]=x;
return true;
}
}
return false;
}

代码综合

将以上信息综合就是 AC 代码了。(之前有注释的地方将不再给出注释)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<iostream>
#include<queue>
#define N 110
using namespace std; int m,n,k,f[N][N][N],Max[N][N],score[N],ans=0x3f3f3f3f;
int map[N][N],vis[N],match[N],from[N];
struct node{
double x,y;
}a[N],b[N]; int read(){
int x=0,f=1;char c=getchar();
while(c<'0' || c>'9') f=(c=='-')?-1:1,c=getchar();
while(c>='0' && c<='9') x=x*10+c-48,c=getchar();
return x*f;
} double dis(node a,node b){return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);} bool Dfs(int x){
for(int i=1;i<=n;i++)
if(map[x][i] && !vis[i]){
vis[i]=true;
if(!match[i] || Dfs(match[i])){
from[x]=i;
match[i]=x;
return true;
}
}
return false;
} void dfs(int sum,int now){
//结束条件。
if(now>m){
ans=min(ans,sum);
return;
}
//剪枝一。
if(sum+score[now]>=ans) return;
//剪枝二。
memset(vis,false,sizeof(vis));
queue<int>q;
int Maxl=now-1;
for(int i=1;i<=n;i++)
if(!match[i])
vis[i]=true,q.push(i);
while(!q.empty()){
int x=q.front();q.pop();
Maxl=max(Maxl,Max[x][now]);
for(int i=1;i<=sum;i++){
if(map[i][x] && !vis[from[i]]){
vis[from[i]]=true;
q.push(from[i]);
}
}
}
if(Maxl==now-1) return;
int from1[N],match1[N];
//为回溯做准备。
memcpy(from1,from,sizeof(from));
memcpy(match1,match,sizeof(match));
memset(vis,false,sizeof(vis));
//注意一下建图。
for(int i=1;i<=n;i++) map[sum+1][i]=f[i][now][Maxl];
Dfs(sum+1);
for(int i=Maxl;i>=now;i--){
//再建图。
for(int j=1;j<=n;j++) map[sum+1][j]=f[j][now][i];
dfs(sum+1,i+1);
}
//回溯。
memcpy(from,from1,sizeof(from1));
memcpy(match,match1,sizeof(match1));
return;
} int main(){
//输入。
m=read(),n=read(),k=read();
for(int i=1;i<=m;i++)
a[i].x=read(),a[i].y=read();
for(int i=1;i<=n;i++)
b[i].x=read(),b[i].y=read();
//预处理。
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
f[i][j][j]=(dis(b[i],a[j])<=(k*k));
for(int k=1;k<=n;k++)
for(int i=1;i<=m;i++){
Max[k][i]=i-1;
if(f[k][i][i]) Max[k][i]=i;
for(int j=i+1;j<=m;j++){
f[k][i][j]=(f[k][i][j-1]&&f[k][j][j]);
if(f[k][i][j]) Max[k][i]=j;
}
}
for(int i=m;i>=1;i--){
int j=i-1;
for(int k=1;k<=n;k++) j=max(Max[k][i],j);
score[i]=score[j+1]+1;
}
//搜索。
dfs(0,1);
printf("%d\n",ans);
return 0;
}

结语

康后是否有些自闭...。

可以结合代码和论文多理解几遍。

完结撒花。

最新文章

  1. swift相关
  2. usb驱动开发24之接口驱动
  3. thinkjs中自定义sql语句
  4. python中的enumerate
  5. PYTHON学习之路_PYTHON基础(8)
  6. JSF 2 textbox example
  7. rpm-bin
  8. Qt String 与char* char int之间的转换
  9. MongoDB 启动异常
  10. Redis 集成Spring(spring-data-redis)
  11. JavaScript HTML Dom改变HTML
  12. Line belt
  13. position:fixed not work?
  14. gets() 与 scanf() 的小尴尬
  15. 修改Ubuntu的aptget源为阿里源的方法
  16. linux 拷贝软连接文件
  17. spring boot 系列之六:深入理解spring boot的自动配置
  18. nginx expires配置
  19. Vue 父组件循环使用refs调用子组件方法出现undefined的问题
  20. bitset相关

热门文章

  1. Python练习题 001:4个数字求不重复的3位数
  2. 【题解】[SCOI]windy数
  3. Centos7安装MySQL8.0(RPM方式)
  4. 3-kubernetes监控与日志管理
  5. nrf528xx bootloader 模块介绍
  6. Python数据类型--集合(set)
  7. &lt;二分查找+双指针+前缀和&gt;解决子数组和排序后的区间和
  8. C语言中数组与指针的异同之处!你不知道的编程奥秘~
  9. trade可撤销贪心正确性证明
  10. spring cloud:搭建基于consul的服务提供者集群(spring cloud hoxton sr8 / spring boot 2.3.4)