题目描述
由于外国间谍的大量渗入,国家安全正处于高度的危机之中。如果 A 间谍手中掌握着关于 B 间谍的犯罪证据,则称 A 可以揭发 B。有些间谍收受贿赂,只要给他们一定数量的美元,他们就愿意交出手中掌握的全部情报。所以,如果我们能够收买一些间谍的话,我们就可能控制间谍网中的每一分子。因为一旦我们逮捕了一个间谍,他手中掌握的情报都将归我们所有,这样就有可能逮捕新的间谍,掌握新的情报。
我们的反间谍机关提供了一份资料,包括所有已知的受贿的间谍,以及他们愿意收受的具体数额。同时我们还知道哪些间谍手中具体掌握了哪些间谍的资料。假设总共有n 个间谍(n 不超过 3000),每个间谍分别用 1 到 3000 的整数来标识。请根据这份资料,判断我们是否有可能控制全部的间谍,如果可以,求出我们所需要支付的最少资金。否则,输出不能被控制的一个间谍。
思路:Tarjan算法
算是Tarjan模板题了,首先利用Tarjan缩点,然后判断是否有不能到达的点。如果有输出`NO`和间谍编号,没有就继续计算每个缩点后的图SG,求和SG入度为0的所有点的最小花费。一个缩点后的点的最小花费是这个联通分量的最小花费,因为在这个联通分量中,收买任意一个,其他的也就被收买。money数组用于记录一个联通分量中的最小花费。为什么求SG中入度为0的点和呢,因为这些是必须收买的点,不收买没有人可以指控他。
下面来进行详细讲解,首先是你必须要会的知识:
联通、强联通分量、深搜 这些知识点本文章不会讲解,主要是Tarjan的讲解。
Tarjan算法是基于对图深度优先搜索的算法,每个强连通分量为搜索树中的一棵子树。搜索时,把当前搜索树中未处理的节点加入一个堆栈,回溯时可以判断栈顶到栈中的节点是否为一个强连通分量。
Tarjan中很关键的两个数组,dfn[ ]数组和low[ ]数组。
dfn[ ]数组:记录当前节点是被访问的次序,也就是被第几个访问的。这也就保证dfn数组中每个数都不一样。
low[ ]数组:记录当前节点可以访问到的最小的节点的编号dfn[ ]值。
当dfn[x]==low[x],代表着发现一个强联通分量。
Tarjan的流程,我们配合着代码来:
对一个点Tarjan,首先dfn[x]=low[x]=num++。然后让x入栈,这时入栈是为了后面划分联通分量。然后是遍历这个节点所能到达的所有边,也做Tarjan,就是深搜嘛..如果这个节点之前没做过Tarjan,那么让他先做,然后让low[x]等于x节点能遍历最小的节点的编号。如果这个节点做过Tarjan,那么判断这个节点在栈中。因为如果不在栈中,那么说明这个节点不属于这个强联通分量。这里不懂的去画个图,走一遍就知道了。因为出栈操作是发现了一个强联通分量,然后将这个强联通分量的所有节点全部出栈。所以这里需要判断这个节点在不在栈内。
然后就是如果发现low[x]=dfn[x],就是发现了一个强联通分量,那么对这个联通分量的所有节点做出栈操作,并且让这些节点的belong[ ]数组为同一个值,这个操作也就是缩点。将这些点等于同一个值,也就是缩成了一个新点。顺便记录下这个环的最小花费,这个操作只是为了解决这个问题,并不是模板需要。
个人有强迫症,所以感觉自己下面写的代码还行吧,可以做为模板来记,只需稍微改动即可。
void Tarjan(int x)
{
int t;
low[x]=dfn[x]=++num;
instack[x]=true; //是否在栈中
stack.add(x); //将x放入栈内,用后面于缩点染色
for(int i=0;i<g[x].size()&&g[x]!=null;i++) //遍历x节点的边
{
t=g[x].get(i);
if(dfn[t]==0)
{
Tarjan(t);
low[x]=Math.min(low[x],low[t]);
}
else if(instack[t]) //如果连接的t这个点在栈内,也就是遍历过t了。那么可让low[x]等于dfn[t]
low[x]=Math.min(low[x],dfn[t]);
}
if(dfn[x]==low[x])
{
++cnt;
while(!stack.empty())
{
t=stack.pop();
belong[t]=cnt; //将这个环的所有点都缩成一个新的数
instack[t]=false;
if(mingdan.containsKey(t)) //取这个环的最小花费
money[cnt]=Math.min(money[cnt], mingdan.get(t));
if(!stack.empty()&&t==x) //因为dfn和low相等的那个点不一定在栈底
break;
}
}
}
如果还不是很明白,可以看下我看的别的大佬的博客:Tarjan,讲的很明白,配合着图来、
附上代码:
然后通过Tarjan缩点后,这个题基本也就做完了。计算缩点后的图的入度,求入度为0的所有点的花费。以为入度为0的点时必须收买的。其他补位0的肯定可以通过其他点到达,不需要花钱。详细的过程看代码吧,主要是Tarjan算法的实现。
代码注释很详细,注意在看代码之前你要先学会Tarjan的思想和基本流程。不然你也看不懂,服了这个题我整整写了两天多,因为有需要其他知识和细节不知道。
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.StreamTokenizer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Stack;
public class Temp {
static int[] dfn;
static int[] low; //核心的数组
static boolean[] instack; //判断一个点是否在栈内
static int num,N,cnt;
static ArrayList<Integer>[] g; //邻接表存放收买关系的有向边
static Stack<Integer> stack=new Stack<Integer>();
static int[] money; //缩点后的图,每个点所需花费最少的金额
static int[] belong; //用于缩点染色
static int[] rd; //计算缩点后的图的入度
static HashMap<Integer, Integer> mingdan; //HashMap记录可收买的间谍所需的金额
public static void main(String[] args) throws IOException {
StreamTokenizer in=new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
PrintWriter pw= new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
int P,R,t,v,ans=0;
mingdan=new HashMap<Integer, Integer>();
in.nextToken();N=(int)in.nval;
in.nextToken();P=(int)in.nval;
dfn=new int[N+1];
low=new int[N+1];
money=new int[N+1];
belong=new int[N+1];
instack=new boolean[N+1];
rd=new int[N+1];
g=new ArrayList[N+1];
for(int i=0;i<=N;i++)
g[i]=new ArrayList<Integer>();
Arrays.fill(money, 0x7FFFFFFF);
for(int i=0;i<P;i++) //记录可收买间谍的金额
{
in.nextToken();t=(int)in.nval;
in.nextToken();v=(int)in.nval;
mingdan.put(t, v);
}
in.nextToken();R=(int)in.nval;
for(int i=0;i<R;i++) //记录间谍指控关系(记录有向边)
{
in.nextToken();t=(int)in.nval;
in.nextToken();v=(int)in.nval;
g[t].add(v);
}
//以上全是初始化,正文开始
for(int i=1;i<=N;i++) //以可收买的间谍为起点,使用Tarjan算法
if(mingdan.containsKey(i)&&dfn[i]==0)
Tarjan(i);
for(int i=1;i<=N;i++) //当缩点完,也就是相当于一次深度搜索,可以收买的间谍为搜索起点
if(dfn[i]==0) //如果这样深搜完,这个点没被遍历,则这个间谍无法被逮捕 输出NO
{
pw.println("NO");
pw.println(i);
pw.flush();
return;
}
for(int i=1;i<=N;i++) //注意!计算的是缩点后的图的入度
for(int j=0;j<g[i].size();j++)
{
v=g[i].get(j);
if(belong[i]!=belong[v]) //如果不是一个联通分量,那么v所属的联通分量的入度加一
rd[belong[v]]++;
}
for(int i=1;i<=cnt;i++)
if(rd[i]==0)
ans+=money[i];
pw.println("YES");
pw.println(ans);
pw.flush();
}
static void Tarjan(int x)
{
int t;
low[x]=dfn[x]=++num;
instack[x]=true; //是否在栈中
stack.add(x); //将x放入栈内,用后面于缩点染色
for(int i=0;i<g[x].size()&&g[x]!=null;i++)
{
t=g[x].get(i);
if(dfn[t]==0)
{
Tarjan(t);
low[x]=Math.min(low[x],low[t]);
}
else if(instack[t]) //如果连接的t这个点在栈内,也就是遍历过t了。那么可让low[x]等于dfn[t]
low[x]=Math.min(low[x],dfn[t]);
}
if(dfn[x]==low[x])
{
++cnt;
while(!stack.empty())
{
t=stack.pop();
belong[t]=cnt; //将这个环的所有点都缩成一个新的数
instack[t]=false;
if(mingdan.containsKey(t)) //取这个环的最小花费
money[cnt]=Math.min(money[cnt], mingdan.get(t));
if(!stack.empty()&&t==x) //因为dfn和low相等的那个点不一定在栈底
break;
}
}
}
}
附上个AC图吧:太不容易了,555~