洛谷 P1262 间谍网络 Tarjan缩点例题+模板 Java实现

题目描述


  由于外国间谍的大量渗入,国家安全正处于高度的危机之中。如果 A 间谍手中掌握着关于 B 间谍的犯罪证据,则称 A 可以揭发 B。有些间谍收受贿赂,只要给他们一定数量的美元,他们就愿意交出手中掌握的全部情报。所以,如果我们能够收买一些间谍的话,我们就可能控制间谍网中的每一分子。因为一旦我们逮捕了一个间谍,他手中掌握的情报都将归我们所有,这样就有可能逮捕新的间谍,掌握新的情报。
  我们的反间谍机关提供了一份资料,包括所有已知的受贿的间谍,以及他们愿意收受的具体数额。同时我们还知道哪些间谍手中具体掌握了哪些间谍的资料。假设总共有n 个间谍(n 不超过 3000),每个间谍分别用 1 到 3000 的整数来标识。请根据这份资料,判断我们是否有可能控制全部的间谍,如果可以,求出我们所需要支付的最少资金。否则,输出不能被控制的一个间谍。

题目详细:洛谷-P1262 间谍网络

思路: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~

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值