【题解】洛谷P4819 [中山市选] 杀人游戏

题目传送门
题面描述:

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


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


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


题意转化:有n个点,每个点都有一个身份。其中有 m m m个关系,每个关系给出 a a a b b b,表示 a a a认识 b b b b b b不一定认识 a a a),求最少要询问多少个人,才能知道所有人的身份(把那个杀手找出来)。

分析:

  • 我们要询问最少的人有什么用呢?
    假设有 n n n个人,我们询问了 k k k个点知道了所有人的身份。但是,杀手有可能在这k个人之中,也就是说,我们在询问这 k k k个人的时候就被杀掉了。对于每个点,他是杀手的概率是 1 n \frac{1}{n} n1,我们询问了 k k k个人,概率就是 k n \frac{k}{n} nk。所以,我们被杀的概率是 k n \frac{k}{n} nk,能够存活的概率就是 1 − k n 1-\frac{k}{n} 1nk

题意理解了之后,我们再思考做法。

考虑样例:
在这里插入图片描述
我们只要访问 1 1 1号节点,就能知道全部的身份,1号节点是杀手的概率是 1 5 \frac{1}{5} 51,所以安全的概率就是 4 5 \frac{4}{5} 54

那么样例太水,尝试来一个复杂一点的样例。
在这里插入图片描述
对于这个样例,我们只要访问1号节点知道2,3,4号点的身份和访问6号点知道5号点的身份就可以了。概率是 2 3 \frac{2}{3} 32

  • 现在我们已经大致理清楚了题意,但是题目上的图不免有些复杂了,我们能否尝试将图变得简单一点呢?
    对于一个集合而言,如果访问了集合内的一个点就知道了集合内所有人的身份,那么这个集合中的点都是等价的,相当于一个点,那么我们只要把他们当成一个点即可。又联想到有向图,我们想到了什么?强连通分量!

没错,这道题就是一道强联通分量缩点的例题。

在这里插入图片描述

图就变得简洁多了,我们只要访问1号点和六号点即可。

第一个猜想:缩点后入度为0的点的个数就是所求的最小点数
恭喜你,那么整整21分!

来组反例:
在这里插入图片描述

那么对于这幅图,我们实际上只要访问一号点知道1,2,3,4号点的身份,通过排除法就可以得到5号点的身份。即我们只需要访问一个人就可以知道所有人的身份。

我们在猜想:对于缩点后的一个点,如果他的大小为1,入度为0,出去的所有的点的入度>1,那么这个点就可以不用访问,不过我们最多只能不访问这么一个节点(用排除法的性质就可以想)


我们在屡一下思路:
  • 强联通分量缩点
  • 找出入度为0的点数k
  • 找出是否存在上述的特属于的点,记为布尔f
  • 答案为 1 − k − f n 1-\frac{k-f}{n} 1nkf

那么具体代码如下:

#include<bits/stdc++.h>
using namespace std;
struct node{
    int x,y,Next; 
}e[600010],e1[600010];
int linkk[600010];
int in[600010];
int linkk1[600010];
int dfn[600010];
int low[600010];
int stackk[600010];
int fam[600010];
int size[600010];
bool vis[600010];
int len=0,cnt=0,num=0,top=0,ans=0;
int n,m;
void insert(int x,int y){
    e[++len].Next=linkk[x];
    linkk[x]=len;
    e[len].y=y;
    e[len].x=x;
}
void insert2(int x,int y){
    e1[++len].Next=linkk1[x];
    linkk1[x]=len;
    e1[len].y=y;
}
void insert_new(){
    for (int i=1;i<=m;i++)
      if (fam[e[i].x]!=fam[e[i].y])
        insert2(fam[e[i].x],fam[e[i].y]),in[fam[e[i].y]]++;
}
void tarjan(int x){
    dfn[x]=low[x]=++cnt;
    stackk[++top]=x;vis[x]=1;
    for (int i=linkk[x];i;i=e[i].Next)
      if (!dfn[e[i].y])
        tarjan(e[i].y),low[x]=min(low[x],low[e[i].y]);
      else if (vis[e[i].y]) low[x]=min(low[x],dfn[e[i].y]);
    if (dfn[x]==low[x]){
	    ++num;
	    int t;
	    do{
		    t=stackk[top--];
		    vis[t]=0;
		    size[num]++;
		    fam[t]=num;
		}while (x!=t);
	}
}
int main(){
    scanf("%d %d",&n,&m);
    for (int i=1,x,y;i<=m;i++)
      scanf("%d %d",&x,&y),insert(x,y);
    for (int i=1;i<=n;i++)
      if (!dfn[i]) tarjan(i);
    len=0;
    insert_new();
    bool f=0;
    for (int i=1;i<=num;i++){
    	bool ff=0;
	    if (!f&&!in[i]&&size[i]==1){
	        ff=1;
		    for (int j=linkk1[i];j;j=e1[j].Next) if (in[e1[j].y]==1){ff=0;break;}
	   }
		if (ff) f=1;
		if (!in[i]) ans++;
	}
    printf("%.6lf",1.0-double(double(ans-f)/double(n)));
    fclose(stdin);
    fclose(stdout);
    return 0;
}
对于洛谷上的p1036题目,我们可以使用Python来解决。下面是一个可能的解法: ```python def dfs(nums, target, selected_nums, index, k, sum): if k == 0 and sum == target: return 1 if index >= len(nums) or k <= 0 or sum > target: return 0 count = 0 for i in range(index, len(nums)): count += dfs(nums, target, selected_nums + [nums[i]], i + 1, k - 1, sum + nums[i]) return count if __name__ == "__main__": n, k = map(int, input().split()) nums = list(map(int, input().split())) target = int(input()) print(dfs(nums, target, [], 0, k, 0)) ``` 在这个解法中,我们使用了深度优先搜索(DFS)来找到满足要求的数列。通过递归的方式,我们遍历了所有可能的数字组合,并统计满足条件的个数。 首先,我们从给定的n和k分别表示数字个数和需要取的数字个数。然后,我们输入n个数字,并将它们存储在一个列表nums中。接下来,我们输入目标值target。 在dfs函数中,我们通过迭代index来择数字,并更新取的数字个数k和当前总和sum。如果k等于0且sum等于target,我们就找到了一个满足条件的组合,返回1。如果index超出了列表长度或者k小于等于0或者sum大于target,说明当前组合不满足要求,返回0。 在循环中,我们不断递归调用dfs函数,将取的数字添加到selected_nums中,并将index和k更新为下一轮递归所需的值。最终,我们返回所有满足条件的组合个数。 最后,我们在主程序中读入输入,并调用dfs函数,并输出结果。 这是一种可能的解法,但不一定是最优解。你可以根据题目要求和测试数据进行调试和优化。希望能对你有所帮助!
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值