P4048 [JSOI2010]冷冻波

本文详细介绍了如何利用网络流算法解决JSOI2010中的冷冻波问题。从理解题意到算法思路,再到特殊处理和代码实现,作者通过实例解析了如何在存在法师、精灵和树木的情况下,计算法师杀死所有精灵的最短时间。文章适合网络流初学者,提出了二分时间结合网络流的解决方案,并提醒了网络流实现中的注意事项。
摘要由CSDN通过智能技术生成

//蒟蒻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) Olog21e9。处理建边时,最多最多为 O ( n m ) O(nm) Onm 。所以统计一下,大概是 O ( n m + ( log ⁡ 2 1 e 9 ) ( ( n + m ) 2 n m ) ) O(nm+(\log_21e9)((n+m)^2nm)) Onm+(log21e9)((n+m)2nm) ,代入数据,就是 O ( 1 e 8 ) O(1e8) O1e8 的样子,但是是远远达不到的,怎么可能哪一个数据故意卡法师精灵全攻击的大数据啊。所以时间上来说,是可行的。

  重复一遍此题流程: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;
	
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值