//蒟蒻de了大半天的BUG,最后居然是输入时r打成了t导致的。
通过记录:
警告
对于这道题,网络流是必须的,请务必学懂网络流捏。但是这题完全可套网络流版子,蒟蒻不会网络流,看了一上午题解,学懂网络流后,也能较为清晰地写出此题。我的网络流blog正在生产。这题墙裂推荐这篇blog。大佬太强了!
题目描述
有法师 n n n 个,精灵 m m m 个,树木 k k k 个。法师要打精灵,攻击范围是 r r r ,并且有 C D CD CD 。树木会挡住攻击路径,使得攻击无效,树的半径是 r r r 。每个精灵一打即死。给出每个法师、树、精灵的求问:所有法师杀死所有精灵的最小时间。
算法思路
(本题解仅仅针对于刚了解网络流的萌新,讲解是怎样用网络流解题的,dalao看到了可以不屑地略过力)
显而易见,如果一个法师可以攻击,即CD转好了,就一定会攻击,毕竟这群家伙哪里会屯技能。于是,时间是 t t t 时,那对于每个法师,攻击的次数为 t / C D + 1 t/CD+1 t/CD+1 (要加1是因为大家在0秒时,CD都是好的)。
但是有攻击次数,不一定能打到人。这时候就要计算是否会被树给挡住了。这一部分涉及到计算几何。恭喜!!!本蒟蒻看到是纯纯数学问题,就去请教了同桌巨佬。上面的题解已经说的很清楚了,我就不多赘述了。总之,不管用什么方法,我们都会枚举每个法师、精灵、树,建立一个法师→可以杀→精灵的关系。
建立好之后,就有很多种做法了 (但是本蒟蒻为首的就没想到正解) 。我们首先会思考会不会有什么优秀的策略。比如说:对于同一个精灵,尽量被CD小的杀,或者说,对于同一个法师,尽量先杀其他法师不好杀的精灵。
但是!!!,这些想法是错误的。而且举出反例是十分轻易的。那么就意味着,优策略只有一条:能打就打不屯技能。那么我们暂时只能???枚举?
又有人会想到,或许可以DP。但是考虑任何变量为状态都是不可行的。为什么?因为DP成立需要每组“没有后效性”。而每个状态对应的法师CD是不一样的,而且会影响后面的击杀方案,所以,DP失败。
那么就传统思路来讲,这道题只能靠暴力枚举+剪枝来实现了。
但是我们注意到一个我们之前提到的规律,在时间确定时,法师能办到的最大攻击数是一定的,问题只在于把这些次数“分配”给哪些精灵。
这么一想,又想起来一个很明显的事情。如果 t t t 时间够法师杀疯了,那比 t t t 大的时间是一定也可以屠村的。也就是说,该时间是具有二分性的。
这样我们就想到了一个优化,也就是说我们可以二分时间,然后去枚举法师的分配方案。如果最后杀够了或者没杀够,就可以改一改二分,就过去了。
这种“分配”的思想出来之后,就会很自然地想到网络流。“水流”的过程,就是将“攻击次数”分配给精灵的过程。
下面证明网络流算法的可行性以及网络流对此题的处理。
首先,我们建立的有向边就是我们之前提到的:法师→可以杀→精灵的关系,流量就是1.因为精灵是秒杀,所以只分配一次攻击即可。这里推荐将精灵的“点编号”设置为精灵编号+
n
n
n 。
接下来是源点和汇点。源点我们可以建在0号点上,给各个法师提供击杀量。每个法师都要建立边,流量根据时间而改变,即上文提到的“
t
/
C
D
+
1
t/CD+1
t/CD+1 ”。汇点就建立在
n
+
m
+
1
n+m+1
n+m+1 就好。汇点的意义是什么呢?网络流里就是留到最后的所有的流量,放在这道题里就是意味着击杀了的精灵的总量。
至此,思路已经大致明晰,思考一下时间复杂度。n,m都是小于200的,总点数不过400,网络流部分时间复杂度 O ( n 2 m ) O(n^2m) O(n2m) ( n n n 为点数 m m m 为边数,对应到此题应该最大最大是 O ( ( n + m ) 2 n m ) O((n+m)^2nm) O((n+m)2nm) ,再来是二分部分。我们大可以莽一点,上限1e9,下限0,时间大概是 O ( log 2 1 e 9 ) O(\log_21e9) O(log21e9)。处理建边时,最多最多为 O ( n m ) O(nm) O(nm) 。所以统计一下,大概是 O ( n m + ( log 2 1 e 9 ) ( ( n + m ) 2 n m ) ) O(nm+(\log_21e9)((n+m)^2nm)) O(nm+(log21e9)((n+m)2nm)) ,代入数据,就是 O ( 1 e 8 ) O(1e8) O(1e8) 的样子,但是是远远达不到的,怎么可能哪一个数据故意卡法师精灵全攻击的大数据啊。所以时间上来说,是可行的。
重复一遍此题流程:1.根据数据,建立法师击杀精灵的对应关系(运用计算几何)。2.扫一遍所有精灵,如果存在有精灵没可能被任何人击杀,直接-1开润。并由此证明其他情况一定存在在一定时间内杀完。2.二分时间,并去判断汇点的总流量是否等于精灵数,并以此为根据二分。3.判断这个时间,跑一遍网络流。
如此,咱们可以痛快开写了。
特殊处理
很多网络流萌新可能跟我一样,在多次走Dinic时重新建边。这里有两个细节需要注意:
一个是,跑一遍网络流后,此时的图是残量图,需要重置之后才能进行下一次网络流。
另一个是,重置图是,不要去建边!!!一个是防止内存大。二个是,残量图虽然残,但他仍然具有影响,是肯定的不能留的。最好的方法是把每一个边的流量值更改回初始值就好了,而不是新建。
代码实现
#include<bits/stdc++.h>
using namespace std;
#define int long long//每日祖宗
int n,m,k;
int mid;
int s,t;
struct ren{
int x,y,r,t;
}wi[300],jl[300],tr[300];
struct road{
int v,dmbh,w;//每条边的信息包括(起点)终点、流量、反向边的编号
};
vector<road>edge[505];//蒟蒻前向星写得很臭,都爱领接表出淤泥而不染(bushi
int dis(int i,int j)//计算两点间距离
{
int dx=(jl[j].x-wi[i].x)*(jl[j].x-wi[i].x);
int dy=(jl[j].y-wi[i].y)*(jl[j].y-wi[i].y);
return dx+dy;
}
inline bool check(int i,int j)//判断是否被某棵树挡住
{//这部分是我的巨佬同学完成的,其他题解也很详细了(其实是我懒捏)
bool flag=1;
for(int q=1;q<=k;q++)
{
double xielv=1.0*(wi[i].y-jl[j].y)/(1.0*(wi[i].x-jl[j].x));
if(wi[i].x==jl[j].x)
xielv=1e-15;
double jieju=wi[i].y-1.0*wi[i].x*xielv;
double xl=-1.0/xielv;
double jj=tr[q].y-1.0*tr[q].x*xl;
double xx=(jj-jieju)/(xielv-xl);
double yy=xx*xielv+jieju;
if(xx+1e-5>min(wi[i].x,jl[j].x)&&xx-1e-5<max(wi[i].x,jl[j].x))
if(sqrt((xx-tr[q].x)*(xx-tr[q].x)+(yy-tr[q].y)*(yy-tr[q].y))-1e-5<tr[q].r)
flag=0;
}
return flag;
}
int dep[500];//网络流基础
queue<int>que;
inline bool bfs()
{//不要在这里学习网络流模板!不会一定要补好!不要浪费青春
memset(dep,0,sizeof(dep));
dep[s]=1;
que.push(s);
while(que.size()>0)
{
int u=que.front();
int S=edge[u].size();
for(int i=0;i<S;i++)
{
int v=edge[u][i].v,w=edge[u][i].w;
if((w!=0)&&(dep[v]==0))
{
dep[v]=dep[u]+1;
que.push(v);
}
}
que.pop();
}
return dep[t];
}
int dfs(int u,int in)
{
if(u==t)
return in;
int out=0;
int S=edge[u].size();
for(int i=0;i<S;i++)
{
if(!in)
break;
int v=edge[u][i].v,w=edge[u][i].w;
if( dep[v]==dep[u]+1 && (w!=0))
{
int minf=dfs(v,min(in,w));
out+=minf;
in-=minf;
edge[u][i].w-=minf;
int tot=edge[u][i].dmbh;
edge[v][tot].w+=minf;
}
}
if(out==0)
dep[u]=0;
return out;
}
inline void addedge()//正如我们之前所说,不是新建而是更改值
{
for(int i=1;i<=n;i++)//更改法师杀精灵的边
{
int S=edge[i].size();
for(int j=0;j<S;j++)
{
edge[i][j].w=1;
int v=edge[i][j].v,dm=edge[i][j].dmbh;
edge[v][dm].w=0;
}
}
int S=edge[t].size();
for(int i=0;i<S;i++)//更改精灵流向汇点的边
{
int v=edge[t][i].v;
edge[t][i].w=0;
int dm=edge[t][i].dmbh;
edge[v][dm].w=1;
}
S=edge[0].size();
for(int i=0;i<S;i++)//更改源点流向各个法师的边
{
int v=edge[0][i].v,dm=edge[0][i].dmbh;
edge[0][i].w=mid/wi[v].t+1;
edge[v][dm].w=0;
}
return ;
}
inline int all()//返回汇点的总流量
{
int ans=0;
s=0,t=n+m+1;
addedge();//更改边信息
while(bfs())
ans+=dfs(s,1e18);
return ans;
}
signed main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
cin>>wi[i].x>>wi[i].y>>wi[i].r>>wi[i].t;
for(int i=1;i<=m;i++)
cin>>jl[i].x>>jl[i].y;
for(int i=1;i<=k;i++)
cin>>tr[i].x>>tr[i].y>>tr[i].r;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
if(dis(i,j)<=(wi[i].r)*(wi[i].r))//首先判断是否在攻击范围内
if(check(i,j))//判断是否被挡住
{
int v=j+n;
road a;
a.v=v,a.w=1;
a.dmbh=edge[v].size();
edge[i].push_back(a);
a.v=i,a.w=0;
a.dmbh=edge[i].size()-1;
edge[v].push_back(a);
}
for(int i=n+1;i<=n+m;i++)//建立精灵流向汇点
{
int u=i,v=n+m+1,w=1;
road a;
a.v=v,a.w=w;
a.dmbh=edge[v].size();
edge[u].push_back(a);
a.v=u,a.w=0;//这里是反向边
a.dmbh=edge[u].size()-1;
edge[v].push_back(a);
}
for(int i=1;i<=n;i++)//建立源点流向法师
{
int v=i,w=0;
road a;
a.v=v,a.w=w;
a.dmbh=edge[v].size();
edge[0].push_back(a);
a.v=0,a.w=0;
a.dmbh=edge[0].size()-1;
edge[v].push_back(a);
}
for(int i=n+1;i<=n+m;i++)//喽一眼有没有精灵无解了
if(edge[i].size()==1)
{//数量为1意味着只有流向汇点的边,而没有流向法师的反向边
//故,它无解
puts("-1");
return 0;
}
int l=0,r=1e9;//开始二分
while(l<r)
{
mid=(l+r)/2;
if(all()==m)
r=mid;
else
l=mid+1;
}
printf("%lld\n",l);
return 0;
}