HDU-5934 Bomb (Tarjan SSC)
这次训练赛美滋滋遇到两个比较水的图论题我能写,总算有用的图论选手感到舒适~
/*虽然因为卡这题很久所以最后还是掉到银牌线下面了,哎呀好气啊。而且如果出得快的话我们还有时间写一个二分D题的,气哭哭*/
题目大意
坐标系上有N个炸弹,第i个炸弹在坐标 ( x i , y i ) (x_i,y_i) (xi,yi)爆炸,有半径 r i r_i ri的范围内(包括边界)所有接触到的其他炸弹都能被引爆,但是引爆第i个炸弹需要花费 c i c_i ci的钱,问最少需要花多少钱才能把所有的炸弹都引爆。
思路
第一时间想到的是强连通分量,强连通分量里面的所有炸弹都能互相引爆,这样只要找强连通分量里面花费最少的点就行了。但是后来又想到如果是一条链或者是一条链上面带环这样的话,必须从链的一端引爆(不必再单独引爆后面连着的强连通分量里的所有点了)。
建图是O( n 2 n^2 n2)的,只要检查每个节点之间的距离和半径关系就可以了。
于是我先用一个普通DFS(从入度为0的点开始)把所有的带链的情况都引爆了,再用一个Tarjan求强连通分量求每个分量里面最小的去引爆,成功WA了。
中国好队友王大浑同志(今天的最佳辅助)成功找到了一组反例,如下:
Input
1
4
0 1 1 1
0 3 2 2
2 3 1 3
-2 3 4 4
Output
Case #1: 4
这个例子我的程序是把第二个点引爆了(因为是第二个点和第四个点的强连通分量里面比较小的),然后又把第一个点和第三个点给引爆了(它们各自是自己所在强连通分量唯一的节点),最终输出了6 。实际上这里的第四个节点已经能够把其余三个节点都引爆了。
恍然大悟,那么这道题的真实思路是这样的:先做一遍Tarjan强连通缩点(这样一来图就变成了无环的图),然后再用普通DFS去引爆入度为0的所有点就可以了,也就是我之前做法反一反。(等等,好像不需要DFS,只需要找入度为0的点不就可以了么???!!)
缩点以后新的节点的cost值就是原先强连通分量里面费用最小的点。
因为Tarjan的复杂度是O(n)的,所以整体复杂度其实是建图那里的O( n 2 n^2 n2)。。。
代码
#include <bits/stdc++.h>
using namespace std;
long long ans;
bool vis[1005];
bool vis2[1005];
stack<long long> ss;
long long timestamp[1005];
long long low[1005];
long long now;
vector<long long> egs[1005];
long long cost[1005];
long long degree[1005];
long long scc[1005];
long long mapper[1005][1005];
vector<long long> sccs;
struct nodes
{
long long x;
long long y;
long long r;
long long ind;
}no[1005];
void tarjan(long long num)
{
now++;
timestamp[num]=now;
low[num]=now;
vis[num]=true;
ss.push(num);
for(long long i=0;i<egs[num].size();i++)
{
long long nxt=egs[num][i];
if(timestamp[nxt]==0)
{
tarjan(nxt);
if(low[nxt]<low[num]){
low[num]=low[nxt];
}
}
else
{
if(vis[nxt] && timestamp[nxt]<low[num]){
low[num]=timestamp[nxt];
}
}
}
if(timestamp[num]==low[num])
{
long long miner=0x3f3f3f3f3f3f3f3f;
while(ss.top()!=num)
{
long long temp=ss.top();
miner=min(miner,cost[temp]);
vis2[temp]=true;//这个其实没有必要了,本来是用来防止普通DFS重复遍历的
scc[temp]=num;//标记缩完点以后原来点的新编号
vis[temp]=false;
ss.pop();
}
miner=min(miner,cost[num]);
scc[num]=num;
cost[num]=miner;
sccs.push_back(num);//标记新图里面有哪些节点,防止第二次DFS遍历旧图里的其他点。
ss.pop();
vis[num]=false;
}
}
void dfs(long long num)//新图的DFS,其实没有用,只要找到入度为0的点直接引爆就行了
{
if(vis2[num])
{
return;
}
vis2[num]=true;
for(auto i:sccs)
{
if(mapper[num][i])
dfs(i);
}
}
void solve(long long ca)
{
long long n;
scanf("%lld",&n);
ans=0;
memset(timestamp,0,sizeof timestamp);
memset(low,0,sizeof low);
memset(vis,0,sizeof vis);
memset(vis2,0,sizeof vis2);
memset(degree,0,sizeof degree);
memset(mapper,0,sizeof mapper);
memset(scc,0,sizeof scc);
sccs.clear();
while (!ss.empty())
ss.pop();
for(long long i=1;i<=n;i++)
{
egs[i].clear();
scanf("%lld%lld%lld%lld",&no[i].x,&no[i].y,&no[i].r,&cost[i]);
if(no[i].r<0)
{
no[i].r=0;//题目里面说r的范围可以为负,以防万一先特判去了
}
no[i].ind=i;//其实没用。。。
}
for(long long i=1;i<=n;i++)
{
for(long long j=i+1;j<=n;j++)
{
if(i==j)
{
continue;
}
long long l=(no[i].x-no[j].x)*(no[i].x-no[j].x)+(no[i].y-no[j].y)*(no[i].y-no[j].y);//判断在不在引爆范围内,避免一个double的运算损失精度
//邻接表建旧图
if(l<=no[i].r*no[i].r)
{
egs[i].push_back(j);
}
if (l<=no[j].r*no[j].r){
egs[j].push_back(i);
}
}
}
for(long long i=1;i<=n;i++)
{
if(timestamp[i]==0)
{
tarjan(i);
}
}
for(int i=1;i<=n;i++)
{
for(int j=0;j<egs[i].size();j++)
{
int nxt=egs[i][j];
if(scc[i]!=scc[nxt])
{
mapper[scc[i]][scc[nxt]]=true;//临界矩阵建新图(防止边重复,反正数据小)
degree[scc[nxt]]++;//入度增加,等会要引爆入度为0的点
}
}
}
for(auto i:sccs)
{
if(!vis2[i] && degree[i]==0)
{
ans+=cost[i];
dfs(i);//讲道理这个可以不要。。。算了反正没有T
}
}
printf("Case #%lld: %lld\n",ca,ans);
}
int main()
{
//freopen("test.txt","r",stdin);
long long t;
scanf("%lld",&t);
for(long long i=1;i<=t;i++)
{
solve(i);
}
return 0;
}
今天没有感谢文章了,这个题完全就是我们自己过的,要感谢都感谢队友们的栽培~