P4819 杀人游戏 (图论 )

题目大意

  • 一位冷血的杀手潜入 Na-wiat,并假装成平民。警察希望能在 N 个人里面查出谁是杀手。
  • 警察能够对每一个人进行查证,假如查证的对象是平民,他会告诉警察,他认识的人, 谁是杀手, 谁是平民。假如查证的对象是杀手,杀手将会把警察干掉。
  • 现在警察掌握了每一个人认识谁。
  • 每一个人都有可能是杀手,可看作他们是杀手的概率是相同的。
  • 问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?

输入格式

  • 第一行有两个整数 N,M。
  • 接下来有 M 行,每行两个整数 x,y,表示 x 认识 y(y 不一定认识 x) 。

输出格式

  • 仅包含一行一个实数,保留小数点后面 6 位,表示最大概率。

样例

样例输入1

5 4 
1 2 
1 3 
1 4 
1 5 

样例输出1

0.800000

样例输入2

4 2
1 2
2 3

样例输出2

0.750000

样例输入3

7 6
4 1
5 4
2 1
7 3
1 6
2 5

样例输出3

0.714286

算法分析:

  • 这个题解法不止一种啊
  • 先来分析几个两种方法都要用的地方
  • 如果这里有4个人 并且关系是1知道2 2知道3 那么我们需要问几次呢? 答案其实是一次 因为我们如果问了1 就知道2 和 3的情况了 那么此时4显然也就知道了 因为如果1,2,3都不是杀手 那么4就是 如果1,2,3中出现杀手 ,那4就不是呗
  • 所以这个地方应该特殊处理掉
  • 关于我的解题方法
    • 感觉还是一眼裸爆出来的 但是码力不够啊…………
    • 分析题意很显然的一个东西就是 如果一个人的入度为0 那么我们必须要询问他 不然我们是无法知道他的情况的
    • 所以我们在输入的时候存下每一个节点的入度 然后通过dfs将每个入度为0的点都处理一遍 为什么是dfs呢? 因为如果我们通过一个点得知了他的子节点的情况 那么我们问他子节点的时候是不会有风险的 所以可以直接处理到头
    • 但是只是处理一遍入度为0的点显然是不可以的 如果有个环1--->2 2--->3 3--->1 没有入度为0的点仍然需要询问一次 所以我们在刚才处理入度为0的点的时候把所有遍历到的点都标记 然后再遍历所有的点 如果碰到没有标记的点显然就是在环中 处理掉
    • 然后就是关于上面提到的特殊处理了 我们可以用一个数记录当前节点dfs的次数 如果只是标记了一个点 就说明这个点不仅需要我们亲自询问 而且不会对其他点的询问作出贡献 因此我们可以孤立掉这个点不用加(相当于正常加上 flag标记真 如果最后flag为真就让ans--)

代码展示1

/*
关于为什么dfs要跑两遍
因为如果只是按照正序跑的话 
那么有种情况是:正序遍历的时候先把那个入度为0 
而且可以孤立的点给跑过了 
这时如果当前点连接了一个环 
那么不会判定当前点是可以孤立的
所以需要再倒序跑一遍 
使得当前点会放在后面处理 
这时候这个点连接的环已经被vis标记过了 
所以它不会更新其他的点(也就是此时的now-late==1)
这样才是正解 
这里附上数据理解
6 6
1 2
2 3
3 1
4 1
6 4
5 3
这个数据的正解应该是只会询问1次 也就是说答案是0.833333
但是如果不加倒序跑的话 答案会是0.677777
因为会先跑到编号为5的点 跑完之后ans会++ 而且编号为5的点不会使flag 标记为真
如果数据水的话可能不加倒序也能水过 但是要严谨嘛 
*/
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e6+10;
int head[maxn],cnt;
int rd[maxn];
int now,flag,ans;
int vis[maxn];

struct node{//正常邻接表
	int next,to;
}a[maxn];

void add(int x,int y){//正常建边
	a[++cnt].to = y;
	a[cnt].next = head[x];
	head[x] = cnt;
}

void dfs(int x){
	now++;
	vis[x] = 1;
	for(int i = head[x];i;i = a[i].next)if(!vis[a[i].to])dfs(a[i].to);
}

int main(){
	int n,m;scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;++i){
		int x,y;scanf("%d%d",&x,&y);
		add(x,y);
		rd[y]++;//记录每个点的入度
	}
	now = 0;//now记录当前操作之后有多少点被标记了
	for(int i = 1;i <= n;++i){
		if(rd[i])continue;//如果入度不为0就让它continue
		int late = now;//late是上次操作有多少点被标记了
		dfs(i);
		vis[i] = 1;
		++ans;
		if(now - late == 1)flag = 1;//如果当前操作只有一个点被标记过 令flag为真
	}
	now = 0;
	ans = 0;
	memset(vis,0,sizeof(vis));
	for(int i = n;i >= 1;--i){//倒序重新跑一遍 防止有的情况没有考虑到  具体解释见代码首
		if(rd[i])continue;//和上面的操作基本一样
		int late = now;
		dfs(i);
		vis[i] = 1;
		++ans;
		if(now - late == 1)flag = 1;
	}
        //处理完入度为0的情况了
	for(int i =  1;i <= n;++i){//处理环的情况
		if(vis[i])continue;//操作还是上面的操作
		int left = now;
		dfs(i);
		++ans;
		if(left - now == 1)flag = 1;
	}
	if(flag)--ans;//如果有孤立点可以不询问就让ans--
	printf("%.6lf",(double)1.0*(n-ans)/n);//强转 注意保留位数
	return 0;
}
  • 下面是一种偏大众的算法吧
    • 刚才也已经分析过了 大体思路就是找入度为0的点 然后环需要特殊处理 那么环怎样处理呢?
    • 看到环而且要处理掉 而且环中只要问一个人就可以知道剩下所有人的情况了 显然可以用tarjan缩点 缩点之后的强连通分量显然可以当成一个点来处理 , 而像之前所说那种因为这个环中的点互相指着导致没有入度为0的点的情况也就不会出现了 因为如果是刚才的情况将它缩点之后就变成了一个点了
    • 那么我们的算法雏形就出来了 剩下的细节看代码注释吧

代码展示2

#include<bits/stdc++.h>
using namespace std;
const int maxn = 3e5+10;

int dfn[maxn],rd[maxn],cnt,head[maxn],low[maxn];
int Time,top,tot,sta[maxn],ans,flag,size[maxn],bel[maxn];
int x[maxn],y[maxn];

struct node{
	int next,to;
}a[maxn];

void add(int x,int y){a[++cnt].to = y;a[cnt].next = head[x];head[x] = cnt;}

void tarjan(int x){
	low[x] = dfn[x] = ++Time;
	sta[++top] = x;
	for(int i = head[x];i;i = a[i].next){
		int v = a[i].to;
		if(!dfn[v]){
			tarjan(v);
			low[x] = min(low[x],low[v]);
		}
		else if(!bel[v])low[x] = min(low[x],dfn[v]);
	}
	if(dfn[x] == low[x]){
		tot++;
		while(1){
			int now = sta[top--];
			bel[now] = tot;
			size[tot]++;
			if(now == x)break;
		}
	}
}

int main(){
	int n,m;scanf("%d%d",&n,&m);
	for(int i = 1;i <= m;++i){//平平无奇的建边
		scanf("%d%d",&x[i],&y[i]);
		add(x[i],y[i]);
	}
	for(int i = 1;i <= n;++i)if(!dfn[i])tarjan(i);//裸的tarjan缩点板子
	memset(head,0,sizeof(head));
	cnt = 0;
	for(int i = 1;i <= m;++i){//没错还是个板子
		int u = x[i],v = y[i];
		if(bel[u] != bel[v]){
			add(bel[u],bel[v]);
			rd[bel[v]]++;//记得记录入度啊
		}
	}
	for(int i = 1;i <= tot;++i){
		if(flag == 0 && rd[i] == 0 && size[i] == 1){//flag == 0是个小优化 不加不会导致错误 但是会慢一些
		//能孤立点的特性:它显然首先要是个没有入度的点 它所指向的点不仅有它一条入边 而且它的size一定是1 即它不能是环
                  	int u = 0;
			for(int j = head[i];j;j = a[j].next){
				int v = a[j].to;
				if(rd[v] == 1) u = 1;
			}
			if(u == 0)flag = 1;//标记为1后下次就不用走这个if语句了因为最多只会有一个孤立点
		}
		if(rd[i] == 0)ans++;//如果入度为0 就让ans++ 因为必须询问它
	}
	if(flag)ans--;//同理 孤立点嘛
	printf("%.6lf",1 - (double)1.0 * ans/n);//强转 六位小数
}
posted @ 2020-07-17 20:26  HISKrrr  阅读(166)  评论(0编辑  收藏  举报